Domain Model

Ten core nouns, each small on purpose — the complexity in this system is in how they relate, not in any single one’s field list — plus nine supporting join, credential, and request records that make the many-to-many relationships, multi-method auth, and support workflows actually work without a bigger table.

Overview

Entity What it represents
User A person with an account on Substratal. One identity, used everywhere.
Organization A domain/grouping object — a company, or a group within one — for Users. Decoupled from infrastructure placement; see Tenancy. Pulled into Phase 2.
Tenant The infrastructure-placement and data-isolation boundary for one Application owner’s data — shared, isolated, or dedicated-region. See Tenant vs. Organization.
Application A catalog entry for a developer’s app, built on this platform for its user/org/tenancy/settings/storage layer.
Role / Permission A named bundle of capabilities, scoped to the platform or to one app.
Entitlement The on/off record: does a User own an Application, and is it currently switched on.
Profile / AppProfile Identity/display data — global, and per-app.
Setting / AppSettings Configuration — global, and per-app, layered over app defaults.
Order Not stored here. The developer’s own commerce record. An Entitlement carries only an opaque order_id reference to it.
Audit Event An immutable log of who changed what access, when.

Supporting entities

Each of these is a join or credential record behind one of the relationships above — real rows in the schema, just not independent concepts the way the ten above are:

Entity Joins What it represents
UserIdentity User ↔ login method How a User actually authenticates — password or sso — since a User can hold more than one at once. See Auth → Native auth and per-Organization SSO.
SSOConnection Organization ↔ identity provider One Organization’s federated-login configuration, referenced by its members’ sso-method UserIdentity rows.
OrganizationMembership User ↔ Organization Which Organizations a User belongs to, and their standing (org_admin/member) in each.
UserRoleAssignment User ↔ Role Which Roles a User holds, platform-wide or scoped to one Application.
Session User ↔ login One successful login. Every refresh token and app token minted from it carries its id (sid), so revoking it ends them all.
TierChangeRequest Tenant ↔ migration One support-run request to move a Tenant to a more isolated tier.
ErasureRequest User ↔ erasure A scheduled right-to-erasure request: at most one per User, run 7 days after it’s made.
Webhook subscription Application (or the platform) ↔ endpoint Where an Application’s events (or, for a platform subscription, every event) are delivered, plus the delivery log.
API Key Application (or the platform) ↔ service A long-lived credential for a backend or agent, scoped to one Application or to the platform, with no User behind it.

How they relate

erDiagram
    USER ||--o{ ENTITLEMENT : holds
    USER ||--o{ ORGANIZATION_MEMBERSHIP : "belongs via"
    ORGANIZATION ||--o{ ORGANIZATION_MEMBERSHIP : "has members via"
    ORGANIZATION ||--o{ ENTITLEMENT : "holds (org-wide)"
    APPLICATION ||--o{ ENTITLEMENT : "granted via"
    USER ||--o{ APPLICATION : owns
    ORGANIZATION ||--o{ APPLICATION : "owns (alt.)"
    USER ||--o| TENANT : "owns (alt.)"
    ORGANIZATION ||--o| TENANT : "owns (alt.)"
    TENANT ||--o{ APPLICATION : places
    ENTITLEMENT ||--o| ORDER : "traces to"
    USER ||--|| PROFILE : "has (global)"
    USER ||--o{ APP_PROFILE : "has, per app"
    APPLICATION ||--o{ APP_PROFILE : scopes
    USER ||--|| SETTINGS : "has (global)"
    USER ||--o{ APP_SETTINGS : "has, per app"
    APPLICATION ||--o{ APP_SETTINGS : scopes
    USER }o--o{ PLATFORM_ROLE : assigned
    USER }o--o{ APP_ROLE : "assigned, per app"
    APP_ROLE }o--|| APPLICATION : scopes
    PLATFORM_ROLE ||--o{ PERMISSION : grants
    APP_ROLE ||--o{ PERMISSION : grants
    USER ||--o{ AUDIT_EVENT : "is target of"
    APPLICATION ||--o{ API_KEY : "scopes (app keys)"
    APPLICATION ||--o{ WEBHOOK_SUBSCRIPTION : "scopes (app subscriptions)"

The relationship worth internalizing before reading further: Entitlement and Role are independent axes. Entitlement answers “can this user reach this app at all, right now.” Role answers “once inside, what can they do.” See Access Control for how the two combine on every request.

ID format

Every entity has an opaque, stable id, prefixed by type for readability (a Stripe-style convention): usr_, uid_, ssc_, ses_, org_, tnt_, tcr_, app_, role_, ent_, evt_, whk_, wev_, dlv_, key_. Most are the prefix plus a ULID. IDs are never reused and never encode meaning beyond the type prefix, with two deliberate exceptions: an Application’s id is app_<slug> and a Role’s is derived from its scope and name. See Conventions → IDs.

Building the actual database, not just calling the API? Database Schema has the Postgres-level types, constraints, and indexes behind every entity above.


Table of contents


Back to top

Substratal Apps Platform API — living specification. This site is the system of record; see git history for how it has changed over time.

This site uses Just the Docs, a documentation theme for Jekyll.