identity is how the platform knows who is asking and whether they may. It holds our customers’
staff, the roles they carry, the machines that act on their behalf, and the OAuth2 provider that
lets a third party integrate without ever handling a password. Every endpoint in the suite is
gated by something defined here.
Who this is for
The problem today
Access control is the part of a B2B platform that customers do not ask about until it fails, and then it is the only thing they ask about. Three specific failures are worth naming because the design exists to prevent them. Roles that are really one switch. Many platforms ship “admin” and “member” and stop. The moment a tenant has a marketing agency, a call centre and a finance team touching the same account, that model forces them to over-grant. Our permission model is per action from the start (DEC-D5), so “can read contacts but not their national IDs” is expressible rather than aspirational. Integrations that ask for a password. Without an OAuth2 provider, a third party integrating with us asks the tenant for an API key with everything on it, or worse, for console credentials. Being an OAuth2 provider is what makes an integration ecosystem possible without that. Support access nobody can audit. Operators need to see what a tenant sees. Done casually, that is an undetectable read of every customer’s data. Time-boxed, reason-required, audit-logged and visible to the tenant turns it from a liability into a trust signal.What it does (normative)
- Holds organizations and users, where a user’s email is unique per organization. The same
person working with two of our customers holds two independent accounts that cannot be correlated
— isolation over convenience (
standards/data.md§2b). - Invitations with expiring single-use tokens, retained after acceptance as evidence of who let whom in.
- A global permission registry of
{module}.{resource}.{action}, generated from the route declarations themselves so it cannot drift from what the code actually enforces. - Roles that bundle permissions, seeded as system roles and extensible by the tenant.
- Machines carry roles exactly like users (DEC-D5): an API key or an OAuth client is a subject with role assignments, checked by the same code path.
- Authentication: password, magic link and passkey, with social and enterprise SSO in F1b.
- MFA with TOTP and hashed single-use recovery codes.
- Sessions a user can inspect and revoke per device.
- OAuth2 in both directions: as a provider, so a partner tool can act on a tenant’s behalf and a tenant’s site can offer “Sign in with Softcrum”; and as a client, so a tenant’s staff can sign in through their own identity provider. The tenant chooses; both can run at once.
- Support impersonation, time-boxed, reason-required, and visible in the tenant’s own audit trail.
Non-goals (normative)
- Not the member realm. Members are authenticated contacts and live in
core(ADR-010). A token from one realm never satisfies the other. - Not a public identity network. “Sign in with Softcrum” serves a tenant’s own surfaces and the partners they authorise. A client is always scoped to the organization that granted it, and a member subject is scoped to that organization too.
- Not SCIM or directory sync in F1. Enterprise provisioning is a real ask that arrives with real enterprise customers; the membership model is shaped so it can be added without migration.
- Not per-record permissions. Permissions are per action, not per row. “Can edit this specific campaign but not that one” is deliberately out — it is where authorization models become unreasonable, for us and for the administrator configuring them.
- No password for members. That is a
coredecision (FS-CORE-0009) and it stands: no member password means no member password to leak.
Success
Commercial shape
identity is not sold and is not metered. Two capabilities are plan-gated rather than priced:
enterprise SSO, which is the standard enterprise-tier feature across the industry, and
tenant-defined custom roles, where lower plans get the seeded system roles.
Seats are not a billing dimension. The suite bills marketable contacts and volume (ADR-014,
ADR-016), and adding a per-seat charge on top would punish a tenant for involving more of their own
team — which is the opposite of what drives adoption.
Phases
Compliance and risk
For the data in this module Softcrum is the controller, not the processor — these are our own users, not our customers’ customers (DEC-J1). That is why it is a separate schema fromcore
(ADR-022), and it means our own privacy policy governs here while the DPA governs there.
Three risks shape the design:
- Privilege escalation across realms. Mitigated structurally: separate realms, separate token audiences, an OAuth client that declares one realm and can never reach the other, and a test asserting a member token is rejected by every console endpoint.
- Cross-tenant identity leakage. The same email in two organizations must never be correlatable.
Mitigated by per-tenant uniqueness, per-organization
subclaims, and invitation responses whose wording and timing do not reveal that an address is known elsewhere. - A leaked API key. Mitigated by hashing at rest, a visible prefix so a leaked key can be
identified without revealing it,
last_used_atfor detection, and rotation without downtime. - An operator reading customer data undetected. Mitigated by making impersonation impossible without a reason and a time box, and by surfacing it in the tenant’s own audit trail — the tenant is the one with the incentive to notice.
Dependencies
Better Auth for both realms (ADR-010) — the only vendor in this module, and the one whose replacement cost the ADR-022 alternatives section weighs explicitly. Upstash for rate limiting and replay checks. Resend for magic links and invitations.core.audit_log for the trail.