Preferences are saved client display state, things like which column a list is sorted by or whether it renders condensed. They live in a single preferences json column on the user, and the defaults come from config/user.php.
Only the keys a user has actually changed get stored. Everything else is filled in from config when the preference is read, so a brand new user has an empty column and still gets a complete set back.
That's the reason there's no backfill command or repair job anywhere. Add a key to config and every user has it on the next request. Remove one and it stops being returned, even though old rows still carry the value. The read is bounded by what config currently defines, so stored keys that no longer exist are simply dropped.
Writes work the same way. A patch merges into what's already stored rather than into the resolved set, which matters more than it sounds: if writes saved the resolved set, the first time someone changed one preference they'd freeze the current defaults for all the others into their row and stop tracking config forever.
The column is namespaced by scope, app and admin, because an admin is just a user with a role and one row has to carry both. The app profile only ever resolves the app branch and the admin profile only the admin branch, so admin preferences can't leak into a client payload that shouldn't see them.
Preferences are read off the profile endpoints and written with PATCH /preferences or PATCH /admin/preferences. Both take a partial set.
This is the part worth knowing before building against it. The server stores preferences and hands them back, but it never applies them to a query. Sort parameters on the list endpoints work exactly as they did before, and the client is still responsible for sending them.
That's deliberate. If an index endpoint silently fell back to a stored preference, the same URL would return different results for different users, which makes caching and debugging worse for no real gain. Treat preferences as client state that happens to persist server side, so it survives a new browser or a second device.
The practical consequence is that a preference only needs to be a preference if the client wants it remembered. Anything the server itself consumes, like locale or timezone, stays a real column instead. Anything you need to filter or sort users by wants a real column too, since querying inside a json column won't use an index.
See Account Management for the rest of what a user manages about themselves, and Admin for the admin side.