Skip to main content
Status: Proposed · Date: 2026-08-17 · Extends: DEC-B1 · Refs: DEC-D5, DEC-D6, ADR-009, ADR-010

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:
  • identity depends on nothing. It is a foundation, alongside core, not above or below it.
  • core, loyalty, messaging and crm never import identity’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.create is checked against a generated union, the same mechanism as the parametric catalogs.
  • Two auth realms exist and never overlap: identity (organizational) and core (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 read identity 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: identity added; cross-schema FK rule unchanged (toward core only — identity has 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 inside core — 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.