feat(identity): identity layer foundation (Phase 1)#3711
Merged
Conversation
… change) A WorkOS user is one credential bundle for one email; an identity is the person and may back multiple WorkOS users. Today every user is a singleton identity. Phase 2 will rewrite mergeUsers to bind multiple WorkOS users to one identity instead of deleting the secondary user, so that linked emails actually work for sign-in (each email is a real WorkOS user) and our DB becomes a truthful cache of WorkOS state. Migration 460 adds `identities` and `identity_workos_users`, backfills 1:1 from `users`, and installs an AFTER INSERT trigger so any new user gets a singleton identity automatically. Auth middleware now resolves identityId on req.user for real users (synthetic admin/API-key users skip it). No app code reads identityId yet — that comes in Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…paths Code review caught two paths that set req.user without resolving identityId. optionalAuth was the most important — it shares sessionCache with requireAuth, so an optionalAuth request would poison subsequent requireAuth cache hits with identityId=undefined for the cache TTL. Phase 1 has no readers so it's latent, but Phase 2 would have hit the bug immediately. Also: - Extract isSyntheticUser(id) helper so the admin/api-key skip convention is single-sourced. - Drop unused identities.updated_at column (no UPDATE trigger, nothing reads it; add it back when Phase 2 actually mutates identities). - Note "do not serialize identityId to clients" on the type — correlating identityId across surfaces would let a client tie multiple emails to one person. - Add scale-threshold comment to migration 460 backfill, matching house style in 448. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of decoupling person from WorkOS user so that "linked emails" can stop being a local-only fiction.
A WorkOS user is one credential bundle for one email — that's WorkOS's data model and we won't fight it. An identity is the person, and may back multiple WorkOS users (one per email). Today every user is its own singleton identity. Phase 2 will rewrite
mergeUsersto bind multiple WorkOS users to one identity instead of deleting the secondary user, so that each linked email is a real WorkOS user that actually works for sign-in and our local DB becomes a truthful cache of WorkOS state.This PR is no functional change. No app code reads
identityIdyet.What's in here
460_identities.sql: addsidentitiesandidentity_workos_users, backfills 1:1 fromusers, installs anAFTER INSERTtrigger that auto-creates a singleton identity for any new user. The trigger fires onINSERTonly —ON CONFLICT DO UPDATEupserts route throughAFTER UPDATE, which is correct (existing users already have an identity).WorkOSUser.identityId?: stringon the type (server/src/types.ts).identityIdfromidentity_workos_usersand attaches it toreq.userfor real users (skipped for synthetic admin / WorkOS-API-key users). Resolved once per session miss; cached. JWT and dev-mode paths also resolve.Why now
Escalation #298 (Ahmed Karim, Kyber1) surfaced that today's
mergeUsersdeletes the secondary WorkOS user but only records the email locally. The "Linked emails" UI implies you can sign in with any of them — you can't, because WorkOS doesn't know about the alias. The right fix is to stop deleting WorkOS users on merge; instead, bind both to one identity. That requires the identity layer to exist first.Test plan
identity_workos_usersrow marked primary.ON CONFLICT DO UPDATEupsert does not double-fire the trigger.DELETE FROM userscascades to remove the binding.set-primary-email.test.tsandmembership-webhook.test.tsstill pass (68/68) — no regression in adjacent flows.npx tsc --noEmitclean.Phase 2 preview (separate PR)
mergeUsers→ bind both WorkOS users to one identity, no deletion.identity_id.<email>, bind to existing identity" — fixes the Ahmed class of escalation directly.🤖 Generated with Claude Code