Status: Proposed (design-panel synthesis) · Date: 2026-07-22 · Track: E3 (identity, SSO, security) Method: adversarial design panel — estate-scout + four thinker-canon lenses (auth/threat · specification · simplicity · data). Dissent preserved below.
Follows the ADR-001 template. Brand-free: providers/mechanisms named by capability only. This ADR governs Pillar 1 (identity spine) and the invariant that the whole tier moat hangs off.
The learning core (the forked LMS) already ships real local auth: username/email + argon2 password hashing, email-verify gating, self-serve reset, four roles (admin/instructor/moderator/student). That is a working single-app login — and only a single-app login. The foundation spec (Pillar 1) commits to something larger: one member identity across every surface (learning core, community, realtime, AI, admin), with tier as one attribute on the member, read everywhere, re-implemented nowhere — and the strategic reason to own identity at all is to own the tier (I1/I2). "One login" and "own the tier" are asserted in prose; nowhere are they specified.
The estate-scout confirmed the hard truth: the identity core
is greenfield. The two candidate estate repos
(wild-capability-gate, wild-session-telemetry)
are Ruby, agent-facing, and sit downstream of identity — one is
a fail-closed authorization primitive keyed on a
service-account string (not a human IdP), the other is
agent-session telemetry with an allowlist PII filter
(not human security-event logging). Neither authenticates a human.
Roughly 80–90% of E3 is net-new; the estate contributes a
security stance (fail-closed authorization + audit,
allowlist-based PII redaction) to carry forward, not a head start on the
IdP itself.
E3's first real work — per Pillar 1 and the build order — is the security/login audit (session, secrets, exposure, MFA, rate-limiting), then the one-identity design on top.
sub per
human; tier is one attribute read everywhere, re-implemented nowhere.
This is the primary key of the entire object graph.sub and the tier
decision live in stores we control — never an un-owned
vendor on the one irreversible seam (I5).sub join key and tier authority must never be
re-keyable.sub, minting tier/role claims, brokering social login, with
native argon2 auth as one credential backend behind it. Every surface is
a client.sub in our namespace; one login = a signed
session/token every first-party surface validates locally. Social/MFA
added as thin adapters upstreamed to the core; no separate IdP
daemon.| Lens | Rank #1 | Core argument |
|---|---|---|
| Auth / threat (security) | A | A single hardened choke point is the only place MFA, login rate-limiting, refresh-token rotation, session revocation, and tier-claim integrity can be enforced uniformly. But "own the IdP" means own and operate a proven one — never hand-write an authorization server (that is B's sin in disguise). Resolve revocation with short token TTL + an introspection/revocation path so a downgrade propagates in seconds. Audit first — but scope it to the one-identity target threat model, not today's single-app posture. |
| Specification (Lamport) | A | "One identity" is an unstated bijection: exactly
one immutable, internally-minted sub per human; every
legacy account and every social login maps to it, never
is it. Two live front doors (native login + IdP running in
parallel) makes "one identity" false — federation must
retire the native path. Own the authority, rent
the protocol (mature OIDC, don't hand-roll). Specify the
invariants now; they are the expensive-to-retrofit part
regardless of mechanism. |
| Simplicity (Torvalds) | D | Tier is a column; sub is a foreign key. You don't stand
up a stateful, patch-it-forever identity service to own a
column and an FK — native auth already owns the member table and
sub. Putting a broker in front of live native auth
doubles identity (two account systems → drift → tier
leaks — the exact failure ADR-001 warned about). "One login" is a
session/token problem, not an IdP problem. Do the
audit first; let its findings — not the slogan — decide whether SSO
needs anything heavier than a shared signed session. Defer the IdP,
don't kill it. |
| Data (Kleppmann) | A | sub is an immutable forever join key — treat it like a
primary key, not a login detail. The catastrophic failure isn't slow
login, it's silent referential corruption: a
re-entering member minted a second sub is orphaned
from every prior enrollment and certificate, no error fires. Tier in the
token is a cache, and cache invalidation is the hard
problem — a tier-bearing JWT contradicts I9 by construction.
Native→sub migration is a one-time,
exactly-once event with no undo. |
| Estate-scout | — | Identity is greenfield (~80–90% net-new). The
estate gives a stance, not a component: fail-closed
authorization + structured audit (wild-capability-gate) and
allowlist-based PII redaction (wild-session-telemetry).
Adopt a proven auth provider/framework; don't build the IdP from estate
parts. |
Despite ranking A over D (or D over A), and unanimously rejecting B and C, the lenses converge on one architecture:
sub and where
tier authority lives — not the IdP mechanism. Every lens names
the same single-most-costly item: which identifier becomes the
immutable join key, and whether sub can ever be duplicated
or reassigned. The IdP (self-run, hosted, or "just native auth") is
a repoint. sub written into every FK and
stamped immutably into the append-only conversation log (ADR-001 I9) is
a rewrite. This is the seam the foundation flags
irreversible.sub internally, immutable, opaque — never
an email, never a vendor's subject id, never the learning core's local
user-id. Own the sub namespace in our own
member store from day one; every credential source (native, social,
future) links to an existing sub, never mints a
new one for a known human. This is what "own the identity so we own the
tier" actually reduces to. It is why C is rejected on invariant
grounds (a vendor owns your primary key) and why B is
dangerous (upstream LMS churn lands on your most irreplaceable
data).sub↔︎tier binding, in our store); rent the
protocol machinery (OIDC token minting, social brokering, MFA, session
crypto) from a proven implementation — self-hosted. Hand-rolled
OIDC/revocation/MFA is where subtle auth CVEs live. B is last or
near-last on every lens for exactly this.A hybrid that takes the winning move from each lens. It is D-shaped now, A-frontable later, and never B or C — and it is identical whether the v1 mechanism is a shared signed session or a self-hosted OIDC broker, because the expensive, locked parts live below that choice.
sub is minted exactly once per human,
internally, opaque, immutable forever — never derived from any mutable
attribute (not email, not a vendor id, not the LMS local
user-id). It lives in a member store we control. This is the primary key
of the platform; treat it as one.(provider, provider-subject) tuple maps
to sub, never is
sub. Native login, social login, and any future
credential source all resolve to an existing sub for a
known human, or mint a new one only for a genuinely new human.
Duplicate-sub prevention (verified-email binding +
an explicit, audited, reversible account-link flow) is a first-class
feature, not a support ticket — and account-linking is
never an implicit email match.sub
(identity), never an authoritative tier claim on
destructive surfaces.sub and the session are designed IdP-frontable
from day one — so a self-hosted OIDC broker can be slotted in
front later without re-keying a single FK.sub or tier.sub/session
IdP-frontable from day one. If that discipline slips, "D-now-A-later"
becomes a real rewrite and the dissent (§9) wins.| Dimension | A broker IdP | B extend native | C hosted IdP | D token/federation | Decision (hybrid) |
|---|---|---|---|---|---|
Own sub namespace + tier authority (moat) |
✅ if minted into our namespace | ✅ | ❌ vendor owns the key | ✅ | ✅ by construction |
| One authentication authority | ✅ | ⚠️ two front doors risk | ✅ | ✅ (retire native front door) | ✅ |
| No hand-rolled auth server | ✅ (rent protocol) | ❌ worst attack surface | ✅ | ✅ (thin, or rent later) | ✅ |
| Tier-at-read survives to session layer | ⚠️ if sub-only tokens |
⚠️ | ⚠️ vendor claim | ⚠️ if sub-only tokens |
✅ (tier never a destructive token claim) |
| Revocation / bounded downgrade | ✅ introspection path | build it | vendor-dependent | ❌ stateless leaks to TTL | ✅ (short TTL + live-resolve on destructive) |
| Ops-service count / small team | ❌ 2nd always-on target | ◑ | ✅ (rented) | ✅ lowest | ✅ (defer daemon, one box) |
| Reversibility of the IdP mechanism | ✅ repoint | ⚠️ merge tax | ❌ vendor sub |
✅ repoint | ✅ IdP-frontable by design |
| Merge tax on upstream syncs | ✅ none | ❌ forever, on auth code | ✅ | ✅ (adapters upstreamed) | ✅ |
New locked invariants (promote into the foundation spec §7):
sub is minted exactly once per human,
internally, opaque, and immutable forever; it is never an email and
never derived from any mutable attribute.
(Lamport/Kleppmann — the single most important missing
sentence.)sub and never is sub; account-linking
is an explicit, audited, reversible operation, never an implicit email
match.sub; tier is never an
authoritative token claim on a destructive surface. Tier is resolved at
the decision boundary against the reader's current tier — the same
evaluation path as the AI index (I9). Any tier caching is short-TTL,
per-surface, and forbidden on billing/admission/private-board
writes.sub↔︎member↔︎tier binding and the tier
decision live in stores we own; any auth provider (self-run or hosted)
is swappable behind the owned sub and never authoritative
for sub or tier.sub migration is an
append-only, immutable, auditable mapping ledger written exactly once,
with self-registration frozen during cutover; the migration is
replayable and diff-able, not a mutable one-shot.New required evals (the moat is only as trustworthy as these):
(legacy account, social login, expected sub) that fails
closed on any split (two subs, one human) or bad merge (one
sub, two humans). The identity analogue of ADR-001's P0
tier-leak eval; run on every auth change and before the first
non-founder logs in.E3's first task is the audit, then the invariant lock — not a build:
sub-minting, and the
immutable native→sub migration ledger before any
surface writes a member FK. This is priority-0 and
precedes all rendering, community, and AI build.Estate seams (what we actually reuse — patterns, not components):
wild-capability-gate's fail-closed evaluation
contract (deny on unknown/unconfigured/error, every decision a
structured audit event) → the authorization layer's default
posture.wild-session-telemetry's allowlist-based PII
filter (per-event allowlists + hardcoded forbidden-field list
enforced at ingestion) → the security-event / auth audit log, so PII
structurally cannot land in the store.sub-in-our-namespace and
tier-in-our-store — and may yield better real-world
security with less ops. The hybrid honors this by deferring
the daemon entirely for v1 and keeping the mechanism as light
as a shared signed session — but if we adopt a broker later, it is
defensible only if sub is minted into our
namespace and tier authority never leaves our store (§6.1). The bet is
reversible; the discipline is not optional.sub-minting rule + the
immutable native-account→sub migration ledger, before any
surface writes a member FK. (priority 0)sub + tier authority =
irreversible; IdP mechanism, social provider, session
transport = repoint.Panel transcript basis: estate-scout + auth/threat, Lamport, Torvalds, Kleppmann lenses, 2026-07-22. Follows the ADR-001 template; the identity/tier seam is the one this platform cannot walk back.