> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-022 — identity as a Separate Bounded Context

> DEC-B1 named four Postgres schemas: core, loyalty, messaging, crm. Authentication and.

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.
