Skip to content

v0.93.0 — Actor model and the users table

Shipped 2026-09-19 — merged to main as 210b9c27, tagged v0.93.0, pushed. Built on version/0.93.0, eight commits, one per task. The fourth of the fifteen releases ROADMAP.md sequences from the August 2026 heuristic evaluation; the roadmap page is user-management-actor-model — phase A of the User Management Overhaul, adopted by the permissions overhaul as the part it inherits.

The platform had no user model of its own: Clerk's id string sat raw in thirty-odd columns, and the closest thing to a user record was a settings bag with is_admin bolted on. Now there is a users table, one server-side resolveActor and one client-side usePlatformAuth that every consumer goes through instead of the provider, and a username — the only identifier the UI shows for another person.

The table

  • users (U.1): id, kind, the provider's subject and name (the swap seam), username, email, phone, is_admin, timestamps. Both dialects, migration 0030.
  • The backfill (U.2): one row per identity the database already knew, id = <clerk id> (decision R1.5), so it is a pure insert with no UPDATE against any existing column. The inventory of user-bearing columns is read off the schema object; a newcomer gets their row on first sight.
  • user_settings folded in (U.3): keyed into users by a foreign key whose migration inserts the rows it needs first; is_admin lives on users, and every reader goes through one funnel.

The seam

  • resolveActor (U.4): req.actor = { userId, kind, capabilities } from the provider's JWT, behaviour-identical to today; trial and invited actors are later phases' additions in this one place. Impersonation rewrites the actor as it rewrites req.auth.
  • Routes consume it (U.5): 158 reads across 29 files; req.auth is read only by the seam, and a guard holds that.
  • usePlatformAuth (U.6): the client's one seam, wrapping the provider's hooks one-to-one for 34 files; user carries only what any surface may show of anyone; the viewer's own profile is useOwnProfile; the provider's account widget is AccountButton; App.tsx names no provider.
  • The guard (U.7): @clerk/* only under src/auth/ on the client, and a ratcheting allow-list of four files on the server.

The username

  • users.username (U.8): seeded from the provider's username or a derived handle, editable in Settings with the same chips a taken slug gets, and read by the display-name resolver from users rather than the provider. The client seam reports the platform's username, so every surface that broadcasts a name broadcasts the right one without changing.