Changelog

What’s changed in this specification over time. Questions still awaiting a decision are tracked in the repo’s DECISIONS.md and marked with an “Awaiting decision” callout on the pages they affect; this page tracks what’s already changed. When a decision is made, add a new dated entry here rather than editing an old one.


2026-10-05 — decisions moved into the spec

  • Removed the Decisions page. Every resolved decision (#1–#15) now lives on the page an implementer would actually read, with its rationale, instead of on a separate log. For example, identity is in Auth → Native auth and per-Organization SSO, and billing ownership is in Orders & Audit → Billing system of record. New sections were added where a page didn’t yet carry the reasoning, among them Trust Model → Applications are separately hosted, Tenancy → Tenant vs. Organization, Settings → Schema validation, and Pricing → How the subscription is charged. Every link to the old page was repointed.
  • Confirmed decisions #16–#30, which the implementation-readiness pass below had written into the spec as proposals. The spec already implemented each one, so the change here is that their reasoning is now stated as settled on the page each one governs: app-token issuance (Auth → Getting an app token), app-confined API Key permissions, concierge onboarding, derived access.* webhooks, order_id as a reference, typed settings via views and expression indexes, self-service Organization creation, seats/lapse/Stripe object model, the 7-day erasure grace period, the region list, Amazon SES, and test-mode isolation.
  • Applied #29: renamed GET /v1/users/{id}/applications/{appId}/effective-permissions to GET /v1/users/{id}/apps/{appId}/effective-permissions (Roles & Permissions), matching the per-user Profile and Settings paths. Free to rename now, since no endpoint is implemented yet. The operationId (permissions.getEffective) and MCP tool name are unchanged.
  • Opened #31 in DECISIONS.md: confirming the Pricing dollar amounts, plus annual plans, a Team trial, and whether Starter requires a card on file. None blocks the build.

2026-10-05 — implementation-readiness pass

A full review aimed at “can development start and finish against this spec without guessing”. Every fix below is stated inline on the page it affects. The product calls this pass had to make are logged as Decisions #16–#30, all 🟡 Proposed (now in DECISIONS.md, each marked with an “Awaiting decision” callout on the page it affects). Each has a complete default written into the spec and a named alternative, so none blocks implementation, but each needs confirmation.

Contradictions fixed

  • Developers had no safe credential to reflect their billing. The billing-system-of-record rule requires developers to call Entitlements, but app-scoped keys could never hold entitlements.manage. Resolved with app-confined permissions (#17).
  • Org self-grant hole: once Organizations are self-service (#22), an org admin creating their own org-wide grant would mean free access to any app. Org grants can now only be created by whoever controls the Application. Org admins scope and toggle, never create.
  • Stripe lapse vs. database: “cancellation reverts plan to starter” violated check (plan != 'starter' or owner_type = 'user') for Organization-owned Tenants. That constraint also referenced a nonexistent owner_type column, now added. Replaced by restricted mode (#24).
  • Order was modeled as an entity (ord_ prefix, billing.manage = “Manage Orders”, a nonexistent /v1/webhooks/incoming/billing in Workflows), even though orders are the developer’s. order_id is now an opaque reference, and billing.* permissions cover platform billing (#20).
  • Revocation gaps: the required webhooks missed org-member removal, member_scope exclusions, org-grant changes, and user suspension/deletion. The access algorithm didn’t check user status at all. Added derived access.granted/access.revoked webhooks and user.status to allow() (#19).
  • Platform permissions leaked into app JWTs: effective_permissions unioned platform Roles. It’s now app keys only.
  • Typed settings via generated columns would collide across apps, hit Postgres’s 1,600-column ceiling, and break inserts on schema change. Replaced by per-app views and partial expression indexes (#21).
  • openapi.yaml’s Page schema put next_cursor/has_more at the top level, while every prose example nests them under page. Fixed (PageEnvelope).
  • Workflows emitted entitlement.revoked for a disable. Webhook retries said “up to 5 attempts” with 5 retry intervals, which is 6. Fixed.
  • tenants.manage was missing from the permissions/roles examples. The Applications settings_schema example had no default, yet every resolved example used one. review_notes_required was 400 in violation of the 400/422 mapping (now 422). The AppRoles plan limit is now defined consistently. The webhook plan limit is now per Tenant.

Completed (previously missing or unspecified)

  • Auth, rewritten: signup, MFA login challenge, recovery codes, refresh-token rotation with reuse detection, sessions, password change/reset, email verification and change, invitations, account lockout, password policy, JWKS/key rotation, how an Application gets its per-app JWT (embedded app-tokens now; hosted authorization code + PKCE later, #16), and the app-token claim set.
  • New Billing API (subscription, usage, Checkout, Portal), Pricing seat definition, caps, enforcement, lapse rules, and the full Stripe catalog (#23, #25). Root DEPLOYMENT.md → Stripe Billing is rewritten (event handling, re-fetch-on-event, seat sync), with new sections for SES email (#28) and the webhook outbox.
  • Users: invitations, pending email change, search filters, the erasure-request endpoint (#26). Roles: get/update/delete, derived ids, escalation guard, default_app_role, implicit assignments. Organizations: self-service create, member add/role-change/leave, last-admin guard. Tenancy: tier-change request lifecycle (scheduled added), owner visibility, region list (#27). Webhooks: get/update, secret rotation with overlap, test, delivery log, redeliver, api_version pinning, auto-disable, SSRF rules. API Keys: get/update, owner self-service for app keys (#18), secret_hint, expires_at.
  • Applications: permissions, redirect_uris, default_app_role, support_url/email_from_name, visibility rules, review-transition table. Settings: exact resolution rules, settings_schema rules, x-pii, stale overrides. Entitlements: full transition matrix, uniqueness, PATCH-able fields, expiry sweep, a 24-hour limit on delete-in-error.
  • Conventions: PATCH merge semantics, ETag/If-Match, size limits, ordering, cross-cutting error codes, response headers, credential prefixes. NFR: password hashing, brute-force limits, per-IP auth rate limits, security logging, SLOs, webhook payload versioning, and a revised hard-delete cascade.
  • Audit Events: an actor/target model (API keys and the system can be actors; many actions have no target user), request_id, and an action catalog expanded to every state change, mirrored exactly in openapi.yaml.
  • Database Schema: sessions, tokens, recovery codes, webhook outbox/deliveries, tier-change and erasure requests, Stripe events, version columns for ETags, test-mode isolation (#30), and corrected entitlement uniqueness.
  • openapi.yaml rewritten: 97 operations (from 56; every existing operationId unchanged), typed error responses on every operation, and x-substratal-destructive / x-substratal-phase extensions. The MCP Server page now holds the single canonical destructive-operation list.

Not changed, needs a decision: #29, the mixed /apps/{appId} vs. /applications/{appId} path spelling. (Since applied: see the entry above.)

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 /v1 path 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_destructive already uses), returning 409/code: "plan_limit_reached". New database constraint: check (plan != 'starter' or owner_type = 'user') on tenants.
  • 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/after dropped 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. Root DEPLOYMENT.md → Audit log archival specifies the AWS mechanics (EventBridge Scheduler → Lambda → S3 Glacier Deep Archive).
  • Fixed Orders & Audit → Action catalog: added tenant.tier_change_requested and tenant.tier_changed, both already live in openapi.yaml’s AuditAction enum 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) to ApiKeyWrite/ApiKey in openapi.yaml — present in the database schema and every prose example since API Keys was written, absent from the YAML.
  • Added plan to the Tenant schema in openapi.yaml (new TenantPlan enum) — 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-Key to organizations.grantEntitlement in openapi.yaml — the near-identical entitlements.grant already required it; this org-wide variant had been missed.
  • Added entitlement.member_scope_changed to the WebhookEvent enum in openapi.yaml and 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: DELETE now rejects (409/code: "entitlement_order_linked") whenever order_id is set, for every caller — not just a restrict_destructive agent 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_id is now exactly one, never neither — closing a real bug where a theoretical ownerless “system app” had no owner for tenant_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 matching check to applications in 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 DELETE actually 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 reach status: suspended — added GET/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 the 409-vs-422 distinction, 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 for action values.
  • 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, and UserRoleAssignment, which the original count silently omitted.
  • Moved the original single-document draft from docs/specs/Substratal-Hub-Access-API.md to root ORIGINAL-SPEC-DRAFT.md, with a superseded banner — it contradicts several current decisions structurally (no Tenant concept, single-method auth) and sitting inside docs/ risked it being mistaken for current spec despite being excluded from the build.
  • Updated root README.md and PLAN.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 me self-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_id and review_status to 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 (now Database Schema → Typed fields), #9 App developer/publisher model (now Applications → The review lifecycle), #10 Compliance scope (now Compliance).
  • 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 (shared default, isolated, dedicated_region), support-gated tier changes via tenants.manage and POST /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 new tenants/organization_memberships tables in Database Schema. Directly resolves Compliance’s previously-open data-residency gap.
  • Replaced User.organization_id with 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. Carries role: 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_overrides to org-wide Entitlements and resolved Decision #13: an Organization’s own grant is authoritative over its own members (an allowlist/denylist an 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 the granted_via shape that surfaces which Organization is responsible for a given member’s resolved access.
  • Resolved Decision #8: storage stays Postgres jsonb as the source of truth, with a Postgres generated column backing every field an Application has declared in its settings_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’s operationIds rather than hand-authored, so it tracks the REST API automatically — see Roadmap → The MCP server tracks the REST API automatically. Added operationId to 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 tier as a priced option — shared included, isolated +$750/month, dedicated_region +$1,500/month) — distinct from what that developer charges their own end users, per Decision #4. Added plan (starter|team|enterprise) to Tenant, independent of tier, with a database constraint tying isolated/dedicated_region to enterprise only. 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.md into this site.

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.