Context
DEC-B1 named four Postgres schemas:core, loyalty, messaging, crm. Authentication and
authorization were never assigned to one of them, because until now they were treated as
infrastructure that simply existed.
They cannot stay unassigned. The platform needs users who sign in to a console, roles that bundle
permissions, API keys and OAuth clients that carry those same roles as machines (DEC-D5), an OAuth2
provider so third parties can integrate, MFA, session management, and time-boxed support
impersonation. That is a domain with its own tables, its own invariants and its own compliance
surface — not a library.
The real question is where it goes. Three options were on the table.
Inside core. Tempting, because core already holds the member realm (ADR-010, FS-CORE-0009):
a member is an authenticated contact, so member auth genuinely belongs with the contact. But
console users are not contacts. They are our customers’ staff, a completely different population
with a different lifecycle, a different blast radius and a different regulatory position — for
console users Softcrum is the controller, while for contacts it is the processor (DEC-J1).
Putting both in one schema would put our own users’ data and our customers’ customers’ data under
the same roof, which is exactly the boundary the DPA draws.
Spread across each module. Each module owning its own permissions. Rejected immediately: the
permission registry has to be global for {module}.{resource}.{action} to mean anything, and a
role that cannot span modules is not a role.
Its own context. More governance surface, one more schema to reason about.
Decision
A fifth bounded context,identity (schema identity), owns the organizational realm and the
platform’s access control plane.
It owns: organizations and users, memberships, invitations, the permission registry, roles and
assignments, authentication methods, social and enterprise SSO, MFA, sessions and devices, machine
credentials (API keys and OAuth clients), the OAuth2 provider, and support impersonation.
The member realm stays in core (FS-CORE-0009), unchanged. A member is an authenticated
contact and the contact belongs to core; moving member sessions into identity would create a
dependency from identity to core for no benefit and would blur the realm separation that
ADR-010 makes structural.
Dependency rules, added to the existing set:
identitydepends on nothing. It is a foundation, alongsidecore, not above or below it.core,loyalty,messagingandcrmnever importidentity’s tables. They receive an already-resolved authorization context from the API layer.- The permission registry is consumed by every module as a build-time constant, not a runtime
foreign key. A route declaring
loyalty.redemptions.createis checked against a generated union, the same mechanism as the parametric catalogs. - Two auth realms exist and never overlap:
identity(organizational) andcore(member). A token from one can never satisfy the other, by construction rather than by check.
Consequences
+ Our own users’ data sits in a different schema from our customers’ customers’ data, which matches the controller/processor split the DPA already describes. That is a compliance property, not a tidiness one. + A global permission registry becomes possible, which is what DEC-D5 needs and what an OAuth2 consent screen renders. + The organizational realm can be hardened independently — stricter session policy, mandatory MFA, tighter audit — without touching the member experience. + Better Auth is configured twice, in two places, which makes realm separation visible in the file tree rather than a convention someone has to remember. − A fifth context to govern, and the constitution’s “four bounded contexts” statement needs updating. − Two Better Auth configurations to maintain and upgrade in step. − The authorization context has to be threaded from the API layer into every module, since modules may not readidentity directly. This is the cost of the boundary and it is deliberate:
a module that could query roles directly would eventually enforce them inconsistently.
Follow-up work
AGENTS.md§1 and §3: five contexts, not four.standards/data.md§1:identityadded; cross-schema FK rule unchanged (towardcoreonly —identityhas no cross-schema FKs at all).standards/api.md: the permission registry becomes a generated artifact with its own CI check.- Module spec, PRD and nine feature specs under
modules/identity/.
Alternatives considered
Auth insidecore — lost on the controller/processor boundary. The regulatory position of our
users and our customers’ customers is different, and a schema boundary is the cheapest place to
make that explicit.
A third-party identity provider (Auth0, Clerk, WorkOS) instead of Better Auth — genuinely
attractive for MFA, SSO and the OAuth2 provider, all of which are real work. Lost on three counts:
per-MAU pricing on a platform whose own pricing is per contact stacks badly; being an OAuth2
provider for our tenants’ integrations is a first-class product surface we should not rent; and
the closed-stack rule means adopting one is an ADR of its own, with a migration path, not a
default. Better Auth stays, and this decision is revisited if MFA or SSO prove more expensive to
build than expected.