Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:

- `manifest.yml`: every version, newest first.
- `<version>/`: that version's frozen static build, served at
https://adron.github.io/substratalapps.com/versions/<version>/
https://compositecode.github.io/substratalapps.com/versions/<version>/
EOF
fi
git config user.name "github-actions[bot]"
Expand Down
82 changes: 41 additions & 41 deletions DECISIONS.md

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions DEPLOYMENT.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ORIGINAL-SPEC-DRAFT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
> **Superseded.** This is the original single-document draft the full specification at [adron.github.io/substratalapps.com](https://adron.github.io/substratalapps.com/) was elaborated from — kept here for history, in the project root rather than the docs site, specifically so it's never mistaken for current spec. It contradicts current decisions in several structural ways (no [Tenant](https://adron.github.io/substratalapps.com/domain-model/tenancy/) concept, single-method auth, a 3-question access algorithm instead of the current 2-axis union model) — if anything below conflicts with the live docs site or [`decisions.md`](https://adron.github.io/substratalapps.com/decisions/), the live site wins, always. See [README.md](README.md) and the [Changelog](https://adron.github.io/substratalapps.com/changelog/#2026-10-03)'s initial-publication entry for how this became that.
> **Superseded.** This is the original single-document draft the full specification at [compositecode.github.io/substratalapps.com](https://compositecode.github.io/substratalapps.com/) was elaborated from — kept here for history, in the project root rather than the docs site, specifically so it's never mistaken for current spec. It contradicts current decisions in several structural ways (no [Tenant](https://compositecode.github.io/substratalapps.com/domain-model/tenancy/) concept, single-method auth, a 3-question access algorithm instead of the current 2-axis union model) — if anything below conflicts with the live docs site or [`decisions.md`](https://compositecode.github.io/substratalapps.com/decisions/), the live site wins, always. See [README.md](README.md) and the [Changelog](https://compositecode.github.io/substratalapps.com/changelog/#2026-10-03)'s initial-publication entry for how this became that.

# Substratal Hub — Access & Identity API

Expand Down
56 changes: 28 additions & 28 deletions PLAN.md

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

The user/organization/tenancy/settings/storage API layer an app developer would otherwise have to build themselves — comparable in shape to Auth0, Clerk, or WorkOS, scoped around everything a multi-tenant app needs: accounts, roles and permissions, per-app settings, team/tenancy structure, and per-user storage, all behind one API. When your business is building applications — on iOS, macOS, Windows, Linux, or the web — Substratal Apps removes the user-management burden so you can focus on the application itself.

**Full API specification: [adron.github.io/substratalapps.com](https://adron.github.io/substratalapps.com/)**
**Full API specification: [compositecode.github.io/substratalapps.com](https://compositecode.github.io/substratalapps.com/)**

## Where things are

This repository is split deliberately, by audience:

| Audience | Lives | Why |
|---|---|---|
| **An API consumer** — a developer building an Application on this platform, or their AI coding agent | [`docs/`](docs/), published at [adron.github.io/substratalapps.com](https://adron.github.io/substratalapps.com/) | The API specification, and nothing else: domain model, access control, every endpoint and schema, workflows, the MCP server for agent callers, pricing. Everything a consumer needs to build *against* this API. |
| **Whoever is building *this* API** | The project root (this file and its siblings) | Deployment/infrastructure, the implementation build order, and the internal decision log aren't things a consumer needs to use the API — they're how it gets built and run. Kept separate so the docs site stays focused, per its own [Home page](https://adron.github.io/substratalapps.com/#what-this-site-is). |
| **An API consumer** — a developer building an Application on this platform, or their AI coding agent | [`docs/`](docs/), published at [compositecode.github.io/substratalapps.com](https://compositecode.github.io/substratalapps.com/) | The API specification, and nothing else: domain model, access control, every endpoint and schema, workflows, the MCP server for agent callers, pricing. Everything a consumer needs to build *against* this API. |
| **Whoever is building *this* API** | The project root (this file and its siblings) | Deployment/infrastructure, the implementation build order, and the internal decision log aren't things a consumer needs to use the API — they're how it gets built and run. Kept separate so the docs site stays focused, per its own [Home page](https://compositecode.github.io/substratalapps.com/#what-this-site-is). |

Root-level documents:

Expand All @@ -38,13 +38,13 @@ Decided 2026-10-06. The API is written in **Go**, deployed to **AWS Lambda**, an

| Layer | Choice | Why |
|---|---|---|
| Language | **Go** (latest stable release, pinned in `go.mod`) | Cold starts in the tens of milliseconds and small memory use on Lambda. That directly serves the p95 < 150 ms target for `effective-permissions`, `app-tokens`, and `oauth/token` in [Non-Functional Requirements](https://adron.github.io/substratalapps.com/non-functional-requirements/), and the cost-first rules in [DEPLOYMENT.md](DEPLOYMENT.md#cost-principles). Strongly typed, stable for years, and plain enough that a one-person team and an AI assistant can both read and change it safely. |
| Language | **Go** (latest stable release, pinned in `go.mod`) | Cold starts in the tens of milliseconds and small memory use on Lambda. That directly serves the p95 < 150 ms target for `effective-permissions`, `app-tokens`, and `oauth/token` in [Non-Functional Requirements](https://compositecode.github.io/substratalapps.com/non-functional-requirements/), and the cost-first rules in [DEPLOYMENT.md](DEPLOYMENT.md#cost-principles). Strongly typed, stable for years, and plain enough that a one-person team and an AI assistant can both read and change it safely. |
| Lambda runtime | `provided.al2023` on **arm64** (Graviton) | Go compiles to one static `bootstrap` binary. arm64 is cheaper per GB-second than x86 for the same work. |
| HTTP layer | Standard library `net/http`. The route table, path/query/header binding, and the `ServerInterface` every operation must implement are **generated from [`docs/openapi.yaml`](docs/openapi.yaml)** by `oapi-codegen`. Request and response bodies are hand-written in `internal/core`. | The spec is the contract: an operation without a handler doesn't compile. The generated *body* models can't express explicit JSON `null`s or PATCH field presence, both of which Conventions requires, so bodies are hand-written and the contract tests validate every response against `openapi.yaml` instead. |
| Lambda adapter | `aws-lambda-go`, plus a thin adapter that turns API Gateway HTTP API events into `http.Request`s | The **same** `http.Handler` serves requests on a laptop and in Lambda. Nothing in handler code knows which one it's running in. |
| Database | **PostgreSQL 16**. In AWS: Aurora Serverless v2 through the **RDS Data API**. Locally and in CI: plain Postgres through **`pgx`**. | Both implement one small driver interface (`internal/db`), as [DEPLOYMENT.md → Local development](DEPLOYMENT.md#local-development) requires. See [The two database backends](#the-two-database-backends). |
| Migrations | Plain, numbered SQL files in `migrations/`, applied by an in-repo `cmd/migrate` tool | Same tool, same files, same tracking table (`schema_migrations`) in every environment. An in-repo runner is used instead of a third-party tool because the common Go migration tools need a `database/sql` driver, and the Data API has no first-party one. |
| Crypto | `golang.org/x/crypto/argon2` (Argon2id), standard-library HMAC/RSA, AWS KMS for JWT signing and secret encryption | Matches [Non-Functional Requirements → Security](https://adron.github.io/substratalapps.com/non-functional-requirements/) exactly, with no native add-ons. |
| Crypto | `golang.org/x/crypto/argon2` (Argon2id), standard-library HMAC/RSA, AWS KMS for JWT signing and secret encryption | Matches [Non-Functional Requirements → Security](https://compositecode.github.io/substratalapps.com/non-functional-requirements/) exactly, with no native add-ons. |
| AWS access | AWS SDK for Go v2 (Data API, KMS, SQS, Secrets Manager, S3, SES) | |
| Billing | `stripe-go`, Stripe's official Go library | |
| MCP server | Go, implementing the buffered-JSON subset of MCP Streamable HTTP directly (`internal/mcp`). Tools are generated from `openapi.yaml` by `cmd/mcpgen`. | The spec requires a stateless, signed `Mcp-Session-Id` and one invocation per call; the official SDK keeps sessions in memory, which doesn't fit a stateless Lambda. It only proxies HTTPS calls to `/v1`. |
Expand Down Expand Up @@ -118,10 +118,10 @@ Every push to `dev` (and to `feature/*`), and every pull request into `main`, ru

1. **Format and lint.** `gofmt` check, `go vet`, `golangci-lint`. `terraform fmt -check`, `terraform validate`, `tflint`.
2. **Generated code is current.** Re-runs `oapi-codegen` against `docs/openapi.yaml` and fails if the result differs from what's committed in `internal/api/gen/`. Editing the spec without regenerating, or editing generated code by hand, can't reach `main`.
3. **Unit tests**, including exhaustive tests of the Access Control resolution logic (every combination of entitlement status × platform role × app role), per [Testing strategy](https://adron.github.io/substratalapps.com/non-functional-requirements/#testing-strategy).
3. **Unit tests**, including exhaustive tests of the Access Control resolution logic (every combination of entitlement status × platform role × app role), per [Testing strategy](https://compositecode.github.io/substratalapps.com/non-functional-requirements/#testing-strategy).
4. **Integration tests** against real Postgres 16 and LocalStack running as GitHub Actions service containers, with every migration applied from zero. These run the pgx backend, including the Row-Level Security policies, which a mocked data layer would let pass while broken.
5. **Contract tests.** The devserver starts, and every response is validated against `docs/openapi.yaml`.
6. **Quickstart test.** The [Quickstart](https://adron.github.io/substratalapps.com/quickstart/) walkthrough runs as a script against the devserver. It's the Phase 1 acceptance test from [PLAN.md](PLAN.md#phase-1--mvp), so it runs on every change, not just at milestones.
6. **Quickstart test.** The [Quickstart](https://compositecode.github.io/substratalapps.com/quickstart/) walkthrough runs as a script against the devserver. It's the Phase 1 acceptance test from [PLAN.md](PLAN.md#phase-1--mvp), so it runs on every change, not just at milestones.
7. **Build.** Every `cmd/*` function compiles for `linux/arm64` (`CGO_ENABLED=0`, `-tags lambda.norpc`) and is zipped as `bootstrap`, which proves the release artifacts can be built.
8. **Docs build.** `jekyll build` of `docs/`, so a broken docs page is caught on `dev`, not on the release.

Expand Down Expand Up @@ -212,7 +212,7 @@ Everything below runs on a laptop with no AWS account. The goal is that `make up
### First run

```bash
git clone git@github.com:Adron/substratalapps.com.git
git clone git@github.com:CompositeCode/substratalapps.com.git
cd substratalapps.com
git checkout dev # all work happens on dev
cp .env.example .env # local defaults, safe to use as-is
Expand All @@ -222,7 +222,7 @@ make seed # a local superadmin (ADMIN_EMAIL / ADMIN_PASSW
make run # devserver on http://localhost:8080
```

Then `make quickstart` runs the [Quickstart](https://adron.github.io/substratalapps.com/quickstart/) against it (it creates its own TimeTrack Application), and `curl` works as written with `SUBSTRATAL_API=http://localhost:8080/v1`. Integration tests need only `make up`: each test package creates, migrates, and drops its own database.
Then `make quickstart` runs the [Quickstart](https://compositecode.github.io/substratalapps.com/quickstart/) against it (it creates its own TimeTrack Application), and `curl` works as written with `SUBSTRATAL_API=http://localhost:8080/v1`. Integration tests need only `make up`: each test package creates, migrates, and drops its own database.

### What runs where

Expand All @@ -247,7 +247,7 @@ Configuration is entirely environment variables, documented in `.env.example`. `
- Every database call goes through one small interface in `internal/db` (`Tx`, `Query`, `Exec`). Handlers never import `pgx` or the Data API client.
- SQL is written once, with named parameters (`:user_id`). The pgx driver rewrites them to `$1` positional form; the Data API driver passes them as `SqlParameter`s. Parameters are only ever scalars; arrays and JSON travel as text with explicit casts (`::text[]`, `::jsonb`), because the Data API has no array parameters.
- Every query that returns rows selects **one JSON column built by Postgres** (`to_jsonb(t)`, `jsonb_build_object(…)`), so both drivers return plain JSON text and the Data API's per-type field mapping never comes into it.
- Every request is one transaction. Its first statement sets `app.current_test_mode`, which the test/live Row-Level Security policy reads, so isolation is enforced by the database on both backends. Audit Events and webhook outbox rows written with a change commit with it ([Transaction boundaries](https://adron.github.io/substratalapps.com/non-functional-requirements/#transaction-boundaries)).
- Every request is one transaction. Its first statement sets `app.current_test_mode`, which the test/live Row-Level Security policy reads, so isolation is enforced by the database on both backends. Audit Events and webhook outbox rows written with a change commit with it ([Transaction boundaries](https://compositecode.github.io/substratalapps.com/non-functional-requirements/#transaction-boundaries)).
- The Data API driver's parameter mapping and error translation have their own unit tests. Integration tests cover the pgx path; the production smoke test checks the Data API path end to end.

### Day-to-day commands
Expand Down Expand Up @@ -296,7 +296,7 @@ The docs site is versioned automatically. Every push to `main` that touches `doc
2. Builds that version as a frozen, self-contained snapshot (its own nav, search, and links, labelled archived, linking back to latest). It commits the snapshot to the **`docs-versions`** branch along with `manifest.yml`, the version history. CI never commits to `main`.
3. Builds the live site, adds every archived snapshot under `/versions/<version>/`, and deploys it to GitHub Pages.

The current version shows in the top right of every page and links to the [Versions](https://adron.github.io/substratalapps.com/versions/) page. A local `jekyll serve` has no version data and shows `local` instead. To reproduce the full versioned build locally, check out `docs-versions` into a folder and run:
The current version shows in the top right of every page and links to the [Versions](https://compositecode.github.io/substratalapps.com/versions/) page. A local `jekyll serve` has no version data and shows `local` instead. To reproduce the full versioned build locally, check out `docs-versions` into a folder and run:

```bash
git worktree add ../docs-versions docs-versions
Expand Down
2 changes: 1 addition & 1 deletion cmd/admin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"os"
"strings"

"github.com/Adron/substratalapps.com/internal/platform/wire"
"github.com/CompositeCode/substratalapps.com/internal/platform/wire"
)

func main() {
Expand Down
4 changes: 2 additions & 2 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"context"
"log"

"github.com/Adron/substratalapps.com/internal/platform/lambdahttp"
"github.com/Adron/substratalapps.com/internal/platform/wire"
"github.com/CompositeCode/substratalapps.com/internal/platform/lambdahttp"
"github.com/CompositeCode/substratalapps.com/internal/platform/wire"
)

func main() {
Expand Down
10 changes: 5 additions & 5 deletions cmd/devserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import (
"syscall"
"time"

"github.com/Adron/substratalapps.com/internal/billing"
"github.com/Adron/substratalapps.com/internal/jobs"
"github.com/Adron/substratalapps.com/internal/mcp"
"github.com/Adron/substratalapps.com/internal/platform/wire"
"github.com/Adron/substratalapps.com/internal/webhooks"
"github.com/CompositeCode/substratalapps.com/internal/billing"
"github.com/CompositeCode/substratalapps.com/internal/jobs"
"github.com/CompositeCode/substratalapps.com/internal/mcp"
"github.com/CompositeCode/substratalapps.com/internal/platform/wire"
"github.com/CompositeCode/substratalapps.com/internal/webhooks"
)

func main() {
Expand Down
4 changes: 2 additions & 2 deletions cmd/jobs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (

"github.com/aws/aws-lambda-go/lambda"

"github.com/Adron/substratalapps.com/internal/jobs"
"github.com/Adron/substratalapps.com/internal/platform/wire"
"github.com/CompositeCode/substratalapps.com/internal/jobs"
"github.com/CompositeCode/substratalapps.com/internal/platform/wire"
)

type event struct {
Expand Down
4 changes: 2 additions & 2 deletions cmd/mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"os"
"time"

"github.com/Adron/substratalapps.com/internal/mcp"
"github.com/Adron/substratalapps.com/internal/platform/lambdahttp"
"github.com/CompositeCode/substratalapps.com/internal/mcp"
"github.com/CompositeCode/substratalapps.com/internal/platform/lambdahttp"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/mcpgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func inline(v any, depth int) any {
}

func resources() []Resource {
base := "https://adron.github.io/substratalapps.com"
base := "https://compositecode.github.io/substratalapps.com"
llms, err := os.ReadFile("docs/llms.txt")
if err != nil {
log.Fatal(err)
Expand Down
8 changes: 4 additions & 4 deletions cmd/migrate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import (
"path/filepath"
"time"

"github.com/Adron/substratalapps.com/internal/db/migrate"
"github.com/Adron/substratalapps.com/internal/platform/config"
"github.com/Adron/substratalapps.com/internal/platform/wire"
"github.com/Adron/substratalapps.com/migrations"
"github.com/CompositeCode/substratalapps.com/internal/db/migrate"
"github.com/CompositeCode/substratalapps.com/internal/platform/config"
"github.com/CompositeCode/substratalapps.com/internal/platform/wire"
"github.com/CompositeCode/substratalapps.com/migrations"
)

func main() {
Expand Down
6 changes: 3 additions & 3 deletions cmd/stripe-webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"context"
"log"

"github.com/Adron/substratalapps.com/internal/billing"
"github.com/Adron/substratalapps.com/internal/platform/lambdahttp"
"github.com/Adron/substratalapps.com/internal/platform/wire"
"github.com/CompositeCode/substratalapps.com/internal/billing"
"github.com/CompositeCode/substratalapps.com/internal/platform/lambdahttp"
"github.com/CompositeCode/substratalapps.com/internal/platform/wire"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/webhook-worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"

"github.com/Adron/substratalapps.com/internal/platform/wire"
"github.com/CompositeCode/substratalapps.com/internal/platform/wire"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: >-
# Project page on the default github.io URL. If a custom domain is added
# later (see docs/README or repo Settings > Pages), update both of these
# and add a CNAME file.
url: "https://adron.github.io"
url: "https://compositecode.github.io"
baseurl: "/substratalapps.com"

permalink: pretty
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference/billing.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The Application owner's **platform subscription**: what Substratal charges the d
- **Payment methods, invoices, and cancellation** are handled in the Stripe Customer Portal, again through a URL from this API.
- **Enterprise** is sales-led. Staff create the subscription in Stripe, usually from a Quote, and it syncs in.

Stripe tells this API about every change through its webhook. This API never takes card data, and the Tenant's `plan` and `subscription_status` only change in response to Stripe events. The handler is documented in [root `DEPLOYMENT.md` → Stripe Billing](https://github.com/Adron/substratalapps.com/blob/main/DEPLOYMENT.md#stripe-billing).
Stripe tells this API about every change through its webhook. This API never takes card data, and the Tenant's `plan` and `subscription_status` only change in response to Stripe events. The handler is documented in [root `DEPLOYMENT.md` → Stripe Billing](https://github.com/CompositeCode/substratalapps.com/blob/main/DEPLOYMENT.md#stripe-billing).

## Endpoints

Expand Down
Loading
Loading