Mapping LuxID identities to your application
When you add "Sign in with LuxID", LuxID proves who the user is. It does not run your application. The question this page answers is the architecture question that sits between the two: how do you connect the identity LuxID gives you to a user record in your own system - without tying your application to LuxID forever?
As the architect or developer on the LuxID Partner side, design from day one so the identity provider is a pluggable component. Your application should depend on your own account record, not on LuxID directly. Done well, you could swap LuxID for another IdP, or support several at once, by changing a small mapping layer - not your whole application. This is the single most important decision on this page.
The boundary: identity vs account
Keep two ideas separate:
- Identity - "who proved they are who they say they are." This is LuxID's job. Its output is a token containing a stable subject identifier,
sub. - Account - the user record your application owns: your domain data, your foreign keys, your audit log. This is your job, stored as a row in your own
userstable and identified by your ownuser_id(e.g.usr_9f8a2c). Throughout this page, account and yourusersrecord mean the same thing.
The seam between them is a small identity mapping layer: it translates an incoming IdP identity into your account, and nothing else in your application needs to know LuxID exists.
Because the rest of your application only ever references user_id, the IdP on the left is interchangeable - and, as the diagram shows, more than one IdP can map to the same account at the same time. A user could sign in via LuxID or via another IdP and land on the same account. That is what "pluggable" means in practice.
The durable link: (idp_id, sub)
The value you link on must be stable and unique. In a LuxID token that value is sub - an opaque, stable, pairwise (Sphere-scoped) identifier (see Identity fundamentals and Roles in the ecosystem). It is the same on every sign-in for a given user in your application, and it is meant to be a key, not a display value.
Store the link as a composite (idp_id, sub), not sub alone:
subis only guaranteed unique per issuer. Recording which IdP issued it keeps you correct the day you add a second one.- One account can have several linked identities - the same person via LuxID today, via another IdP tomorrow.
Email is mutable and can be reassigned. If your permanent link is the email, a user who changes their address loses their account, and whoever later receives that address could inherit it. Link permanently on sub. Email still has two legitimate, separate roles - bootstrapping a link, and driving authorization - covered below.
A minimal data model
Two tables carry the whole pattern (USERS here is the account record from the boundary section). A third table (or a column) supports onboarding accounts before they first sign in.
The key insight: a user can exist with zero linked identities. That is a pre-provisioned (bootstrapped) account, waiting for its first sign-in to attach an identity.
How the link gets established (bootstrapping)
On first sign-in there is no (idp_id, sub) row yet, so you have to decide which account this identity belongs to. There are three strategies, and an application can support more than one.
| Strategy | First sign-in finds the user by | Onboard before user arrives? | Trade-off |
|---|---|---|---|
| Just-in-time (JIT) create | Nothing - create a fresh user | No | Simplest; ideal for open consumer sign-up |
| Email match / pre-provision | The token's verified email matches a pre-seeded user | Yes - seed users by email (import, admin) | Convenient, but fails if the user signs in with a different email; only ever match on email_verified: true |
| Invitation code | A one-time code the user redeems while signed in | Yes - issue a code to a known person | Deterministic and email-independent; needs a channel to deliver the code |
After any strategy runs once, you have captured (idp_id, sub). From then on every sign-in is the fast "already linked" path, and the bootstrap mechanism is irrelevant.
Identity is not authorization
A subtle but important separation:
- Identity continuity - "is this the same person as last time?" - answer with
sub. - Authorization - "should they still have access?" - answer with email, email domain, group membership, or a claim.
This is why email can stay useful after linking. For example, a workforce application may keep access tied to a corporate email or domain on purpose: when someone leaves and that email stops resolving, access is revoked. The clean way to get that behaviour is to keep the sub link for continuity and run a separate ongoing entitlement check on the email/domain - so losing the entitlement revokes access without destroying the user's identity and history. Overloading the email to do both jobs works, but it throws away the record when the email changes.
Designing for multiple, interchangeable IdPs
Because the durable key is (idp_id, sub) and the rest of your code references only user_id, supporting more than one IdP is a data change, not an architecture change:
- Add a second
idp_id; users can link more than one identity to the sameuser_id. - Migrating off an IdP becomes "stop accepting that
idp_id", not a rewrite. - If your own organisation runs several applications and you want the same person recognised across them, that is a LuxID-side concern: ask LuxID to place those applications in the same Sphere so they receive the same
sub(see Roles in the ecosystem).
One account, several IdPs at once (account linking)
The same model lets a single account have several active identities simultaneously - for example, a user who can sign in either with LuxID or with another IdP and reaches the same account. Each provider just adds its own (idp_id, sub) row pointing at the one user_id:
The challenge is how a second provider gets attached safely. Knowing that two different IdP logins are the same person is not something you can infer automatically without risk.
- Recommended - link from an authenticated session. The user signs in with their first provider, then chooses "Connect another sign-in" in your account settings. You run an authentication against the second IdP and, on return, insert the new
(idp_id, sub)row against the already-knownuser_id. Because the user proved control of both, the link is trustworthy. - Use with care - link by verified email. Auto-attaching a second provider because its
emailequals an existing account's email is convenient but is an account-takeover vector. Only ever consider it whenemail_verifiedistrue, and prefer an explicit "is this you?" confirmation step. - Never merge two accounts silently on an unverified or guessed value.
If a user signs in with one provider (creating account A) and later with a second provider before linking (creating account B), you now have two accounts for one person. Avoid this by offering the "connect another sign-in" flow prominently; recover from it with a deliberate merge flow that requires the user to prove control of both identities. Merges are messy (which data wins?), so prevention beats cure.
Migrating from an existing auth provider
If you already authenticate users another way - a password table, Firebase, Supabase Auth, or similar - you do not have to migrate data in a big bang:
- Add the
identitiestable alongside your existing users. - When a user first signs in with LuxID, link their new
(idp_id, sub)to the existing account - typically by matching the verified email during a migration window, ideally with an explicit "is this you?" confirmation step. - Once a user is linked, prefer LuxID sign-in; retire the old credential path when adoption is high enough.
Your application keeps using the same internal user_id throughout, so foreign keys, history and business logic never move. Email-matching here is a deliberate one-time linking step - not your steady-state key.
Best practices
- Do link permanently on
(idp_id, sub)and reference your ownuser_ideverywhere else. - Do treat
subas opaque - never parse it or assume a format. - Do treat LuxID as the source of truth for profile claims; cache a snapshot for display and refresh it on each sign-in.
- Do only ever auto-match on an email when
email_verifiedistrue. - Don't use email (or name, or phone) as your durable primary key.
- Don't scatter
subthrough your schema - it belongs only in the mapping layer. - Don't conflate identity and authorization; decide each separately.
Related
- Identity fundamentals - what
subis, and why it is pairwise and opaque - Roles in the ecosystem - Subscriptions, Spheres, and what LuxID does and does not store
- Tokens and claims - the values you receive to seed and refresh your user
- Add login - the protocol-level step that produces the token this page consumes