> ## 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.

# Module Spec — identity (schema identity)

> Owns the organizational realm and the platform's access control plane: who our customers' staff are, what they may do, and how machines prove they may do it. Depends on nothing (ADR-022).

The **member realm stays in `core`** (ADR-010, FS-CORE-0009). A member is an authenticated contact;
a user is not.

## Entities & invariants

| Table                 | Key invariants                                                                                                                                                                    |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| organizations         | one per tenant; `slug` unique; billing and locale settings; never hard-deleted                                                                                                    |
| users                 | `organization_id` NOT NULL; email unique **per (organization, email)** — never globally (`standards/data.md` §2b); the same address in two organizations is two independent users |
| memberships           | (user, organization) unique; carries the roles; deactivation never deletes history                                                                                                |
| invitations           | single-use token, expiring; accepted invitations become memberships and are retained as evidence                                                                                  |
| permissions           | the registry: `{module}.{resource}.{action}`; generated from route declarations, seeded, never tenant-created                                                                     |
| roles                 | system roles seeded per organization + tenant-defined roles; a role is a set of permissions                                                                                       |
| role\_assignments     | polymorphic subject: user **or** machine (DEC-D5); at most one open assignment per (subject, role)                                                                                |
| auth\_accounts        | Better Auth provider linkage: password, magic link, passkey, oauth provider                                                                                                       |
| mfa\_factors          | TOTP and recovery codes; `verified_at`; recovery codes single-use and hashed                                                                                                      |
| sessions              | device, IP, user agent, `expires_at`, `revoked_at`; revocable individually or in bulk                                                                                             |
| api\_keys             | hashed secret, prefix stored in clear for identification, roles attached, `last_used_at`, rotatable                                                                               |
| oauth\_clients        | third-party integrations: client id, hashed secret, redirect URIs, allowed scopes, **target realm** `user`\|`member` (never both)                                                 |
| oauth\_authorizations | issued grants with the scopes the user consented to; revocable by the user                                                                                                        |
| impersonations        | operator, target, reason, time box, `ended_at`; append-only, visible to the tenant                                                                                                |

## Events emitted

`identity.user.invited|joined|deactivated` · `identity.role.assigned|revoked` ·
`identity.api_key.created|rotated|revoked` · `identity.oauth.authorized|revoked` ·
`identity.impersonation.started|ended` · `identity.mfa.enabled|disabled`

## Endpoints

* Auth: `POST /v1/identity/auth/{sign-in,sign-up,sign-out,magic-link,passkey}` — public, rate-limited
* MFA: `POST /v1/identity/mfa/{enroll,verify,disable}` — `identity.mfa.*`
* Users and memberships: CRUD `/v1/identity/users`, `/v1/identity/memberships` — `identity.{resource}.{action}`
* Roles: CRUD `/v1/identity/roles`, `POST /v1/identity/role-assignments` — `identity.roles.*`
* Machine credentials: CRUD `/v1/identity/api-keys`, `/v1/identity/oauth-clients`
* OAuth2 provider: `GET /oauth/authorize`, `POST /oauth/token`, `POST /oauth/revoke`, `GET /oauth/userinfo`
* Impersonation: `POST /v1/identity/impersonations` — Ops only, `identity.impersonation.start`

## OAuth runs in both directions

Softcrum is an OAuth2/OIDC **provider** (FS-IDN-0007) so a partner tool can act on a tenant's behalf
and a tenant's own site can offer "Sign in with Softcrum", **and** an OAuth2/OIDC **client**
(FS-IDN-0008) so a tenant's staff can sign in through their own identity provider. Neither is the
default; both are configuration, and a tenant may use both at once.

## Non-negotiables

Every uniqueness constraint carries the tenant. Two realms, never overlapping. Every endpoint in the platform declares exactly one permission from
this registry. Machines carry roles exactly like users. No module reads these tables directly —
the API layer resolves the authorization context and passes it down.
