Skip to main content
Version 0.2Draft

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?

Design principle - keep the IdP pluggable

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 users table and identified by your own user_id (e.g. usr_9f8a2c). Throughout this page, account and your users record 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 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:

  • sub is 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.
Do not make email your durable key

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.

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.

StrategyFirst sign-in finds the user byOnboard before user arrives?Trade-off
Just-in-time (JIT) createNothing - create a fresh userNoSimplest; ideal for open consumer sign-up
Email match / pre-provisionThe token's verified email matches a pre-seeded userYes - 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 codeA one-time code the user redeems while signed inYes - issue a code to a known personDeterministic 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 same user_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-known user_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 email equals an existing account's email is convenient but is an account-takeover vector. Only ever consider it when email_verified is true, and prefer an explicit "is this you?" confirmation step.
  • Never merge two accounts silently on an unverified or guessed value.
Two accounts created by accident

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:

  1. Add the identities table alongside your existing users.
  2. 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.
  3. 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 own user_id everywhere else.
  • Do treat sub as 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_verified is true.
  • Don't use email (or name, or phone) as your durable primary key.
  • Don't scatter sub through your schema - it belongs only in the mapping layer.
  • Don't conflate identity and authorization; decide each separately.
Updated 2026-07-03