Changelog
What’s changed in this specification over time. Decisions tracks what’s still open; this page tracks what’s already changed — the two are related but answer different questions, so keep entries here even after a decision above gets resolved (add a new dated entry, don’t just flip the status).
2026-10-05
- Docs are now versioned automatically. Every publish of this site gets a new version number, starting at v1.0.0. The current version shows in the top right of every page’s header and links to the new Versions page. Each version is also kept as a frozen, read-only copy under
/versions/<version>/, so a link to an old version keeps showing exactly what that version said. This is the version of the documentation, separate from the API’s own/v1path version.
A full consistency/UX review surfaced drift between the prose docs, openapi.yaml, and the root planning files — this entry is every fix that came out of it:
- Fixed a self-contradiction in Pricing: Starter’s own “For” cell claimed “3+ Applications” while its own Applications row capped at 1. Reworded; “3+ Applications” is Team’s case, not Starter’s.
- Added Pricing → Enforcement: the Applications/custom-AppRole/webhook-subscription caps now have a real mechanism — checked server-side at creation time against the owning Tenant’s plan, independent of the caller’s own permission standing (the same “two independent gates” pattern
restrict_destructivealready uses), returning409/code: "plan_limit_reached". New database constraint:check (plan != 'starter' or owner_type = 'user')ontenants. - Resolved the audit-retention contradiction between Pricing’s plan-tiered “retention” numbers and Non-Functional Requirements/Compliance’s “retained indefinitely”: those numbers were never a deletion period, they’re a hot-storage window — added Non-Functional Requirements → Audit log lifecycle specifying the archive-to-cold-storage mechanism (shape-only,
before/afterdropped at archive time, same redaction the hard-delete cascade already does) that makes “indefinite” literally true without every plan paying for the same hot-storage size. RootDEPLOYMENT.md→ Audit log archival specifies the AWS mechanics (EventBridge Scheduler → Lambda → S3 Glacier Deep Archive). - Fixed Orders & Audit → Action catalog: added
tenant.tier_change_requestedandtenant.tier_changed, both already live inopenapi.yaml’sAuditActionenum but missing from this page’s own “authoritative source” table. - Added the MFA enrollment endpoint to
openapi.yaml(POST /v1/users/{id}/mfa/totp) — fully specified in Auth since its own addition, never added to the machine-readable spec. - Added
mode(live/test) toApiKeyWrite/ApiKeyinopenapi.yaml— present in the database schema and every prose example since API Keys was written, absent from the YAML. - Added
planto theTenantschema inopenapi.yaml(newTenantPlanenum) — present in Tenancy, Database Schema, and this page’s own earlier Pricing/Stripe entries, absent from the YAML. Also added it to API Reference → Tenancy’s own example, which had drifted from the matching Domain Model example. - Added
Idempotency-Keytoorganizations.grantEntitlementinopenapi.yaml— the near-identicalentitlements.grantalready required it; this org-wide variant had been missed. - Added
entitlement.member_scope_changedto theWebhookEventenum inopenapi.yamland to Webhooks → Event types — Workflows already implied an app could subscribe to it; the event didn’t actually exist yet. - Closed a real gap in Entitlements:
DELETEnow rejects (409/code: "entitlement_order_linked") wheneverorder_idis set, for every caller — not just arestrict_destructiveagent key. Hard-deleting a real, order-linked Entitlement was previously stopped only by prose discipline, inconsistent with the platform’s own stance that this exact class of operation is destructive enough to hard-gate. - Tightened the Application ownership constraint:
owner_user_id/owner_organization_idis now exactly one, never neither — closing a real bug where a theoretical ownerless “system app” had no owner fortenant_id(not null) to resolve from. Every current example already set an owner; this just makes the constraint match what was already practiced. Added the matchingchecktoapplicationsin Database Schema. - Added a “most Users never touch Tenant at all” note to Tenancy — only an Application owner gets one; the common case (someone who signs up, often through an app itself, purely to use it) never does.
- Added Conventions → Delete semantics: hard vs. soft: a resource-by-resource catalog of what
DELETEactually does — hard, soft, soft-but-irreversible, or no endpoint at all — and what a soft delete generically means (status flips, record retained, excluded from default lists, nothing scrubbed or moved). Building this surfaced that Organizations had no way to actually reachstatus: suspended— addedGET/PATCH /v1/organizations/{id}. - Elaborated Conventions → Errors: fixed the illustrative example (
entitlement_not_active, which no page actually defines) to a real code (entitlement_required); added an explicit HTTP-status-to-error-class mapping table and the409-vs-422distinction, for implementation; clarified there’s no single global error-code catalog — each resource’s own table is authoritative, the same pattern Orders & Audit already uses foractionvalues. - Fixed MCP Server’s hardcoded operation count (“all 55 operations”) — already stale (56+ since this was written), and guaranteed to drift again. Reworded to not state a number at all.
- Fixed Domain Model’s “Ten entities” framing — added a Supporting entities table listing
UserIdentity,SSOConnection,OrganizationMembership, andUserRoleAssignment, which the original count silently omitted. - Moved the original single-document draft from
docs/specs/Substratal-Hub-Access-API.mdto rootORIGINAL-SPEC-DRAFT.md, with a superseded banner — it contradicts several current decisions structurally (no Tenant concept, single-method auth) and sitting insidedocs/risked it being mistaken for current spec despite being excluded from the build. - Updated root
README.mdandPLAN.md, both of which had gone stale against decisions resolved in the prior round: README’s Status section still named Decision #1 as the one open item (all 15 are resolved); PLAN’s Phase 3 section still described the Application review process as unspecified (fully specified by Decision #9).
2026-10-04
- Added API Keys as its own resource — service-to-service credentials were referenced throughout (Conventions, NFR, Trust Model) but never actually specified.
- Added a global
GET /v1/entitlements(admin/support, filterable) alongside the existing per-user list — there was no way to answer “who currently has app X enabled” without it. - Added a canonical Platform permission catalog and Platform role grants table, and added an explicit
Requires: <permission>line to every endpoint that was missing one. - Added a canonical Audit Event action catalog.
- Filled in previously-deferred numbers: rate limits, the idempotency-key TTL (24h), and the version-deprecation window (6 months) — see Non-Functional Requirements.
- Added the
meself-addressing convention and a credential-vs-resource-ID distinction to Conventions. - Added Quickstart (a runnable walkthrough) and this Changelog.
- Fixed: the Audit endpoint’s documented filter name (
user_id→target_user_id) didn’t match its own example. - Added Deployment Architecture: first-deployment and scale-out plans on AWS, replacing an earlier Vercel-based plan, designed around predictable, capped cost rather than deploy-and-see.
- Reframed the product. Substratal Apps is the user/organization/tenancy/settings/storage API layer an app developer builds on — not a storefront a customer browses. See Home and What Substratal Apps actually is. Every page that described the platform as “the hub a customer lands on to reach apps they purchased” was describing the eventual onboarding UI, not the API’s own purpose.
- Added
owner_user_id/owner_organization_idandreview_statusto Application — a real gap once outside developers can register their own apps: there was no concept of who owns an Application’s catalog entry, only platform-admin control. API Reference → Applications now distinguishes an owner’s self-service rights from platform moderation. - Added Trust Model → Trust runs the other direction too: the existing API Key scoping and webhook signing already made third-party apps safe to onboard later — now stated explicitly instead of left implicit.
- Resolved Decisions #2 (Organizations) and #4 (Billing system of record) based on product clarification: both individual and team end users are expected (Organizations pulled forward to Phase 2), and each Application’s developer owns their own billing relationship — Substratal Apps was never going to be a payment processor. Added three new open decisions: #8 Storage primitive scope, #9 App developer/publisher model, #10 Compliance scope.
- Added Compliance & Data Protection: a calibrated recommendation on SOC 2 (design the posture now, defer the formal audit), GDPR/CCPA (not optional, build for it now), and HIPAA (don’t build for it speculatively — it’s a function of which apps join the platform, not the core product).
- Added Database Schema: Postgres types, constraints, and indexes for every entity — the implementer-facing counterpart to the API-caller-facing field tables.
- Added implementer-readiness gaps across Non-Functional Requirements (concurrency control via
ETag/If-Match, transaction boundaries, the concrete Postgres Row-Level Security multi-tenancy mechanism, enum forward-compatibility policy, request tracing, testing strategy) and Conventions (test-vs-live API keys, multi-field validation error shape, default exclusion of soft-deleted/terminal records from lists, idempotency-key storage detail). - Added Webhooks → Verifying the signature: the concrete
Substratal-Signature: t=…,v1=…header format and replay-window guidance — “HMAC-SHA256 over the raw body” wasn’t enough to actually implement against. - Added Deployment Architecture → Local development (the RDS Data API choice has no clean local emulator — addressed via a repository-pattern seam, not by changing the AWS choice) and → Growth trajectory (the stated dozens → 10x → 100x plan mapped against Tier 0 and the Scale-out triggers).
- Added Tenant: the infrastructure-placement and data-isolation entity “tenancy” named in the product pitch but previously unspecified. Tied to the subscription — the owner of an Application’s catalog entry (
owner_user_id/owner_organization_id), not to Organization, which stays a pure grouping/domain object with no infrastructure meaning. Three tiers (shareddefault,isolated,dedicated_region), support-gated tier changes viatenants.manageandPOST /v1/tenants/{id}/tier-change-requests, a scheduled-downtime migration process, and deliberately no reach into a downstream Application’s own infrastructure. See Decisions #11 and #12, API Reference → Tenancy, Deployment Architecture → Tenancy tiers, and the newtenants/organization_membershipstables in Database Schema. Directly resolves Compliance’s previously-open data-residency gap. - Replaced
User.organization_idwith OrganizationMembership, a many-to-many join — a User can belong to more than one Organization at once (even across different Tenants), which a single column couldn’t express. Carriesrole: org_admin | member, which also resolves Organizations → Delegated admin’s previously-unresolved question: an org admin is this field, not a third Role scope. - Added
member_scope/member_overridesto org-wide Entitlements and resolved Decision #13: an Organization’s own grant is authoritative over its own members (anallowlist/denylistan org admin controls), but never reaches into a member’s separate, personally-sourced Entitlement to the same app. Access Control → Organization vs. User precedence has the formal resolution rule and a worked multi-org example; Entitlements → Attribution shows thegranted_viashape that surfaces which Organization is responsible for a given member’s resolved access. - Resolved Decision #8: storage stays Postgres
jsonbas the source of truth, with a Postgres generated column backing every field an Application has declared in itssettings_schema— typed and indexed, without giving up the blob’s flexibility for undeclared fields. - Added Deployment Architecture → Database engine: AWS options compared: RDS vs. Aurora Provisioned vs. Aurora Serverless v2, evaluated for lowest starting price. Aurora Serverless v2 stays the pick — not the cheapest floor in isolation (~$30/month more than bare RDS), but the only option that keeps one connection mechanism across every Tenancy tier and avoids a VPC/NAT Gateway entirely; the comparison and the reasoning are now explicit rather than asserted.
-
Added MCP Server: a Model Context Protocol server letting an AI agent call the Platform API as typed tools instead of hand-rolled HTTP calls. Introduces no new authorization model — every tool call forwards the caller’s own Bearer credential (user token or API Key) into the same REST call and the same Access Control check. The tool list is generated from
openapi.yaml’soperationIds rather than hand-authored, so it tracks the REST API automatically — see Roadmap → The MCP server tracks the REST API automatically. AddedoperationIdto all 55 operations in openapi.yaml (none had one before) specifically so this generation scheme is buildable today, not aspirational. Deployment Architecture → MCP server specifies the AWS build-out: a second, dedicated Lambda behind the existing API Gateway at/mcp(no new domain, no VPC, no database — fully stateless at Tier 0), with named scale-out triggers (Provisioned Concurrency, a Lambda Function URL with streaming responses for server-push/resumability, a small DynamoDB table if session state ever outgrows a signed token). Added Decision #14, still open: whether agent callers should get a recommended (or eventually required) narrower API Key scoping convention, given an LLM’s tool-call decision is a different risk shape from deterministic service code. - Added Pricing: three subscription tiers for what Substratal itself charges the Application-owner developer (Starter free, Team per-seat, Enterprise with a choice of Tenant
tieras a priced option —sharedincluded,isolated+$750/month,dedicated_region+$1,500/month) — distinct from what that developer charges their own end users, per Decision #4. Addedplan(starter|team|enterprise) to Tenant, independent oftier, with a database constraint tyingisolated/dedicated_regiontoenterpriseonly. Added Decision #15: how Substratal bills this subscription is still open (leans Stripe Billing), with no corresponding API surface built yet. Cites the AWS Pricing, Billing and Cost Management, and Postgres MCP servers as the tooling to keep these numbers grounded in real infrastructure cost over time.
2026-10-03
- Initial publication: domain model, access control, full API reference, workflows, trust model, non-functional requirements, roadmap, open decisions, and glossary — elaborated from the original single-document draft at
docs/specs/Substratal-Hub-Access-API.mdinto this site.