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
8 changes: 7 additions & 1 deletion internal/core/webhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,13 @@ func TestPagination(t *testing.T) {
}

func TestRateLimits(t *testing.T) {
e := testenv.NewWith(t, testenv.Options{RateLimits: true})
// Limits count per clock minute, so the API's clock starts at the top
// of one: all 11 logins land in the same window however slow the run.
// Its own database keeps the counters from leaking between runs.
start := time.Now()
minute := start.Truncate(time.Minute)
e := testenv.NewWith(t, testenv.Options{RateLimits: true, OwnDatabase: true,
Now: func() time.Time { return minute.Add(time.Since(start)) }})
var last *testenv.Resp
for i := 0; i < 11; i++ {
last = e.Post("/auth/login", "", map[string]any{"email": "rl+" + testenv.Unique() + "@example.com", "password": "whatever passphrase"})
Expand Down
5 changes: 4 additions & 1 deletion internal/testenv/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ type Options struct {
// data would leak into others sharing the package database (a free
// Application, say, is granted to every User created afterwards).
OwnDatabase bool
// Now replaces the API's clock (time.Now). Rate-limit windows, token
// lifetimes, and every other time check read it.
Now func() time.Time
}

// New starts an API for one test, with a fresh superadmin.
Expand Down Expand Up @@ -226,7 +229,7 @@ func NewWith(t testing.TB, opts Options) *Env {
s := core.New(core.Deps{DB: d, Signer: signer(), Encrypter: enc, Email: mail, Push: recorder,
Cursors: httpx.Cursors{Key: []byte("test-cursor-key")}, Log: log,
Issuer: "https://api.substratalapps.com", PublicBaseURL: "http://localhost", AuthorizeURL: "https://auth.substratalapps.com/authorize",
DashboardURL: "https://dashboard.test", DisableRateLimits: !opts.RateLimits, Billing: opts.Billing})
DashboardURL: "https://dashboard.test", DisableRateLimits: !opts.RateLimits, Billing: opts.Billing, Now: opts.Now})
srv := httptest.NewServer(s.Handler())
e := &Env{T: t, Server: srv, Core: s, DB: d, Mail: mail, Deliverer: deliverer, Push: recorder, Signer: signer(), Validate: true,
validator: specValidator(t)}
Expand Down
7 changes: 4 additions & 3 deletions migrations/0008_scim_tokens.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
-- One SCIM bearer token per Organization, for its identity provider. Only
-- the SHA-256 of the secret is stored, like API Keys.
--
-- Numbered after 0005_test_user_expiry, 0006_self_registered_apps, and
-- 0007_audit_exports, which ship first; the action list below keeps their
-- user.expired and audit.exported.
-- This reached production before the test-user-expiry, self-registered
-- apps, and audit-export migrations, which were renumbered 0009–0011 to
-- follow it. The action list below already includes their user.expired
-- and audit.exported.

create table scim_tokens (
id text primary key,
Expand Down
2 changes: 1 addition & 1 deletion plan-remaining.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ As of 2026-10-20, every operation in `openapi.yaml` is implemented, including su

The phase exit criteria have only been checked against the local devserver. Once production is up, check them there:

- **Phase 0:** `GET /health` returns `200` through the real Lambda and API Gateway path. (It returned `200` at the old `/v1/health` on 2026-10-20. Check `/health` again after the first deploy without the `/v1` prefix.)
- **Phase 0:** `GET /health` returns `200` through the real Lambda and API Gateway path. (Done: it returned `200` at `/health` on 2026-10-22, after the move to header versioning; the old `/v1/health` now returns `404`.)
- **Phase 1:** the [Quickstart](https://compositecode.github.io/substratalapps.com/quickstart/) runs end to end against production: create a user, grant an app, check access, turn it off. Run `scripts/quickstart.sh` with `SUBSTRATAL_API=https://api.substratalapps.com` and a test-mode key.
- **Phase 2:** a third-party-shaped app signs a user in through the embedded flow, receives `access.revoked` and kills the session, calls introspection, has its `settings_schema` validated and typed on write, and receives a push sent through its app key. A test-mode User and a test-mode Organization subscribe to Standard through Checkout (needs a Stripe test-mode key in a non-production environment, or temporarily in production), gain the standard Applications, and lapse back to Free.

Expand Down
18 changes: 9 additions & 9 deletions remaining_manual_steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

One-time setup that has to be done by hand, in a console or from a laptop, to take the Platform API live. Code that still has to be built is in [plan-remaining.md](plan-remaining.md); how the deployment works is in [infra/README.md](infra/README.md).

Already done (2026-10-20): the AWS account (`substratal-prod`, `196936048600`) and its management account; CLI access with `aws login`; DNS moved from Vercel to Route 53; the Organization SCP exception for `substratal-prod`; the bootstrap Terraform (budgets, anomaly detection, state and artifact buckets, GitHub OIDC roles); GitHub variables, the `production` environment limited to `main`, and the `main` ruleset; and a first partial deploy (Aurora, all five Lambdas, API Gateway, the certificate, SES domain). `https://api.substratalapps.com/v1/health` returns `200`.
Already done (2026-10-20): the AWS account (`substratal-prod`, `196936048600`) and its management account; CLI access with `aws login`; DNS moved from Vercel to Route 53; the Organization SCP exception for `substratal-prod`; the bootstrap Terraform (budgets, anomaly detection, state and artifact buckets, GitHub OIDC roles); GitHub variables, the `production` environment limited to `main`, and the `main` ruleset; and a first partial deploy (Aurora, all five Lambdas, API Gateway, the certificate, SES domain). `https://api.substratalapps.com/health` returns `200` (the API moved from `/v1/…` paths to the `Substratal-Version` header on 2026-10-21; see [Conventions → Versioning](https://compositecode.github.io/substratalapps.com/api-reference/conventions/#versioning)).

Work through these in order. The first four make the API usable; the rest make it ready for real customers.

Expand Down Expand Up @@ -38,7 +38,7 @@ After the quota shows **1000** (`aws lambda get-account-settings --profile subst
- **Easiest:** GitHub → **Actions → Deploy → Run workflow**, branch `main`, **Run workflow**.
- Or from a terminal: `gh workflow run deploy.yml -R CompositeCode/substratalapps.com --ref main`.

The run builds the Lambdas, applies Terraform (it picks up where the first run stopped), runs the migrations on the new cluster, makes sure the protected superadmin exists (`SUPERADMIN_EMAIL`), and smoke-tests `/v1/health` and JWKS. It takes about 5–10 minutes. Every later merge into `main` deploys the same way, automatically.
The run builds the Lambdas, applies Terraform (it picks up where the first run stopped), runs the migrations on the new cluster, makes sure the protected superadmin exists (`SUPERADMIN_EMAIL`), and smoke-tests `/health` and JWKS. It takes about 5–10 minutes. Every later merge into `main` deploys the same way, automatically.

## 3. Set the superadmin's password

Expand All @@ -53,7 +53,7 @@ export DATABASE_BACKEND=dataapi \
go run ./cmd/admin bootstrap -email adronhall@proton.me # prompts for the password (12+ characters)
```

Then sign in: `curl -s https://api.substratalapps.com/v1/auth/login -H 'content-type: application/json' -d '{"email":"adronhall@proton.me","password":"…"}'`. Turn on MFA right away ([Auth → MFA](https://compositecode.github.io/substratalapps.com/api-reference/auth/#mfa-totp)).
Then sign in: `curl -s https://api.substratalapps.com/auth/login -H 'Substratal-Version: 1' -H 'content-type: application/json' -d '{"email":"adronhall@proton.me","password":"…"}'`. Turn on MFA right away ([Auth → MFA](https://compositecode.github.io/substratalapps.com/api-reference/auth/#mfa-totp)).

## 4. Confirm the alert emails

Expand Down Expand Up @@ -86,16 +86,16 @@ The billing code (Free and Standard at $6 per user per month) is built and deplo

Push notifications are merged (PR #12) and ship with the next successful deploy, which also applies migration `0003_push.sql`. Migrations `0003_push` and `0004_protected_superadmin` touch different tables, so it doesn't matter that `0004` reached `main` first.

For each Application that sends pushes, its owner uploads credentials with `POST /v1/applications/{id}/push-credentials` (and rotates them with `PATCH …/push-credentials/{provider}`): an APNs key (`.p8`, key id, team id, bundle id), an FCM service account, or Web Push VAPID keys ([Push Notifications → Push credentials](https://compositecode.github.io/substratalapps.com/api-reference/notifications/#push-credentials)).
For each Application that sends pushes, its owner uploads credentials with `POST /applications/{id}/push-credentials` (and rotates them with `PATCH …/push-credentials/{provider}`): an APNs key (`.p8`, key id, team id, bundle id), an FCM service account, or Web Push VAPID keys ([Push Notifications → Push credentials](https://compositecode.github.io/substratalapps.com/api-reference/notifications/#push-credentials)).

## 8. Register Substratal's Applications

For each of Substratal's apps, with the superadmin's token ([Applications → Owner onboarding](https://compositecode.github.io/substratalapps.com/api-reference/applications/#owner-onboarding-end-to-end)):
For each of Substratal's apps, with the superadmin's token and `Substratal-Version: 1` on every request ([Applications → Owner onboarding](https://compositecode.github.io/substratalapps.com/api-reference/applications/#owner-onboarding-end-to-end)):

1. `POST /v1/applications` with its slug, name, `launch_url`, `redirect_uris`, `permissions`, `available_app_roles`, and **`access_tier`** (`free` or `standard`, which decides who gets it under [Pricing](https://compositecode.github.io/substratalapps.com/pricing/)).
2. Give each seeded Role its permissions (`PATCH /v1/roles/role_<slug>_<name>`).
3. Create the app backend's key (`POST /v1/api-keys`, scope the app, `entitlements.manage` and `users.list`), plus a test-mode key.
4. Subscribe the backend to `access.revoked` and `role.removed` (`POST /v1/webhooks`): the required revocation integration.
1. `POST /applications` with its slug, name, `launch_url`, `redirect_uris`, `permissions`, `available_app_roles`, and **`access_tier`** (`free` or `standard`, which decides who gets it under [Pricing](https://compositecode.github.io/substratalapps.com/pricing/)).
2. Give each seeded Role its permissions (`PATCH /roles/role_<slug>_<name>`).
3. Create the app backend's key (`POST /api-keys`, scope the app, `entitlements.manage` and `users.list`), plus a test-mode key.
4. Subscribe the backend to `access.revoked` and `role.removed` (`POST /webhooks`): the required revocation integration.

## 9. Fix the pull-request plan job

Expand Down
Loading