Skip to content
Like what we’re building? Star on GitHub

Data model — identity and tenancy ​

A team id is the tenant of almost every resource an iterion deployment stores, a team belongs to at most one org, and the credentials a person signs in with all hang off a single user row.

Drawn from the code at commit 98787101a (branch fix/org-tree-errors-1668, on v3.276.0); how this page is kept true: README.

Block 1 — Who is who ​

  • teams._id is the tenant, not orgs._id. Plenty of rows key on the org instead.
  • memberships.role is not a five-rung ladder. config_editor ranks 0, so AtLeast places it nowhere.

Block 2 — How a credential is born and dies ​

  • The access JWT is not a document and is absent here on purpose. sessions holds refresh tokens only.
  • Untagged Go structs are stored LOWERCASED with no underscores — so the embedded names above are the real query keys.
  • Single use is a store primitive, not a flag. oidc_states, desktop_sso_tickets and ws_tickets redeem through FindOneAndDelete. sessions and password_resets instead compare-and-set revoked_at / consumed_at.
  • A refresh retires its session in the compare-and-set that records the rotation — rotation_nonce, rotation_kid, successor_hash, rotation_ua, rotation_ip — with revoked_at stamped by the database's clock ($$NOW), and the successor names it in rotated_from and copies its lineage and generation. revoked_at is written once: a sign-out or a revoke-all never moves it — their pass writes only the live sessions it covers, and a retired session keeps its rotation record, whose successor the revocation's record refuses at its delivery — so a session revoked with no rotation record was signed out, or revoked with its lineage or its account, while it was live. A successor is handed out once the refresh that writes it has checked it and claimed it (delivered_at, stamped once and only on a session not revoked); no refresh deletes a session it wrote, and a successor never claimed was handed to nobody, so its predecessor's next presentation completes it. sessions stores no refresh token, only hashes: the successor is re-derived from the token a client presents. desktop_sso_tickets rests no credential either (#2635): the callback DEFERS the login — the ticket's payload is the account, the lineage the session will be born under and the landing team, keyed by the ticket's SHA-256 — and the session is minted at redemption, so no refresh token exists while a ticket waits and an unredeemed one leaves no session behind.
  • A lineage is one login's sessions: the root IssueSession writes and every successor rotated from it, all naming the root's id in lineage. A sign-out and reuse detection revoke one lineage (RecordLineageRevocation, then RevokeLineageSessions), "sign out everywhere" every session of the account (RecordAccountRevocation, then RevokeAccountSessions). Each first records itself in session_revocations — the lineage row, or the account row's generation raised — which is its commit point; its pass then revokes the live sessions it covers, after the answer, reading them through the user_live and user_lineage_live indexes, never the retired history; and every successor is checked against those rows before it is delivered and again once it is claimed (SessionRevoked): a successor written after the pass ran is covered by the row written before it. No row of session_revocations is ever reaped — a lineage's row covers a session its pass did not reach for as long as that session lives, whatever refresh TTL it was written under — so a restore of sessions brings session_revocations with it. A row with no lineage was written before lineages were recorded: it is its own root, every write that touches it stamps one, and a retired one presented again names no family, so its reuse revokes the account.
  • personal_access_tokens has no TTL index and no tenant_id: rows survive expiry and revocation for audit, and the hash IS the credential.
  • An oidc_links row linked from the account settings records the lineage of the session that linked it (session_lineage); a row an SSO sign-in wrote itself — the account's sign-up through the IdP, its first SSO sign-in — and a row written before links recorded it carry none. "Sign out everywhere" and an admin disable delete every row that carries one (DeleteSessionOIDCLinks), a reuse detection the rows of the reused lineage; a sign-out deletes none, and the rows with no session_lineage stay.

Block 3 — Org-level SSO config and the audit trail ​

  • tenant_id on both org SSO collections is a TEAM id — the org's primary team — not an org id. Reading these rows by org id finds nothing.
  • audit_events.tenant_id is polymorphic and nothing in the document says which kind it holds: auditTenant and auditSystem write a team id (none on an auditTenant row a team-less caller writes from a /api/me/ route), auditOrg an org id, all with scope: tenant. The partition is only "which id was written", which is why two dotted edges leave the SAME field for different collections — the store never joins it. The sign-in rows (auth.login.*, ADR-157) key on the ORG: an accepted sign-in is a tenant row on the org the session is active in, a refused one a platform row with no tenant at all.
  • An unverified GitHub grant is stored but inert: it shows in grants and never in github_team_keys.
  • verified_at carries the whole value of org_verified_domains: an unproven row exists so the operator can read the TXT challenge. A verified domain is not enough to auto-link onto an existing account.

Collections ​

collectionpackagedocument structtenant field (values)unique / TTL indexestwin
userspkg/identityidentity.Usernone — platform-globalemail unique; statusnone (cloud-only)
orgspkg/identityidentity.Org_id IS the org tenantslug unique; migrated_from_team_id unique partial on $exists; created_at, statusnone (cloud-only)
teamspkg/identityidentity.Team_id IS the resource tenantslug unique; runner_pool unique partial on non-empty; org_id, created_at, statusnone (cloud-only)
org_membershipspkg/identityidentity.OrgMembershiporg_id — an org{user_id, org_id} unique; {org_id, role}none (cloud-only)
membershipspkg/identityidentity.Membershipteam_id — a team{user_id, team_id} unique; {team_id, role}none (cloud-only)
invitationspkg/identityidentity.Invitationteam_id — a team; an org invitation names one, defaulting to the org's primary teamtoken_hash unique partial on $exists; TTL expires_at at 0s; {team_id, email}none (cloud-only)
oidc_linkspkg/identityidentity.OIDCLinknone — platform-global{provider, provider_user_id} unique; user_idnone (cloud-only)
sessionspkg/authauth.Sessionnone — the hash is the credentialtoken_hash unique; TTL expires_at at 0s; {user_id, issued_at}; {user_id, lineage}none (cloud-only)
session_revocationspkg/authauth.sessionRevocationnone — user_id names the account_id only, no TTL: no row is reapednone (cloud-only)
password_resetspkg/authauth.PasswordResetnone — the hash is the credentialtoken_hash unique; TTL expires_at at 0snone (cloud-only)
personal_access_tokenspkg/patpat.Tokennone — team_id is an optional pin to a teamtoken_hash unique; {user_id, created_at}; no TTLnone (cloud-only)
oidc_statespkg/auth/oidcoidc.mongoStateDoc wrapping oidc.PendingAuthnone; pending.tenantid is the org's primary teamTTL expires_at at 0s, 10 min as wiredmemory-only
desktop_sso_ticketspkg/auth/desktopsso{_id, pending, expires_at} — _id is the ticket's SHA-256, the payload is auth.DesktopLogin (the deferred login: account, lineage, landing team — no token)none; pending.preferred_team is a teamTTL expires_at at 0s, 2 min as wiredmemory-only
ws_ticketspkg/auth/wsticketnone — {_id, identity, expires_at} built by storekit.TicketMongo.Mint; the payload is auth.Identitynone; identity.teamid is the minter's team, re-scoped to the run's team on a run-addressed WS pathTTL expires_at at 0s, 1 min as wiredmemory-only
org_sso_providerspkg/auth/orgssoorgsso.OrgSSOProvidertenant_id — the org's primary TEAM idtenant_id unique partial on kind: github; {tenant_id, kind}; github_team_keys partial on enabled github rowsnone (cloud-only)
org_verified_domainspkg/auth/orgssoorgsso.VerifiedDomaintenant_id — the org's primary TEAM id{tenant_id, domain} uniquenone (cloud-only)
audit_eventspkg/auditaudit.Eventtenant_id — a TEAM id on auditTenant and auditSystem rows (none on an auditTenant row a team-less caller writes from a /api/me/ route), an ORG id on auditOrg and auth.login.succeeded rows, empty or either on scope: platform rows (always empty on auth.login.failed)TTL created_at at 400 days; {tenant_id, created_at}, {scope, created_at}, {action, created_at}none (cloud-only)
"external: GitHub teams"not a collection — GitHub's own roster———n/a (external)