Skip to main content

Subjects & identities

Real organizations rarely have one canonical user id across every system. The same person might authenticate to one app via an SSO email claim and to another via a phone-based login, with nothing forcing the two to visibly agree. CNS resolves all of that fragmentation down to one stable, opaque subject_id per tenant — the id every event's actor/target, every policy, and every authz decision is keyed on.

Two layers, deliberately separate

  • adapters/identity.Identity — the adapter for one specific IdP (SSO-JWT, Supabase Auth, Firebase Auth, …). Its Verify(token) checks a Bearer token against that IdP and returns claims in whatever shape that IdP uses; its Resolve call hands those claims to the layer below.
  • core/identitymap.Mapper — tenant-agnostic stitching policy: given a narrow, adapter-independent ClaimSet{TenantID, Email, Phone, Raw}, decide which subject_id this is. Core never imports a specific identity adapter — it only knows this narrow claim shape.

Stitching

The default implementation (StitchingMapper) resolves claims by email or phone against a store. When a ClaimSet carries one identifier the store already knows and one it doesn't, it links ("stitches") the new identifier onto the existing subject — this is how, for example, a user's email-based SSO session today and a phone-based login tomorrow converge on the same subject_id instead of fracturing their activity timeline into two people. If a claim set instead asserts two identifiers that are already each bound to a different existing subject, that's a genuine conflict — the mapper rejects it (ErrIdentityConflict) rather than silently merging two people into one.

PII stays out of the hot path

Raw email/phone live only in the identity-mapping store. They are never copied into event rows — the event store (long-retention, queried constantly) only ever carries the opaque subject_id. This is a deliberate boundary, not an incidental one: it's what lets the event store be broadly queryable without also being a PII store.

What Phase 1 is (and isn't)

Phase 1 is a thin adapter off JWT claims — one cached lookup at ingestion time, not a general-purpose identity service. There's no identity UI, no manual merge/split tooling beyond what the mapper does automatically, and no cross-tenant identity resolution (identity is always resolved within one tenant).

Authenticating via a non-native IdP

The identity adapter (ssojwt) is IdP-shape-agnostic by design — issuer, audience, signing-key source (static HMAC or JWKS), and claim names are all configurable. A tenant doesn't have to use the platform operator's own SSO; see Wiring a non-native IdP for a worked example using Supabase Auth, and Authentication for the token mechanics.