Merge users and profiles (request for feedback)
This was partially discussed in https://github.com/pocketbase/pocketbase/discussions/225#discussioncomment-3511148 and other similar older discussions.
The current approach of having 2 separate entities (users and profiles) to manage the application users is not very convenient and has several drawbacks:
-
It is confusing (and unnecessary) to have both
userandrelationtype fields. -
Setting a profile field value during user create is too verbose and nonintuitive. Currently the process requires sending multiple non-transactional requests:
// create the user model await client.users.create({ email: "[email protected]", password: "123456", passwordConfirm: "123456", }); // authorize as the previously created user await client.users.authViaEmail('[email protected]', '123456'); // update the user profile associated to the previously created user await client.records.update('profiles', client.authStore.model.id, { name: "test", anotherCustomField: "Lorem ipsum...", }); -
The
profilescollection hascreateanddeleteAPI rules, but they are not really useful. Furthermore, because an empty profile is created automatically on user create, theprofilescreate API rule is not actually triggered (unless the profile record was deleted and then created manually). -
Because the users api doesn't have API rules, currently everyone is allowed to create a new user.
-
In addition to the above, there also no easy support for full "manager-subordinate" users management. While one user could be allowed to edit the profile data of another user, currently it is not possible, without using PocketBase as framework, to allow for example one user (eg. manager) to completely manage or delete another user (eg. subordinate).
Considering the above shortcomings, I propose to merge the user models and the profiles collection into a new single system collection called users.
This basically means that:
-
We will remove the
usertype field and migrate it existing usage to arelationtype field (pointing to the newuserscollection). -
We will be able to use the new collection's API rules to better control who can access and perform each crud user operation.
-
We will be able to create/update a user with just one request, something like:
await client.users.create({ email: "[email protected]", password: "123456", passwordConfirm: "123456", name: "test", anotherCustomField: "Lorem ipsum...", }); -
Because of the specifics and the edge cases related to the system fields (email, verified, password) we will have to modify the default behavior of the
record_upsertform.
While I'll try to automate as much as possible the required DB changes, this unfortunately will not be fully backward compatible. Of course, there will be a detailed migration guide, but still users will have to update manually their existing custom app code.
This issue is with high prirority because it will not be backward compatible and the quicker we introduce the changes the "less damage" it will cause.
Please comment with your feedback if you have any different use case or idea how to handle better the current implementation shortcomings (pseudo code and examples are also welcomed).
Honestly it sounds pretty good, as I don't need to create a a profile after user creation, but small question. Does this mean the user has to edit via the user's endpoint and user list had to be from that as well?
This sounds reasonable to me @ganigeorgiev, I like the plan as you have proposed it. It would be nice to have API rules for the user itself and full "manager-subordinate" permissions would be important for an app I'm working on. I also think working off a single user.id and not having to worry about user.profile.id could help simplify things from a dev perspective.
While I'll try to automate as much as possible the required DB changes, this unfortunately will not be fully backward compatible.
For my specific app this shouldn't be a problem at this point. Happy to delete my users and manually recreate them, but don't want to speak for others.
This issue is with high priority because it will not be backward compatible and the quicker we introduce the changes the "less damage" it will cause.
I definitely appreciate this. Would love to start building on the new user architecture sooner rather than later.
@theMackabu: Does this mean the user has to edit via the user's endpoint and user list had to be from that as well?
Good question. I haven't decided on that yet. Since the users will be a system collection, I think we can allow access from both services in order to minimize the breaking changes and for consistency with the admins service.
Or in other words, the following SDK calls would be equivalent:
client.users.getList(); <=> client.records.getList("users");
client.users.getOne("USER_ID"); <=> client.records.getOne("users", "USER_ID");
client.users.create({ ... }); <=> client.records.create("users", { ... });
client.users.update("USER_ID", { ... }); <=> client.records.update("users", "USER_ID", { ... });
client.users.delete("USER_ID"); <=> client.records.delete("users", "USER_ID");
Internally the users api action could "redirect" to the records api so that the actual handling will happen in a single place.
The create and update user handling will be almost identical as for a regular record, with the exception for the special system fields like email and password.
Personally I'd rather have one way of doing things over backwards compatibility (assuming it doesn't limit functionality). PocketBase is new enough that hopefully it wouldn't be unduly burdensome for folks to rewrite these sections of their apps. Allowing both is something you'll carry forward and new users will continually question which way they should do it and the difference between them. Maybe this is an unpopular opinion so thumbs down this if you disagree.
What would the update look like for changing passwords?
await client.records.update('users', 'RECORD_ID', {
password: '123456',
passwordConfirm: '123456',
});
@jimafisk Maybe you are right. I will think a little more on this when I start working on the proposed changes, because supporting both apis may cause also confusion which request event hook will be triggered (user or record), so maybe it would be better to throw an error if someone tries to update the users collection records via the regular records api (or the other way arround).
About the password change - in my local notes I have outlined 2 options to consider:
-
Don't allow users to directly update their email and password, requiring them to explicitly call
/api/users/request-password-resetand/api/users/request-email-change(at it is now). -
Allow users to update their email and password, but require to send also the old password for verification (when changing the password also require 2 new fields to read the new password from). Here are couple of examples how this could look like:
// change only the email await client.users.update('USER_ID', { email: '[email protected]', password: '123456', }); // change only the password await client.users.update('USER_ID', { password: '123456', newPassword: '654321', newPasswordConfirm: '654321', }); // change the email and password at the same time await client.users.update('USER_ID', { email: '[email protected]', password: '123456', newPassword: '654321', newPasswordConfirm: '654321', }); // change the email and password at the same time + other fields await client.users.update('USER_ID', { email: '[email protected]', password: '123456', newPassword: '654321', newPasswordConfirm: '654321', // other regular fields: name: "test", });
For admins we will have a separate endpoint which will allow changing all user system fields (email, password, verified) from the Admin UI, without requiring to enter the current user password beforehand.
Honestly the second option seems more in-line with how a lot of other services do it, require old password to set new one.
These changes sounds great to me!
I also agree that keeping both methods for the sake of backwards compatibility is not a good idea - just remove the old method. Users should expect breaking changes before 1.0
I was able to experiment a little with the proposal and the suggestions from above and here are a little more details about the planned changes.
I think the user model and it related services could be completely removed.
Instead we will introduce the following collection types:
- auth
- catalogue (default)
- single (for example to handle app config/options; #159)
(the names of the types are subject to change; suggestions are welcomed)
Or in other words, the "users" will be replaced with auth collection(s).
All existing user auth routes will be replaced with a corresponding collection route, for example:
/api/users/auth-via-email => /api/collections/users/auth-via-email
The SDKs will no longer have a client.users and client.records services and instead a new collection service will be created that will be accessible through client.collection(ID_OR_NAME).*. For example:
// base crud:
client.records.getFullList("test", ) => client.collection("test").getFullList()
client.records.getList("test", ) => client.collection("test").getList()
client.records.getOne("test", "RECORD_ID") => client.collection("test").getOne("RECORD_ID")
client.records.create("test", {}) => client.collection("test").create({})
client.records.update("test", "RECORD_ID", {}) => client.collection("test").update("RECORD_ID", {})
client.records.delete("test", "RECORD_ID") => client.collection("test").delete("RECORD_ID")
// auth collection only:
client.users.authViaEmail(...) => client.collection("test").authViaEmail(...)
client.users.authViaOAuth2(...) => client.collection("test").authViaOAuth2(...)
client.users.refresh() => client.collection("test").authRefresh()
client.users.requestPasswordReset(...) => client.collection("test").requestPasswordReset(...)
client.users.confirmPasswordReset(...) => client.collection("test").confirmPasswordReset()
client.users.getAuthMethodsList() => client.collection("test").getAuthMethodsList()
client.users.getExternalAuthsList(...) => client.collection("test").getExternalAuthsList(...)
client.users.unlinkExternalAuth(...) => client.collection("test").unlinkExternalAuth(...)
// deleted:
[X] client.users.requestEmailChange(...)
[X] client.users.confirmEmailChange(...)
The special @request.user.* filter rule will be replaced with @request.auth.*.
You will be able to query the auth collection name (@request.auth.collectionName) allowing you to even use different auth collections for roles/groups separation.
A new manage action and API rule will be added for the auth collections to allow manager/supervisor auth models to be able to change the special subordinate fields like email, password, verified, etc. without providing the original user password.
The owner of the auth model will be able to change their email or password via the update action if authorizationPassword is provided, for example:
// change only the email (the verified state will be auto set to `false` after this call)
await client.collection('users').update('RECORD_ID', {
email: '[email protected]',
authorizationPassword: 'old_pass',
});
// change only the password
await client.collection('users').update('RECORD_ID', {
password: 'new_pass',
passwordConfirm: 'new_pass',
authorizationPassword: 'old_pass',
});
// change the email and password at the same time + some other regular field(s)
await client.collection('users').update('RECORD_ID', {
email: '[email protected]',
password: 'new_pass',
passwordConfirm: 'new_pass',
authorizationPassword: 'old_pass',
// regular fields:
name: 'test',
});
The authorization token schema will be included in the token, meaning that you no longer will have to prefix the token with Admin or User:
Authorization: Admin TOKEN => Auhtorization: TOKEN
Authorization: User TOKEN => Auhtorization: TOKEN
There will be also a lot of other changes to the internals, for example to simplify the Record model accessors we will rename:
SetDataValue -> Set
GetDataValue -> Get
GetBoolDataValue -> GetBool
GetStringDataValue -> GetString
GetIntDataValue -> GetInt
GetFloatDataValue -> GetFloat
GetTimeDataValue -> GetTime
GetDateTimeDataValue -> GetDateTime
GetStringSliceDataValue -> GetStringSlice
For example, you'll be able to access the user email via record.GetString("email") (this is to unify the record data access API when PocketBase is used as framework).
Overall this will be a major refactoring and a lot of components will change. As mentioned in https://github.com/pocketbase/pocketbase/discussions/123#discussioncomment-3539859, new features will be pushed back for a while, so please be patient. In short, the next couple of weeks I'll try to work on:
- [ ] Finalize the core code changes.
- [ ] Refactor all unit and integration tests.
- [ ] Refactor the admin UI and incorporate the collection types in the design.
- [ ] Refactor the JavaScript and Dart SDKs to reflect the changes (including their unit tests).
- [ ] Redesign the documentation (maybe it will be a good idea to cleanup the code and finally make it public)
- [ ] Prepare a migration script for the database
- [ ] Prepare a migration guide for all other changes that cannot be automated
This probably will be the last major breaking change before v1.0.0 and if you have any other ideas or suggestions, please add them as comment, so that I could consider them during the refactoring.
@ganigeorgiev some feedback:
in the example above you used "test" as the collection name for both the catalogue type collection and the auth type collection right:
// base crud:
client.collection("test").getFullList()
client.collection("test").getList()
...
// auth collection only:
client.collection("test").authViaEmail(...)
client.collection("test").authViaOAuth2(...)
but this would not be the case for a real app right? you could not have the same name for two different collections - so maybe you would use "test" for the catalogue collection and maybe "users" for the auth collection?
just want to make sure I'm following along.
as for the collection type names, I like both auth and single but not sure about catalogue. I don't have a better suggestion though :) just does not feel very natural to me
when it comes to the naming changes, I'm also not sure.
previously, we had:
client.records.getList("test", ) // get list of records
client.records.getOne("test", "RECORD_ID") // get one record
client.records.create("test", {}) // create a record
client.records.update("test", "RECORD_ID", {}) // update a record
client.records.delete("test", "RECORD_ID") // delete a record
with the new naming changes:
client.collection("test").getList() // does not get list of collections?
client.collection("test").getOne("RECORD_ID") // does not get a collection?
client.collection("test").create({}) // does not create a collection?
client.collection("test").update("RECORD_ID", {}) // does not update a collection?
client.collection("test").delete("RECORD_ID") // does not delete a collection?
to me this makes less sense! but I guess it's personal preference
maybe:
client.collections("test").records.getList() // get list of records
client.collections("test").records.getOne("RECORD_ID") // get one record
client.collections("test").records.create({}) // create a record
client.collections("test").records.update("RECORD_ID", {}) // update a record
client.collections("test").records.delete("RECORD_ID") // delete a record
could work but I'm guessing you have thought of this already and probably had a good reason not to choose this (maybe too verbose) 😅
regarding the changes to the auth collection:
client.users.authViaEmail(...) => client.collection("test").authViaEmail(...)
client.users.authViaOAuth2(...) => client.collection("test").authViaOAuth2(...)
client.users.refresh() => client.collection("test").authRefresh()
client.users.requestPasswordReset(...) => client.collection("test").requestPasswordReset(...)
client.users.confirmPasswordReset(...) => client.collection("test").confirmPasswordReset()
client.users.getAuthMethodsList() => client.collection("test").getAuthMethodsList()
client.users.getExternalAuthsList(...) => client.collection("test").getExternalAuthsList(...)
client.users.unlinkExternalAuth(...) => client.collection("test").unlinkExternalAuth(...)
is there a specific reason why you want to specify the name of the collection here (in this example "test") versus it being a internal constant value? do you want to support people having multiple auth collections?
if not, then maybe it could still make sense to use something like client.users even though it now uses a collection under the hood rather than another model.
why I personally would prefer client.users over something like client.collection("users") comes down to three reasons:
- it is shorter.
const { token, user } = await locals.pocketbase.users.authViaEmail(email, password);
is already pretty long and
const { token, user } = await locals.pocketbase.collection("users").authViaEmail(email, password);
would be even longer
-
more convention over configuration makes for more readable code whenever you look at other peoples pocketbase auth code
-
the difference between
client.usersandclient.collections("test")makes it obvious in the code that they have different uses/APIs (the former has.authViaEmail(...))
should be noted that 1), 2) and 3) are not major issues but minor nitpicks.
finally, regarding
if you have any other ideas or suggestions, please add them as comment, so that I could consider them during the refactoring.
not sure if this is what you meant, but I would like to make another push for https://github.com/pocketbase/pocketbase/issues/312 if you are anyways refactoring collections. I've used pocketbase in two web apps so far and indirect expand would have been very useful in both of them!
sorry for the long post, hopefully some of it might have been useful.
again, these changes look great and I'm sure they will be great for the pocketbase project regardless of what function names are used.
thanks!
@ollema Thanks for the feedback.
in the example above you used "test" as the collection name for both the catalogue type collection and the auth type collection right:
The idea is that the auth collection will have everything what the default catalog colleciton has + additional auth endpoints and some special system fields (email, verified, passwordHash, etc.). Or in other words, you could treat the above example as if the "test" colleciton is auth.
as for the collection type names, I like both auth and single but not sure about catalogue. I don't have a better suggestion though :) just does not feel very natural to me
I also don't like catalog (or catalogue) but I can't think of anything better. Initially I considered "list", but then in the code it kinda read strange when collection.IsList() is called (eg. someone may think that this is slice or similar).
why I personally would prefer client.users over something like client.collection("users") comes down to three reasons:
The idea of introducing client.collection(ID_OR_NAME).* is to unify the crud and auth actions in a single service and to allow multiple auth collections (eg. you could want the manager auth collection to have different fields than the subordinate auth collection).
Keeping both client.users and client.records could lead to some strange inconsistencies and duplication of the crud services. I think it is a lot easier to reason when everyhing is a collection and can be accessed through the same api (eg. you don't have to know whether a collection is auth, catalog or single in order to call create or update).
Yes, it is a little longer to type, but users coming from Firebase may find this pattern familiar.
Initially I considered "list"
Was coming here to make the same suggestion. I thought list made more sense than catalogue but I see your point @ganigeorgiev. Just spitballing, is the "collection" naming open to change? It's a little strange to have a single collection in the first place. Maybe the overall structure could just be "data" instead of "collection" of which you have auth, collection, and single types. Not sure if that conflicts with other PB concepts so sorry to add noise, just wanted to throw other ideas out there.
@jimafisk We can change "collections", but "data" is too broad/general as a term and I don't think it is better in this case. In practice, users rarely will have to work directly with the types, since in the Admin UI we will have human readable labels and probably a short description what each collection type do and expect to store.
An alternative to "single" I considered previously also "oneoff" but I dropped the idea because the spelling could be ambigious (one_off, oneOff, etc.).
About the SDK service naming - Instead ofclient.collection(name) we can use client.use(name) (similar to how in SQL you target specific database) as a more shorter version. For example:
client.collection('users').getList() vs client.use('users').getList()
client.collection('users').authViaEmail() vs client.use('users').authViaEmail()
Personally, I prefer collection() because it is clear what actually the method is referring/targetting.
// auth collection only: client.users.authViaEmail(...) => client.collection("test").authViaEmail(...) client.users.authViaOAuth2(...) => client.collection("test").authViaOAuth2(...) client.users.refresh() => client.collection("test").authRefresh() client.users.requestPasswordReset(...) => client.collection("test").requestPasswordReset(...) client.users.confirmPasswordReset(...) => client.collection("test").confirmPasswordReset() client.users.getAuthMethodsList() => client.collection("test").getAuthMethodsList() client.users.getExternalAuthsList(...) => client.collection("test").getExternalAuthsList(...) client.users.unlinkExternalAuth(...) => client.collection("test").unlinkExternalAuth(...)
I'm confused with this API. As in, if a collection wasn't an auth collection are these methods going to exist on it? For example, if I did
client.collection("products").authViaEmail(...) // <- where products is obviously not an auth collection
Is that valid?
@blackmann Yes, you will be able to call that, but you'll get an API error telling you that the referred collection is not an auth.
TLDR; Mixing auth methods with the base collection methods is an overextension of its responsibilities
IMO, I think this design/API will become convoluted. Because as auth collections get extended with new methods, they'll be applied to collections in general. I (personally) find that weird.
How about, say a more explicit, name like client.auth("managers") which will just return a result with these methods as an extension of [the base] collections.
I also want to use this opportunity to say, this is really great work and I'm using this in my next SaaS project starting this weekend. It's providence that I found this.
Thank you!
Stumbled on it from some Youtube shorts!
Mixing auth methods with the base collection methods is an overextension of its responsibilities
But this is currently how the client.users.* service is organized.
client.users.getFullList()
client.users.getList()
client.users.getOne()
client.users.create()
client.users.update()
client.users.delete()
client.users.refresh()
client.users.authViaEmail()
client.users.authViaOAuth2()
...
How about, say a more explicit, name API like client.auth("managers") which will just return a result with these methods as an extension of [the base] collections.
I thought of this, but I don't like that it collides with client.authStore.* and it is not consistent with client.admins.* (the admins will not be a collection since they have different business logic binded to them, like skipping API rules, special super-access middlewares, etc.).
An alternative to "single" I considered previously also "oneoff"
I agree that single is better, I don't have a problem with it, just wanted to think it through.
Personally, I prefer collection() because it is clear what actually the method is referring/targetting.
I'd tend to agree that collection('whatever') is more clear than use('whatever').
I think this design/API will become convoluted. Because as auth collections get extended with new methods, they'll be applied to collections in general.
I would assume the extension would apply to auth collections only?
@ganigeorgiev - the API decisions you've made so far have be great, so I'm definitely comfortable with you making executive decisions on what you think is best.
@jimafisk I would assume the extension would apply to auth collections only?
Yes, auth related methods will be available only for auth collections. Obviously, since the collection is accessed via its name or id, nothing will stop you to us an auth method with a non-auth collection, as @blackmann mentioned, but you'll get an API error.
The same will be true if we move the auth related methods in a separate auth(name) service. We will have a type checker on the server-side no matter the SDK API. The whole complication around client.collection(name).* is because I want to support multiple auth collections (at work I have a use for that for a small internal company knowedge base, where there is expected to have 2 different auth models - staff and client and both have different set of fields).
@ganigeorgiev what I mean is...
How the current client.users.* is great. Because you could only access those methods .authVia*, .confirmPassword, etc on just the client.users but not other collections.
In other words, intentions should be clear while reading code instead of cross-checking with admin panel or during runtime.
But as @jimafisk said, you'll have to make the executive decision. I'm just a troublemaker, haha.
@blackmann : In other words, intentions should be clear while reading code instead of cross-checking with admin panel or during runtime.
I get what you mean but I think even if we ended up choosing the client.collection().* API, the name of the collection should be enough to make it clear while reading the code what is the intention of the collection (eg. "users", "clients", etc.). Tomorrow after work I'll research other libraries for ideas/inspirations and will post here if I stumble on something.
In the meantime, the TLDR version of the above discussions, seems to be around:
-
The names of the 3 new collection types -
auth,catalog,single. -
The changes in the SDKs API. Based on the feedback, I think we can narrow the options to these 2:
client.records.getList("users"); vs client.collection("users").getList(); client.records.getOne("users", "RECORD_ID"); vs client.collection("users").getOne("RECORD_ID"); client.records.create("users", {}); vs client.collection("users").create({}); client.records.update("users", "RECORD_ID", {}); vs client.collection("users").update("RECORD_ID", {}); client.records.delete("users", "RECORD_ID", {}); vs client.collection("users").delete("RECORD_ID", {}); client.auth.viaEmail("users", "[email protected]", "123456"); vs client.collection("users").authViaEmail("[email protected]", "123456"); client.auth.viaOAuth2("users", ...); vs client.collection("users").authViaOAuth2(...); // other auth related methods...
(for now, I'm inclined more towards the latter because it is consistent with the existing admins service and it also may feel familiar to Firebase users).
I'm still a newbie but I like client.collection("users").authViaEmail("[email protected]", "123456") more than client.auth.viaEmail("users", "[email protected]", "123456") (it is also similar to how it is now with client.users).
Could you also add the auth methods to the api preview in the dashboard for the auth collections? I often use the preview to copy paste the code and I think it will be useful for the auth methods too.
Could you also add the auth methods to the api preview in the dashboard for the auth collections? I often use the preview to copy paste the code and I think it will be useful for the auth methods too.
@denisgurev Yes, I will consider it when redesigning the Collections UI.
The names of the 3 new collection types - auth, catalog, single.
Other suggestions instead of collections with collections types auth, catalog, single:
-
collectionswith the followingcollectionstypes:-
auth, ~list~ (confusing),single -
auth,multiple,single -
auth,set,single -
auth,series,single -
auth,array,single(this kinda sucks)
-
-
datawith the followingdatatypes:-
auth,collection,single
-
Did I miss anything?
I honestly like almost every example listed above here more than catalog, but of course that is just personal preference.
I especially like replacing collection with data, that feels very at home to me!
The changes in the SDKs API. Based on the feedback, I think we can narrow the options to these 2:
If you are looking for user input, I would like to cast my vote for option 1 for reasons stated earlier in the thread
@ollema I honestly like almost every example listed above here more than catalog, but of course that is just personal preference.
data is too common and miscellaneous as a term, because everything could be data and will require special naming for local variables when working with the "data" models. collection is a little more specific and doesn't require too much additional context to understand what the model/variable store.
I thought of multiple initially, but I wasn't sure whether it was clear enough. Now that I think on it again, I'm starting to like it - it is a direct antonym of single and it also reads well in code (eg. collection.IsMultiple()). I'll still do a further research, but I think it is a good alternative to catalog.
While the collection type is not critical even if we change them in the future, because most users will use the Admin UI to manage their collections, the SDK public API on the other hand is problematic, because I don't want to introduce another major breaking change in the future. Once I finalize the pure backend changes, I'll spend a little more time on this to explore other options.
thanks @ganigeorgiev - very valid points, sounds great!
Hi, just became aware of this discussion due to the linking in another issue. It's a long thread with a lot of things discussed. I would like to summarize the most important things at the beginning for me (the following discussions are mostl about naming and conventions for CRUD services) and would like to add some notes:
- Merge collection "user" and "profile" in a new system collection "users" which are currently connected via a relation
- New system collection "users" should be used as a normal collection ("clien.records.getList('users')") rather using it as a special resurce ("client.users.getList()")
- Use not a new system collection "users" but have
collection types(auth, catalogue, single); thecollection typeauthhas special CURD service like `authViaEmail, etc. - Use new API rules to query
@request.auth.*to query different collections with the tpyeauth; using different collections for users would allow to have roles like "admin"-users are in the collection "admin", "member"-users are in the collection "member"
Hope that matches the most important points; especially the new collection type auth.
I'm not a big fan of mixing the special process of authentication with the underlying resources user / profile / account (however you are calling it) with the normale resources stored in the collection. I do understand that the basic CRUD services like "list all", "get one", "create", and so an are technically the same and can be internally shared in the core app. But the resources "user" / "profile" will ever have some special methods or implementation for example to sanitize sensitive information like password or a method which will update the current password. That's why I would say that the underlying resources for the authentication should be handled separately. PocketBase is not an IAM or a fully identity solution; that's why I would keep it simple. Products like Firebase are separating the resource user and the corresponding process of authentication too (see for example https://firebase.google.com/docs/reference/js/v8/firebase.User ). Plus, in future there will be additional feature requests to include special processes or special attributes like mobile number verification and authentication, OTP code, and very important to have a federation of users. I think it's easier to keep it separately.
Personally, I'm a fan of separating the resources user and profile. The resource user is to manage the authentication process and profile is used to save additional information of the user which may change over the time. I like the idea of having a simple key/value store managed by the user-land code. Looking, for example at Keycloak, the have an additional relation table user_attribute. A technical implementation not have an additional relation may to use the existing table user and have an attribute profile as a JSON document for a key/value store. That would mean I can store more or less anything from user-land code as long as I'm responsible for un/marshalling it.
To be honest, I do not the like the idea of having a special collection type auth. I do not understand the benefits for this. I'm managing all my DB updates with the migrations to have a streamlined CI / CD that results in the same database in the different stages. By using a collection type auth you must ensure that the database migration files will have all necessary attributes / fields like username, password, email, and so on. I think this results in a heavy mess in the future by having new feature requests like for example OTP, federated users, and so on. I do unterstand the technically background to share the underlying methods for the collections (which are basically SQL tables) and to provide the same CRUD services to the SDK API. But again, for me the authentication process and underlying resource are too important and special to mix; plus new feature requests make it more difficult to implement.
You have mentioned two additional benefits for using the special collection type auth as follows
- Using different collections for users would allow to have roles like "admin"-users are in the collection "admin", "member"-users are in the collection "member"
- Use new API rules to query
@request.auth.*to query different collections with the typeauth
I think that these points are not necessarily linked to a new collection type auth.
Using different collections for different kind of users (like admin, member, staff, etc.) may result in multiple account for the same human person (human person "max" may be a "member" and a "staff", and so on). Linking OAuth provider would be quite tricky; federating user identities as well, SSO would not be possible. I would rather see a new attribute "roles" which may be in the core app or is a user-land managed attribute.
Haven't yet used API rules in details but I'm using Open Policy Agent (OPA) a lot; it's the same idea. As a vision I would LOVE to have OPA integrated in PocketBase API rules. In the meantime it would be great to have full access to the request-input (or the authenticated user; JWT and user-model) to access all user-land attributes like "roles". With that you could implement an RBAC, ABAC, and a full "manager-subordinate".
So, long post, short:
- Do not mix authentication (and related resource like "user") with collections
- Have a key/value store for "profile" (additional table, additional JSON attribute in the same table)
- No special collection type
auth - Extend API rules to have full access to the JWT and authenticated user model (for example to get the attributes"profile.roles")
- Implement OPA for API rules and JWT / user model is the input data
Sorry, to have different oppinion. Hope this helps in the discussion.
@mbecker Thanks for the feedback.
Personally, I'm a fan of separating the resources user and profile.
You still will be able to create your own profile collection and create a relation to it if you want to keep your users data in a separate collection.
By using a collection type auth you must ensure that the database migration files will have all necessary attributes / fields like username, password, email, and so on.
The existing user model fields (email, verified, passwordHash, etc.) are system fields and will be automatically created for each auth collection (and underlying table) and they cannot be deleted or renamed. From the Admin UI you will be able to manage only the configuration of your custom fields (the same as it is now with the profiles collection).
To be honest, I do not the like the idea of having a special collection type auth. I do not understand the benefits for this.
The main benefits of treating the users models as a regular collection are:
- there is no need for special search handling
- you'll have API rules to control who is allowed to create users
- you could have multiple auth collections supporting auth models with different set of fields, not just "role".
- one
authcollection can be configured to be allowed to fully manage the users in anotherauthcollection (including changing their password, email, verified state without prior confirmation to be required).
Using different collections for different kind of users (like admin, member, staff, etc.) may result in multiple account for the same human person (human person "max" may be a "member" and a "staff", and so on). Linking OAuth provider would be quite tricky; federating user identities as well, SSO would not be possible.
If your auth models have the same set of fields, then you could just use a single auth collection (by default "users" will be created). OAuth2 linking and everything related to the authentication will be only within the collection namespace.
Extend API rules to have full access to the JWT and authenticated user model (for example to get the attributes"profile.roles")
This is already supported. You should be able to query the user profile fields by doing @request.user.profile.*.
Implement OPA for API rules and JWT / user model is the input data
I don't plan implementing an extra access layer. It will complicate both the management in the UI and when used as framework. The event hooks are the recommended way to extend and tailor the default PocketBase behavior if users need more fine grained auth controls than what the API rules provide.
While the field type “user” may be identical to a relation field pointing at the user table, I do think it’s a comforting and nice feature to have this dedicated “user” field type.
Also, I do think it’s important to not force PII like email into a public facing record. That appears to be one benefit of the separate “profile” table.
@lukepighetti Based on the several raised issues and discussions in the past, most users seems to find it confusing to have both a user model and profiles collection (for example https://github.com/pocketbase/pocketbase/discussions/242).
Having only one entity ("collection") should be a lot easier and intuitive for the developers.
Also, I do think it’s important to not force PII like email into a public facing record. That appears to be one benefit of the separate “profile” table.
I agree, but since the users will be a collection, you will be able to limit the access to the user records with the collection API rules.
Users will always have the option to create their own profiles collection and reference it with a relation field. The only difference from the existing implementation is that it will not be created by default.