From 598c2702fd7b3c3a473d9c43a97631e7d85fedca Mon Sep 17 00:00:00 2001 From: Adron Hall Date: Wed, 21 Oct 2026 20:21:17 -0700 Subject: [PATCH 1/2] Spell out how API versioning works across the docs [docs:patch] A top-to-bottom review after moving the version into the Substratal-Version header: - NFR: what counts as a breaking change; Deprecation (RFC 9745) and Sunset (RFC 8594) go on responses to requests naming the deprecated version; after Sunset, that version is 400 unsupported_api_version - Conventions: the same retirement rule, and that a webhook subscription's payload api_version is a separate version - Webhooks -> Payload versions: independent of Substratal-Version - OpenAPI Specification: versions are numbers, not URL prefixes - Auth -> POST /auth/oauth/token: no version header needed, and why - Glossary: API version and Payload version - Changelog entry Co-Authored-By: Claude Opus 5.5 --- docs/api-reference/auth.md | 2 +- docs/api-reference/conventions.md | 3 ++- docs/api-reference/webhooks.md | 2 +- docs/changelog.md | 9 +++++++++ docs/glossary.md | 6 ++++++ docs/non-functional-requirements.md | 3 ++- docs/openapi/index.md | 4 ++-- 7 files changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/api-reference/auth.md b/docs/api-reference/auth.md index 8ab3af8..81981ee 100644 --- a/docs/api-reference/auth.md +++ b/docs/api-reference/auth.md @@ -356,7 +356,7 @@ Called by Substratal's hosted sign-in page (or the future dashboard's "Launch" b ### `POST /auth/oauth/token` -Called by the Application. A public-client endpoint: there's no client secret, the PKCE verifier is the proof. +Called by the Application. A public-client endpoint: there's no client secret, the PKCE verifier is the proof. It's the one Auth endpoint that doesn't need `Substratal-Version`, because standard OAuth libraries call it without custom headers (see [Conventions → Versioning](../conventions/#versioning)). Sending the header is fine. ```json // Request — exchange an authorization code diff --git a/docs/api-reference/conventions.md b/docs/api-reference/conventions.md index 9d6381d..815e276 100644 --- a/docs/api-reference/conventions.md +++ b/docs/api-reference/conventions.md @@ -329,7 +329,8 @@ Versions are whole numbers, and `1` is the only one today. The URL never carries - **Echoed.** Every response to a versioned request carries `Substratal-Version` set to the version that served it. - **Exempt:** `GET /health`, so uptime monitors can call it as is; `POST /auth/oauth/token`, which standard OAuth libraries call without custom headers; and the `/.well-known` endpoints. They behave the same in every version. [MCP](../../mcp-server/) clients don't send it either: the MCP server calls the API with the version its tools describe. - **Browsers:** `Substratal-Version` is allowed in CORS requests and exposed on CORS responses. -- **Changes.** Additive changes (new optional fields, new endpoints, new enum values) ship within a version. A breaking change ships as the next number, and the previous version keeps working for a deprecation window, with `Deprecation` and `Sunset` headers on every response it serves. See [Non-Functional Requirements → Versioning](../../non-functional-requirements/#versioning). +- **Changes.** Additive changes (new optional fields, new endpoints, new enum values) ship within a version. A breaking change ships as the next number, and the previous version keeps working for at least 6 months, with `Deprecation` and `Sunset` headers on every response to a request that names it. After its `Sunset` date it's retired, and requests naming it get `400 unsupported_api_version`. See [Non-Functional Requirements → Versioning](../../non-functional-requirements/#versioning) for what counts as breaking. +- **Not the webhook version.** A webhook subscription's `api_version` (a date, such as `2026-10-05`) fixes the shape of the payloads it receives, and is versioned separately. See [Webhooks → Payload versions](../webhooks/#payload-versions). - **Generated clients:** `openapi.yaml` doesn't declare the header on each operation, so configure your client to send it as a default header. ## Timestamps diff --git a/docs/api-reference/webhooks.md b/docs/api-reference/webhooks.md index 4dc5fdc..cf678a3 100644 --- a/docs/api-reference/webhooks.md +++ b/docs/api-reference/webhooks.md @@ -298,7 +298,7 @@ During a [secret rotation](#post-webhooksidrotate-secret), deploy with both secr ## Payload versions -Each subscription's `api_version` fixes the shape of every payload it receives. Versions are dates, and a new one is published only for a breaking change to an existing event's payload. New event types and new optional fields in `data` are additive and ship to every version. +Each subscription's `api_version` fixes the shape of every payload it receives. It's separate from the `Substratal-Version` request header ([Conventions → Versioning](../conventions/#versioning)): a request's API version doesn't change which payload version a subscription gets, and a new API version doesn't imply a new payload version. Payload versions are dates, and a new one is published only for a breaking change to an existing event's payload. New event types and new optional fields in `data` are additive and ship to every version. | `api_version` | Status | Changes from the previous version | |---|---|---| diff --git a/docs/changelog.md b/docs/changelog.md index 005848b..0b7af8b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,15 @@ What's changed in this specification over time. Every decision is written into t --- +## 2026-10-21 — Versioning rules spelled out + +Clarifications to the header-based versioning below; nothing about the API's behavior changed. + +- **What counts as breaking** is now listed in [Non-Functional Requirements → Versioning](../non-functional-requirements/#versioning): removing or renaming a field, endpoint, or parameter; changing a type or format; a new or newly required request field; a different status or error code for an existing condition; or a changed default or behavior. +- **Retirement.** `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) appear on responses to requests that name a deprecated version. After its `Sunset` date, requests naming it get `400 unsupported_api_version`. +- **Two separate versions.** The `Substratal-Version` request header and a webhook subscription's payload `api_version` are independent, now stated on [Conventions](../api-reference/conventions/#versioning), [Webhooks](../api-reference/webhooks/#payload-versions), and in the [Glossary](../glossary/#api-version). +- [OpenAPI Specification](../openapi/#versioning-and-change-policy) no longer describes versions as URL prefixes, and [Auth → `POST /auth/oauth/token`](../api-reference/auth/#post-authoauthtoken) notes it needs no version header. + ## 2026-10-21 — The API version moves from the URL to a header **Breaking.** The `/v1` path prefix is gone. Every endpoint is now at the host root, and every request names its API version in a header instead: diff --git a/docs/glossary.md b/docs/glossary.md index bf6cfb5..9c4a3a4 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -21,6 +21,9 @@ Which [Pricing](../pricing/) tier unlocks an [Application](#application): `free` #### API Key A long-lived service credential (`satk_live_…`/`satk_test_…`), scoped to the platform or to one Application. An app-scoped key can only ever touch its own Application's rows. See [API Keys](../api-reference/api-keys/). +#### API version +The version of the REST API a request is written against, sent as a whole number in the required `Substratal-Version` header. `1` is the only one so far. Unrelated to a webhook [payload version](#payload-version). See [Conventions → Versioning](../api-reference/conventions/#versioning). + #### App token The short-lived (5-minute) JWT an Application verifies locally, with `aud` set to that Application. Carries `entitlement_status` and `effective_permissions`. See [Auth → The app token](../api-reference/auth/#the-app-token). @@ -66,6 +69,9 @@ A domain/grouping object for Users — a company, or a group within a company #### OrganizationMembership The join record between a User and an Organization, carrying that User's standing *within* that one Organization (`org_admin` or `member`) and whether it's `pending` (invited, not yet accepted) or `active` — a User can hold this for any number of Organizations at once. See [Domain Model → Users & Organizations](../domain-model/users-and-organizations/#organizationmembership). +#### Payload version +The shape of a webhook's payloads, a date pinned on each subscription as `api_version` (currently `2026-10-05`) and moved forward only by its owner. Unrelated to the [API version](#api-version). See [Webhooks → Payload versions](../api-reference/webhooks/#payload-versions). + #### Permission An atomic, checkable capability, written as a dotted key (`billing.manage`, `app.timetrack.export`). Never assigned directly to a user — always granted through a [Role](#role). diff --git a/docs/non-functional-requirements.md b/docs/non-functional-requirements.md index 8a6aef3..5d13b8a 100644 --- a/docs/non-functional-requirements.md +++ b/docs/non-functional-requirements.md @@ -98,7 +98,8 @@ Targets for the API itself, measured monthly at the API Gateway. They're publish ## Versioning - The API version is a whole number sent in the `Substratal-Version` request header, never in the URL. `1` is the only version today, and the header is required (`400 api_version_required` without it), so no client is ever on a version it didn't ask for. See [Conventions → Versioning](../api-reference/conventions/#versioning). Additive, backward-compatible changes (new optional fields, new endpoints) ship without a version bump. -- A breaking change gets the next version number. The old version keeps working for a minimum 6-month deprecation window, announced in the [Changelog](../changelog/) the day the replacement ships, with a `Deprecation` and `Sunset` response header (RFC 8594) added to every response the old version serves from that point on. +- A breaking change gets the next version number. The old version keeps working for a minimum 6-month deprecation window, announced in the [Changelog](../changelog/) the day the replacement ships. From then on, every response to a request that names the old version carries a `Deprecation` header (RFC 9745) and a `Sunset` header (RFC 8594) giving the retirement date. Once that date passes, the old version is removed from the supported list, and requests naming it get `400 unsupported_api_version`. +- **What counts as breaking:** removing or renaming a field, endpoint, or query parameter; changing a field's type or format; making an optional request field required, or adding a new required one; changing the status code or error code for an existing condition; and changing a default or a behavior that existing callers depend on. Everything else, such as new endpoints, new optional fields, new enum values, and new error codes for new conditions, is additive. - Webhook payloads are versioned independently of the API version, by a date string (`api_version`, currently `2026-10-05`) pinned on each subscription at creation. A breaking payload change ships as a new date. Existing subscriptions keep their pinned shape until their owner moves them forward with `PATCH`, and old payload versions are supported for at least 12 months. Additive payload fields don't get a new date. - The [MCP Server](../mcp-server/)'s protocol version (negotiated per MCP's own date-based scheme during `initialize`) is likewise independent of the API version — a protocol-version bump there doesn't imply `Substratal-Version: 2` here, and vice versa. - **Enums can grow new values without a version bump.** `EntitlementStatus`, `UserStatus`, and similar closed-looking lists in [openapi.yaml](../openapi.yaml) are allowed to gain new members as additive, non-breaking changes. Clients — and any implementation — must treat an unrecognized enum value as "handle generically / no special case," never as an error to reject the response over. This is a contract, not just a suggestion: don't write a `switch` with no default case against any enum in this spec. diff --git a/docs/openapi/index.md b/docs/openapi/index.md index a247083..649031e 100644 --- a/docs/openapi/index.md +++ b/docs/openapi/index.md @@ -133,6 +133,6 @@ To explore by hand, import `openapi.yaml` into Postman, Insomnia, Bruno, or any The API version is the `Substratal-Version` request header (currently `1`), not part of the base URL. The document doesn't declare it on each operation, so configure a generated client to send it as a default header; the Interactive Reference sends it for you. `info.version` in the document (currently a `1.0.0-draft` pre-release) is the version of the document itself. -- **Additive changes** (new optional fields, new operations, new enum values) ship without a new prefix. Write clients to ignore fields they don't know, and to handle enum values they don't recognize. -- **Breaking changes** get a new prefix and a deprecation window for the old one. See [Non-Functional Requirements → Versioning](../non-functional-requirements/#versioning). +- **Additive changes** (new optional fields, new operations, new enum values) ship within the current version. Write clients to ignore fields they don't know, and to handle enum values they don't recognize. +- **Breaking changes** ship as the next version number, and the previous version keeps working for at least 6 months. See [Non-Functional Requirements → Versioning](../non-functional-requirements/#versioning) for what counts as breaking. - Every change is recorded in the [Changelog](../changelog/). From b7c0194a45fce5144e191002e062254859bee985 Mon Sep 17 00:00:00 2001 From: Adron Hall Date: Wed, 21 Oct 2026 20:29:37 -0700 Subject: [PATCH 2/2] Regenerate MCP tools.json for the updated Glossary tools.json embeds the Glossary as an MCP resource, so gen-check failed. Co-Authored-By: Claude Opus 5.5 --- internal/mcp/tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mcp/tools.json b/internal/mcp/tools.json index 39a3cbf..57a5e56 100644 --- a/internal/mcp/tools.json +++ b/internal/mcp/tools.json @@ -6556,7 +6556,7 @@ "name": "Glossary", "description": "Precise definitions for every term the API uses.", "mimeType": "text/markdown", - "text": "# Glossary\n{: .no_toc }\n\nPrecise definitions for every term used across this site. If a page uses a capitalized term (`Entitlement`, `AppRole`) without defining it inline, it means exactly what's written here.\n{: .fs-6 .fw-300 }\n\n---\n\n#### Access path\nOne way a User reaches an Application: their own personal [Entitlement](#entitlement), or an org-wide grant from an [Organization](#organization) that includes them. Resolved access is the union of every active path. See [Access Control](../access-control/#the-algorithm).\n\n#### Access tier\nWhich [Pricing](../pricing/) tier unlocks an [Application](#application): `free` (every User) or `standard` (Users covered by a [Subscription](#subscription)). See [Applications](../domain-model/applications/#fields).\n\n#### API Key\nA long-lived service credential (`satk_live_…`/`satk_test_…`), scoped to the platform or to one Application. An app-scoped key can only ever touch its own Application's rows. See [API Keys](../api-reference/api-keys/).\n\n#### App token\nThe short-lived (5-minute) JWT an Application verifies locally, with `aud` set to that Application. Carries `entitlement_status` and `effective_permissions`. See [Auth → The app token](../api-reference/auth/#the-app-token).\n\n#### Application\n(also \"App\") One developer's separately-hosted app (web, iOS, macOS, Windows, Linux, …) that uses Substratal Apps for its users, settings, organizations, tenancy, and storage. This is its catalog entry. See [Domain Model → Applications](../domain-model/applications/).\n\n#### AppProfile\nPer-app identity data for one user — e.g. a display handle meaningful only inside that one app. Distinct from the global [Profile](#profile).\n\n#### AppRole\nA [Role](#role) scoped to one Application, defined by that app's own catalog entry rather than by the platform. See [Domain Model → Roles \u0026 Permissions](../domain-model/roles-and-permissions/).\n\n#### AppSettings\nPer-app configuration for one user, layered over that user's global [Settings](#settings) and the Application's declared defaults. See [Workflows → Settings resolution](../workflows/#settings-resolution-in-practice).\n\n#### Audit Event\nAn immutable record of who changed what access, when. See [Domain Model → Orders \u0026 Audit](../domain-model/orders-and-audit/).\n\n#### Device\nOne install of an [Application](#application) on one device, signed in as one [User](#user), with the APNs, FCM, or Web Push token that reaches it (`dvc_…`). Tied to the [Session](#session) that registered it, so signing out stops its pushes. See [Push Notifications → Devices](../api-reference/notifications/#devices).\n\n#### Effective permissions\nThe resolved set of [Permissions](#permission) a user holds for a given Application — the union of the permissions on their app-scoped Roles for that app (explicit assignments plus the app's `default_app_role`), and empty unless the User and their Entitlement are both active. Only that app's own `app.\u003cslug\u003e.*` keys ever appear; platform Roles govern the hub, not apps. See [Access Control](../access-control/#step-2-effective-permissions).\n\n#### Entitlement\nThe join record between a User (or Organization) and an Application: do they own it, and is it currently switched on. The record behind \"turn an app on/off for a user.\" See [Domain Model → Entitlements](../domain-model/entitlements/).\n\n#### Hub\nShorthand used throughout this site for Substratal Apps itself — the platform this API belongs to.\n\n#### MCP\nModel Context Protocol — the standard this site's AI-agent-facing tool-call interface implements. See [MCP Server](../mcp-server/). Introduces no new authorization model of its own; see [Authentication — no new model](../mcp-server/#authentication--no-new-model).\n\n#### Notification\nOne alert or update to one User (`ntf_…`), from an Application or from the platform. Pushed to the User's [Devices](#device) when it can be, and always kept in their inbox for 90 days. See [Push Notifications](../api-reference/notifications/).\n\n#### Order\nNot an entity of this API. It's the developer's own commerce record, in their own billing system. An [Entitlement](#entitlement) carries only an opaque `order_id` reference to it. See [Domain Model → Orders \u0026 Audit](../domain-model/orders-and-audit/#order).\n\n#### Organization\nA domain/grouping object for Users — a company, or a group within a company — used to organize shared admin standing and group-wide Application access. Decoupled from infrastructure placement; see [Tenant](#tenant) and [Tenant vs. Organization](../domain-model/tenancy/#tenant-vs-organization). Pulled into [Phase 2](../roadmap/#phase-2); see [Organizations](../domain-model/users-and-organizations/#organization).\n\n#### OrganizationMembership\nThe join record between a User and an Organization, carrying that User's standing *within* that one Organization (`org_admin` or `member`) and whether it's `pending` (invited, not yet accepted) or `active` — a User can hold this for any number of Organizations at once. See [Domain Model → Users \u0026 Organizations](../domain-model/users-and-organizations/#organizationmembership).\n\n#### Permission\nAn atomic, checkable capability, written as a dotted key (`billing.manage`, `app.timetrack.export`). Never assigned directly to a user — always granted through a [Role](#role).\n\n#### Plan\nThe subscription tier covering a User: `free` or `standard`. A User is on Standard through their own subscription or an Organization's. Business and Enterprise plans come later. Independent of [Tier](#tier). See [Pricing](../pricing/).\n\n#### Platform alert\nA [Notification](#notification) the platform raises itself about the User's account, such as a new sign-in or a changed password. Its category is from a fixed catalog under `security.` and `account.`. See [Push Notifications → Platform alerts](../api-reference/notifications/#platform-alerts).\n\n#### PlatformRole\nA [Role](#role) that applies across the whole hub rather than one Application — `superadmin`, `support`, `billing_admin`, `member`.\n\n#### Profile\nGlobal, cross-app identity and contact data for a User — name, avatar, email, locale. Distinct from [AppProfile](#appprofile).\n\n#### Push credential\nAn Application's own key for one push provider: an APNs `.p8` key, an FCM service account, or a Web Push (VAPID) key pair (`pcr_…`). Write-only and KMS-encrypted. Providers deliver only for the app a token was issued to, so every Application brings its own. See [Push Notifications → Push credentials](../api-reference/notifications/#push-credentials).\n\n#### Role\nA named bundle of [Permissions](#permission). Either a [PlatformRole](#platformrole) or an [AppRole](#approle).\n\n#### Seat\nA covered member of an Organization with a Standard subscription: a distinct, active, non-test-mode member included by the subscription's member selection. The unit an Organization is billed on, $6 each per month. See [Pricing](../pricing/#what-a-user-means-for-an-organization).\n\n#### Session\nOne successful login (`ses_…`). Every refresh token and app token minted from it carries its id as `sid`, and revoking it ends them all. See [Auth → Sessions](../api-reference/auth/#sessions).\n\n#### Settings\nGlobal, cross-app configuration for a User — locale, timezone, notification preferences. Distinct from [AppSettings](#appsettings).\n\n#### Subscription\nA User's or an Organization's Standard subscription (`sbs_…`), billed through Stripe at $6 per covered user per month. While it covers its holder, the platform grants `subscription` [Entitlements](#entitlement) to every standard Application. See [API Reference → Billing](../api-reference/billing/).\n\n#### Tenant\nThe infrastructure-placement and data-isolation boundary for an Application's data — it belongs to the owner of the Application's catalog entry, and isn't billed. `shared` (default), `isolated`, or `dedicated_region`. Not the same thing as [Organization](#organization) — see [Tenant vs. Organization](../domain-model/tenancy/#tenant-vs-organization). See [Domain Model → Tenancy](../domain-model/tenancy/).\n\n#### Tier\nWhere a [Tenant](#tenant)'s data physically lives: `shared`, `isolated`, or `dedicated_region`. Independent of [Plan](#plan). The non-`shared` tiers are reserved for business and enterprise pricing, which isn't offered yet.\n\n#### User\nA person with an account on Substratal. One identity, used everywhere in the hub and, via the [Trust Model](../trust-model/), in every app they launch from it.\n\n#### Webhook\nA signed HTTPS POST the hub sends an Application when something changes. `access.revoked` and `role.removed` are required subscriptions. See [Webhooks](../api-reference/webhooks/).\n" + "text": "# Glossary\n{: .no_toc }\n\nPrecise definitions for every term used across this site. If a page uses a capitalized term (`Entitlement`, `AppRole`) without defining it inline, it means exactly what's written here.\n{: .fs-6 .fw-300 }\n\n---\n\n#### Access path\nOne way a User reaches an Application: their own personal [Entitlement](#entitlement), or an org-wide grant from an [Organization](#organization) that includes them. Resolved access is the union of every active path. See [Access Control](../access-control/#the-algorithm).\n\n#### Access tier\nWhich [Pricing](../pricing/) tier unlocks an [Application](#application): `free` (every User) or `standard` (Users covered by a [Subscription](#subscription)). See [Applications](../domain-model/applications/#fields).\n\n#### API Key\nA long-lived service credential (`satk_live_…`/`satk_test_…`), scoped to the platform or to one Application. An app-scoped key can only ever touch its own Application's rows. See [API Keys](../api-reference/api-keys/).\n\n#### API version\nThe version of the REST API a request is written against, sent as a whole number in the required `Substratal-Version` header. `1` is the only one so far. Unrelated to a webhook [payload version](#payload-version). See [Conventions → Versioning](../api-reference/conventions/#versioning).\n\n#### App token\nThe short-lived (5-minute) JWT an Application verifies locally, with `aud` set to that Application. Carries `entitlement_status` and `effective_permissions`. See [Auth → The app token](../api-reference/auth/#the-app-token).\n\n#### Application\n(also \"App\") One developer's separately-hosted app (web, iOS, macOS, Windows, Linux, …) that uses Substratal Apps for its users, settings, organizations, tenancy, and storage. This is its catalog entry. See [Domain Model → Applications](../domain-model/applications/).\n\n#### AppProfile\nPer-app identity data for one user — e.g. a display handle meaningful only inside that one app. Distinct from the global [Profile](#profile).\n\n#### AppRole\nA [Role](#role) scoped to one Application, defined by that app's own catalog entry rather than by the platform. See [Domain Model → Roles \u0026 Permissions](../domain-model/roles-and-permissions/).\n\n#### AppSettings\nPer-app configuration for one user, layered over that user's global [Settings](#settings) and the Application's declared defaults. See [Workflows → Settings resolution](../workflows/#settings-resolution-in-practice).\n\n#### Audit Event\nAn immutable record of who changed what access, when. See [Domain Model → Orders \u0026 Audit](../domain-model/orders-and-audit/).\n\n#### Device\nOne install of an [Application](#application) on one device, signed in as one [User](#user), with the APNs, FCM, or Web Push token that reaches it (`dvc_…`). Tied to the [Session](#session) that registered it, so signing out stops its pushes. See [Push Notifications → Devices](../api-reference/notifications/#devices).\n\n#### Effective permissions\nThe resolved set of [Permissions](#permission) a user holds for a given Application — the union of the permissions on their app-scoped Roles for that app (explicit assignments plus the app's `default_app_role`), and empty unless the User and their Entitlement are both active. Only that app's own `app.\u003cslug\u003e.*` keys ever appear; platform Roles govern the hub, not apps. See [Access Control](../access-control/#step-2-effective-permissions).\n\n#### Entitlement\nThe join record between a User (or Organization) and an Application: do they own it, and is it currently switched on. The record behind \"turn an app on/off for a user.\" See [Domain Model → Entitlements](../domain-model/entitlements/).\n\n#### Hub\nShorthand used throughout this site for Substratal Apps itself — the platform this API belongs to.\n\n#### MCP\nModel Context Protocol — the standard this site's AI-agent-facing tool-call interface implements. See [MCP Server](../mcp-server/). Introduces no new authorization model of its own; see [Authentication — no new model](../mcp-server/#authentication--no-new-model).\n\n#### Notification\nOne alert or update to one User (`ntf_…`), from an Application or from the platform. Pushed to the User's [Devices](#device) when it can be, and always kept in their inbox for 90 days. See [Push Notifications](../api-reference/notifications/).\n\n#### Order\nNot an entity of this API. It's the developer's own commerce record, in their own billing system. An [Entitlement](#entitlement) carries only an opaque `order_id` reference to it. See [Domain Model → Orders \u0026 Audit](../domain-model/orders-and-audit/#order).\n\n#### Organization\nA domain/grouping object for Users — a company, or a group within a company — used to organize shared admin standing and group-wide Application access. Decoupled from infrastructure placement; see [Tenant](#tenant) and [Tenant vs. Organization](../domain-model/tenancy/#tenant-vs-organization). Pulled into [Phase 2](../roadmap/#phase-2); see [Organizations](../domain-model/users-and-organizations/#organization).\n\n#### OrganizationMembership\nThe join record between a User and an Organization, carrying that User's standing *within* that one Organization (`org_admin` or `member`) and whether it's `pending` (invited, not yet accepted) or `active` — a User can hold this for any number of Organizations at once. See [Domain Model → Users \u0026 Organizations](../domain-model/users-and-organizations/#organizationmembership).\n\n#### Payload version\nThe shape of a webhook's payloads, a date pinned on each subscription as `api_version` (currently `2026-10-05`) and moved forward only by its owner. Unrelated to the [API version](#api-version). See [Webhooks → Payload versions](../api-reference/webhooks/#payload-versions).\n\n#### Permission\nAn atomic, checkable capability, written as a dotted key (`billing.manage`, `app.timetrack.export`). Never assigned directly to a user — always granted through a [Role](#role).\n\n#### Plan\nThe subscription tier covering a User: `free` or `standard`. A User is on Standard through their own subscription or an Organization's. Business and Enterprise plans come later. Independent of [Tier](#tier). See [Pricing](../pricing/).\n\n#### Platform alert\nA [Notification](#notification) the platform raises itself about the User's account, such as a new sign-in or a changed password. Its category is from a fixed catalog under `security.` and `account.`. See [Push Notifications → Platform alerts](../api-reference/notifications/#platform-alerts).\n\n#### PlatformRole\nA [Role](#role) that applies across the whole hub rather than one Application — `superadmin`, `support`, `billing_admin`, `member`.\n\n#### Profile\nGlobal, cross-app identity and contact data for a User — name, avatar, email, locale. Distinct from [AppProfile](#appprofile).\n\n#### Push credential\nAn Application's own key for one push provider: an APNs `.p8` key, an FCM service account, or a Web Push (VAPID) key pair (`pcr_…`). Write-only and KMS-encrypted. Providers deliver only for the app a token was issued to, so every Application brings its own. See [Push Notifications → Push credentials](../api-reference/notifications/#push-credentials).\n\n#### Role\nA named bundle of [Permissions](#permission). Either a [PlatformRole](#platformrole) or an [AppRole](#approle).\n\n#### Seat\nA covered member of an Organization with a Standard subscription: a distinct, active, non-test-mode member included by the subscription's member selection. The unit an Organization is billed on, $6 each per month. See [Pricing](../pricing/#what-a-user-means-for-an-organization).\n\n#### Session\nOne successful login (`ses_…`). Every refresh token and app token minted from it carries its id as `sid`, and revoking it ends them all. See [Auth → Sessions](../api-reference/auth/#sessions).\n\n#### Settings\nGlobal, cross-app configuration for a User — locale, timezone, notification preferences. Distinct from [AppSettings](#appsettings).\n\n#### Subscription\nA User's or an Organization's Standard subscription (`sbs_…`), billed through Stripe at $6 per covered user per month. While it covers its holder, the platform grants `subscription` [Entitlements](#entitlement) to every standard Application. See [API Reference → Billing](../api-reference/billing/).\n\n#### Tenant\nThe infrastructure-placement and data-isolation boundary for an Application's data — it belongs to the owner of the Application's catalog entry, and isn't billed. `shared` (default), `isolated`, or `dedicated_region`. Not the same thing as [Organization](#organization) — see [Tenant vs. Organization](../domain-model/tenancy/#tenant-vs-organization). See [Domain Model → Tenancy](../domain-model/tenancy/).\n\n#### Tier\nWhere a [Tenant](#tenant)'s data physically lives: `shared`, `isolated`, or `dedicated_region`. Independent of [Plan](#plan). The non-`shared` tiers are reserved for business and enterprise pricing, which isn't offered yet.\n\n#### User\nA person with an account on Substratal. One identity, used everywhere in the hub and, via the [Trust Model](../trust-model/), in every app they launch from it.\n\n#### Webhook\nA signed HTTPS POST the hub sends an Application when something changes. `access.revoked` and `role.removed` are required subscriptions. See [Webhooks](../api-reference/webhooks/).\n" } ] }