All notes

Designing Aira Nexus' shared identity layer

SSO across Campus, Creator and Labs — role propagation, tenant boundaries, JWKS rotation, and why we don't use Auth0.

A principal who logs into Campus to look at attendance is, in a different organisation, the owner of a diagnostic chain that runs on Labs. The same email. The same device. Two entirely separate worlds of data. Our identity layer has to make that one login feel seamless without ever blurring the wall between the two tenants. That problem — federation inside a multi-product, multi-tenant suite — is what Nexus Identity does.

Why not Auth0

We priced Auth0 (now Okta CIC) at our projected three-year scale: roughly $94,000 a year once you count B2B Enterprise, machine-to-machine tokens, and the SSO add-on for the staff side. That's not the reason we didn't pick it. The reason is that Auth0's tenant model maps poorly to ours. Auth0 thinks of "an Auth0 tenant" as your company. We think of "a Nexus tenant" as the school or lab or creator-org that uses our company's product. Modelling 800 schools as 800 Auth0 organisations works on paper and falls over the moment you need to do anything cross-cutting like "rotate signing keys for all schools in Telangana". Building our own identity service was eight weeks. The economics decide themselves after that.

We also didn't want a vendor in the request path of every login. Identity is the one service that absolutely has to be up.

The token shape

Every authenticated request carries a JWT issued by Nexus Identity. The interesting parts of the claim set:

  • sub — the user's stable Nexus ID, never reused.
  • tenant — the active tenant for this session.
  • tenants — the full set of tenants this user can switch into without re-auth.
  • roles — array of {tenant, role, scopes} triples, scoped per tenant.
  • product — which product surface the token was minted for (campus / labs / creator).
  • kid — the JWKS key ID, per-environment.

The same physical user logging into Campus in the morning and Labs in the afternoon ends up with two different tokens, different tenant claims, different roles. The only thing that persists across them is sub and the device-bound refresh token. Token exchange between products goes through a server-side endpoint, never the client — the browser never holds two tokens at once.

Role propagation is the hard problem

A teacher in Campus has a role like teacher with scopes attendance:write, grades:write, parents:message. A pathologist in Labs has pathologist with reports:validate, samples:read. These role definitions live inside the products, not in identity — Nexus Identity does not know what a pathologist is. What it does know is that user usr_a8c2… has, in tenant tnt_lab_xyz, the role string pathologist, and that this string is meaningful to Labs.

That separation is deliberate. Roles change frequently inside products. Identity should not be in the deployment path of a Campus role rename. The product's own authorisation layer interprets the role and enforces it. Identity just propagates the assertion, signed.

Tenant boundaries are a wall, not a fence

The token's tenant claim is matched against every database query at the application layer (via the RLS context we wrote about previously). But tokens themselves are bound to the tenant they were minted for. A token minted with tenant: tnt_a cannot be re-presented to access tnt_b data, even by the same user. To switch tenants, the client calls a dedicated /v1/identity/switch-tenant endpoint that returns a new token. The old token is revoked on the server. The audit log shows the switch.

This rule has bitten us exactly once, when an internal admin tool was caching tokens across tenants for "convenience". The cache hit was a single line of code. Removing it took ten minutes. Designing a system where that bug couldn't have been written in the first place is what good identity looks like.

JWKS rotation, in production

The signing keys live in a dedicated KMS, separate from any other application keys. Rotation runs on a 90-day cadence with an overlap window:

  1. T-7 days: new key generated, published to the JWKS endpoint, but not yet used for signing.
  2. T-0: signer flips to the new key. Old key remains in JWKS for verification.
  3. T+30 days: old key removed from JWKS. Any token still signed with it fails verification.

The 30-day overlap is comfortably longer than our longest-lived refresh token, so no session is ever invalidated by rotation alone. The JWKS endpoint is cached at the edge with a 5-minute TTL, which means a forced rotation (in response to a compromise) takes effect in roughly that time across every product surface.

Rotating keys on a calendar, not on an incident, is the only way to know rotation works. Surprise rotations during a real incident are how identity systems fail badly.

What the identity layer doesn't do

It deliberately does not handle:

  • Authorisation — products own role enforcement.
  • User profile data — name, photo, preferences live in the product database scoped to the tenant. Identity holds only what's needed to authenticate.
  • Audit logging beyond auth events — product audit logs cover product actions.

That minimalism is the point. The identity service is about 4,200 lines of code with a deliberately tiny surface, and we deploy it on a Tuesday afternoon without a war room.

If you're integrating with Nexus across products, the identity story is documented at admin@airanexus.in for partner technical contacts.