Ga naar inhoud

Taak 2.5 — Provisioning van tenant-sites (plan)

A ledger row in provisioning becomes a Postgres database, a Coolify app, runtime env, and a triggered deploy. The operator watches that on the tenant detail page. The visitor still has no preview hostname. That wait is task 2.6. The program keeps ciphertext out of every browser JSON body and retries to the same end state. Planned PRs in order are T25-1, T25-2, T25-3, T25-4, and T25-5.

One box is one unit of work. Every box names the evidence that checks it. A nested box is a sub-step of the box above it. Check a box only when its evidence exists, a file, a log line, a screenshot, a test run, or a SHA. The body is a how-to. The appendices explain and record.

The program runs pstack/skills/poteto-mode/playbooks/autopilot-stack.md. The operator lands T25-1 through T25-5 bottom-up. Owners stop at merge-ready.

Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • State the protocol and this plan to the operator, then stop. Start execution only on her explicit go.
  • On her go, arm a /goal with this exact text. “Taakplan 2.5, T25-1 T25-2 T25-3 T25-4 T25-5, a PR is verified only when its unit, live, and perf boxes are all checked, the operator lands the stack, done when a provisioning row without Coolify config still gets a database and migrations, and with Coolify config also gets an app uuid, env, and a deployments row.”
  • Read these from trunk at program start. Re-read them at every tick.
    • git show origin/main:pstack/skills/poteto-mode/playbooks/autopilot-stack.md
    • git show origin/main:pstack/skills/swarm/SKILL.md
    • git show origin/main:pstack/skills/control-ui/SKILL.md
    • git show origin/main:pstack/skills/control-cli/SKILL.md
    • git show origin/main:pstack/skills/poteto-mode/playbooks/opening-a-pr.md
    • git show origin/main:pstack/skills/principle-make-operations-idempotent/SKILL.md
    • git show origin/main:pstack/skills/principle-model-the-domain/SKILL.md
  • Arm the 30-minute audit tick. In a local session, a real terminal /loop. In a cloud root, a cloud-sleeper wake chain. Never leave the cadence to memory.
  • Use this tick prompt, verbatim. “Re-read the execution playbook from trunk and the armed /goal. Audit the operation against both and fix drift in this tick. Probe every active lane and judge progress by side effects only. Stand down a stuck lane and dispatch its replacement now. Then send the operator a status message, whether or not anything changed, with the queue table of PR, owner, state, and head SHA, the verdicts since the last tick, what merged, open operator gates, and blockers.”
  • On the operator’s hold or stand-down, send every owner a zero-writes order at once.
  • Spawn one owner per PR with the full lifecycle the execution playbook names.
  • Follow this dependency graph. Start dependent work only after its parent merges, or base it on the parent branch when the execution playbook stacks.
    • T25-1 is first. It branches from main.
    • T25-2 after T25-1.
    • T25-3 after T25-2.
    • T25-4 after T25-3.
    • T25-5 after T25-4.
  • Hold the file boundaries. T25-1 touches only packages/ops/**, packages/db/scripts/**, packages/db/src/migrate-tenant.ts, and scripts/provision-tenant-db.sh. T25-2 touches master schema, contract docs, and webhook verify. T25-3 touches only the Coolify client and its tests. T25-4 touches Nitro provision routes, provision-tenant.ts, and provision-tenant-migrate.ts. T25-5 touches apps/master-dashboard/app/pages/tenants/** and copy in Taken.
  • Hold the review gate. T25-5 changes an interaction. It waits for the operator’s review in chat with screenshots and a video before merge.
  • Resolve the forge once. Default to gh; if command -v origin succeeds and Origin can resolve the repository, use origin pr for every PR operation. Record any fallback to gh. Never require gt.
  • Open the PR ready, never draft, with origin pr create --status open --base <base-branch> or gh pr create --base <base-branch> according to the resolved forge. A stack child targets its parent branch.
  • Run the repo’s lint and typecheck once before the PR-facing push. Push with hooks on.
  • Run /deslop before each commit and /no-comments before review.
  • Triage every Bugbot and security-reviewer comment per ../references/bugbot-triage.md.
  • Rebase onto current trunk before babysit and again before the merge-ready report.
  • At the merge-ready head SHA, run the swarm per pstack/skills/swarm/SKILL.md. One gates lane. The ten live lanes from the PR’s Verify, live block. The perf lane from its Verify, perf block. One audit lane that reads the diff and the receipts and distrusts the PR body.
  • Clean only when every lane is PASS. Findings go back to the owner. A new head gets a fresh swarm and a fresh verdict.
  • The root appends the PR to the base-branch stack. The operator lands it bottom-up. Compare git patch-id after rebase per playbooks/shipping.md.

Each live lane runs on its own cloud VM at the PR head. Drive through control-ui or control-cli from cursor-team-kit.

  • git fetch origin <head-branch> && git checkout <head SHA>.
  • Start Compose Postgres on port 5432 if it is down. For T25-4 and T25-5 start pnpm --filter master-dashboard run dev and wait until GET http://localhost:3000/login returns 200. For T25-1 run pnpm --filter @platform/ops run typecheck before the probe.
  • Deliver input only through the control skill’s commands. Name the read-only diagnostics.
  • Save every screenshot to /tmp/swarm-<pr-id>/worker-<n>/<slug>.png and return the paths with the report.

Apply tenant database create and migrate (T25-1)

Section titled “Apply tenant database create and migrate (T25-1)”

Depends on. None.

Files.

  • Create packages/ops/src/provision-tenant-db.ts.
  • Create packages/ops/src/provision-tenant-db.test.ts.
  • Edit packages/ops/src/index.ts.
  • Edit packages/ops/package.json.
  • Create packages/db/src/migrate-tenant.ts.
  • Edit packages/db/scripts/migrate.mjs.
  • Edit scripts/provision-tenant-db.sh.
  • Edit Operations.

Build.

  • Add provisionTenantDatabase({ adminUrl, tenantId, password }) in packages/ops/src/provision-tenant-db.ts. The SQL matches scripts/provision-tenant-db.sh (CREATE ROLE and CREATE DATABASE only when missing, then ALTER SCHEMA public OWNER and REVOKE ALL ON DATABASE … FROM PUBLIC). If the role already exists, leave its password unchanged. A retry must reuse the stored DSN, not mint a second password.
  • Return a percent-encoded DATABASE_URL. Generated passwords contain +, /, and =. Encode the userinfo or later getDb fails with an auth error that looks unrelated.
  • Add applyTenantMigrations({ databaseUrl, migrationsFolder }) in packages/db/src/migrate-tenant.ts. Point migrate.mjs at that function so Coolify and the library share one path.
  • Make scripts/provision-tenant-db.sh call the ops CLI. Do not keep a second SQL copy.

You see.

  • DATABASE_URL=… pnpm --filter @platform/db run migrate still prints Migrations applied successfully on tenant_demo.
  • A second migrate on the same database prints the same line and leaves drizzle.__drizzle_migrations at count 3.
  • provisionTenantDatabase against a role with CREATEROLE creates tenant_<slug> twice with exit 0. This VM’s platform role cannot create roles. Record that skip with the SQL error text if the lane has no superuser.

Verify, unit. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • packages/ops/src/provision-tenant-db.test.ts covers missing-role create, existing-role skip without ALTER PASSWORD, percent-encoding of + in the DSN, and invalid slug. Run pnpm --filter @platform/ops exec node --test src/provision-tenant-db.test.ts.

Verify, live. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on grok-4.6-fast-xhigh at the PR head, per the boot recipe.

  • Lane 1. Regression lane against trunk. Run DATABASE_URL=postgresql://platform:platform@localhost:5432/tenant_demo pnpm --filter @platform/db run migrate at trunk and head. If trunk lacks the feature, record that and gate the same migrate success plus the log line Migrations applied successfully. Save t25-1-migrate-regression.png. Pass when both sides print that line and to_regclass('public.site') is site.
  • Lane 2. Second migrate on tenant_demo. Save t25-1-migrate-idempotent.png. Pass when the command exits 0 and the migrations count stays 3.
  • Lane 3. Invalid slug 1bad through provisionTenantDatabase. Save t25-1-bad-slug.png. Pass when the function throws the contract slug message and no CREATE runs.
  • Lane 4. Shell wrapper ./scripts/provision-tenant-db.sh with PGDATABASE=postgres. Save t25-1-shell.png. Pass when the wrapper reaches the TS entry and does not embed a second CREATE ROLE string.
  • Lane 5. pnpm --filter @platform/ops run typecheck. Save t25-1-ops-typecheck.png. Pass when exit is 0.
  • Lane 6. pnpm --filter @platform/db run typecheck. Save t25-1-db-typecheck.png. Pass when exit is 0.
  • Lane 7. pnpm turbo run typecheck. Save t25-1-turbo-typecheck.png. Pass when the pipeline is green.
  • Lane 8. Read packages/db/scripts/migrate.mjs. Save t25-1-migrate-mjs.png. Pass when the file calls applyTenantMigrations and does not inline migrate() from drizzle.
  • Lane 9. Attempt provisionTenantDatabase as platform. Save t25-1-createrole-denied.png. Pass when the error is permission denied to create role or a created tenant_* database exists. Do not leave a stray database. Drop it with the admin user if one was created.
  • Lane 10. Confirm tenant_demo still serves the demo site row after the probes. Save t25-1-demo-intact.png. Pass when SELECT name FROM site still returns the demo name.

Verify, perf. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • Metric. Wall time of pnpm --filter @platform/db run migrate on already-migrated tenant_demo at trunk and head. The diff-added work is the extra function call in migrate.mjs. The user waits for the success log line.
  • Probe. /usr/bin/time -f %e around that migrate, trunk then head, interleaved once each.
  • Baseline. Record the trunk seconds first.
  • Rule. Head must stay under 5 seconds absolute. Head must not exceed trunk by more than 1.0 second. Fail if either bound breaks.

Review gate. None. T25-1 is not review-gated.

Merge.

  • Root’s clean verdict at the exact head SHA.
  • Bugbot triage done.
  • Rebased onto current trunk after the verdict, patch-id unchanged.
  • The root appends T25-1 to the stack. The operator squash-merges it onto main.

Store per-tenant secrets encrypted (T25-2)

Section titled “Store per-tenant secrets encrypted (T25-2)”

Depends on. T25-1.

Files.

  • Edit packages/db/src/master/schema.ts.
  • Create the generated files under packages/db/drizzle-master/ from pnpm --filter @platform/db run generate:master.
  • Edit apps/master-dashboard/server/utils/secrets.ts only if the format must name more than database_url_encrypted in the comment.
  • Edit apps/master-dashboard/server/api/webhooks/site.post.ts.
  • Edit apps/master-dashboard/server/utils/receive-site-webhook.ts.
  • Edit apps/master-dashboard/server/utils/webhooks.test.ts.
  • Edit apps/master-dashboard/server/utils/tenant-store.ts.
  • Edit Datamodel.
  • Edit Contract.

Build.

  • Add nullable text columns webhook_secret_encrypted, platform_api_key_encrypted, and better_auth_secret_encrypted on tenants. Keep encryptSecret and decryptSecret as the only codec.
  • Resolve the HMAC secret in site.post.ts by X-Platform-Tenant slug. Use the decrypted tenant secret when the column is set. Fall back to NUXT_PLATFORM_WEBHOOK_SECRET when the column is null so existing sites keep working.
  • Keep platformTenantSchema without those columns. getTenantListItem must not select them.

You see.

  • pnpm --filter @platform/db exec drizzle-kit check --config drizzle.master.config.ts is green.
  • A webhook with a per-tenant secret returns 200. The same body with only the shared env secret returns 401 once the column is set.
  • GET /api/tenants JSON has no encrypted substring.

Verify, unit. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • apps/master-dashboard/server/utils/webhooks.test.ts plus a receive helper test for tenant-secret lookup and shared-secret fallback. Run pnpm --filter master-dashboard exec node --test server/utils/webhooks.test.ts server/utils/receive-site-webhook.integration.ts.

Verify, live. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on grok-4.6-fast-xhigh at the PR head, per the boot recipe.

  • Lane 1. Regression lane against trunk. POST a valid HMAC to /api/webhooks/site with NUXT_PLATFORM_WEBHOOK_SECRET at trunk and head. If trunk lacks the feature, record that and gate HTTP 200 plus one activity row. Save t25-2-webhook-shared.png. Pass when status is 200 on both and the payload type is unchanged.
  • Lane 2. Insert ciphertext with encryptSecret into webhook_secret_encrypted for a test slug. POST with that plaintext. Save t25-2-webhook-tenant.png. Pass when status is 200.
  • Lane 3. Same row, POST with only the shared env secret. Save t25-2-webhook-tenant-reject.png. Pass when status is 401.
  • Lane 4. GET /api/tenants as a session. Save t25-2-list-no-secrets.png. Pass when the body has no databaseUrlEncrypted, webhook_secret, or encrypted.
  • Lane 5. GET /api/tenants/:slug as a session. Save t25-2-detail-no-secrets.png. Pass when the same strings are absent.
  • Lane 6. Duplicate eventId still 200 without a second activity row. Save t25-2-idempotent.png. Pass when webhook_receipts has one row for that id.
  • Lane 7. pnpm --filter @platform/db exec drizzle-kit check --config drizzle.master.config.ts. Save t25-2-drizzle-check.png. Pass when exit is 0.
  • Lane 8. pnpm turbo run typecheck. Save t25-2-typecheck.png. Pass when exit is 0.
  • Lane 9. Tamper the ciphertext tag then POST. Save t25-2-tamper.png. Pass when decrypt throws and the handler returns 500 or 401 without writing activity.
  • Lane 10. Null secret column still accepts the shared env secret. Save t25-2-fallback.png. Pass when status is 200.

Verify, perf. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • Metric. Time from webhook POST to HTTP status at trunk (shared secret) and head (shared secret, then tenant secret). The diff-added work is one decrypt plus one extra SELECT. The user waits for the 200.
  • Probe. curl -o /dev/null -s -w %{time_total} on /api/webhooks/site, trunk then head, interleaved.
  • Baseline. Record the trunk time_total first.
  • Rule. Head shared-secret path must stay under 200 ms. Tenant-secret path must stay under 250 ms. Fail if either bound breaks.

Review gate. None. T25-2 is not review-gated.

Merge.

  • Root’s clean verdict at the exact head SHA.
  • Bugbot triage done.
  • Rebased onto current trunk after the verdict, patch-id unchanged.
  • The root appends T25-2. The operator squash-merges it.

Depends on. T25-2.

Files.

  • Create apps/master-dashboard/server/utils/coolify.ts.
  • Create apps/master-dashboard/server/utils/coolify.test.ts.
  • Edit apps/master-dashboard/nuxt.config.ts.
  • Edit apps/master-dashboard/.env.example.

Build.

  • Add a typed client that talks to /api/v1 with a bearer token. Methods are createDockerfileGitApp, bulkSetEnvs, startApplication, and getApplication. Parse JSON at the boundary. Trust the return type inside.
  • Create the app with git plus build_pack=dockerfile, ports_exposes=4321, base_directory=/, health check path /robots.txt. Do not use POST /applications/dockerfile with inline file content. That endpoint stores Dockerfile text, not this repo.
  • When coolifyToken is empty, the client throws a named CoolifyDisabledError. The orchestrator in T25-4 catches that.

You see.

  • node --test on coolify.test.ts passes with a fake fetch.
  • .env.example lists NUXT_COOLIFY_BASE_URL, NUXT_COOLIFY_TOKEN, NUXT_COOLIFY_PROJECT_UUID, NUXT_COOLIFY_SERVER_UUID, NUXT_COOLIFY_ENVIRONMENT_UUID, NUXT_COOLIFY_PRIVATE_KEY_UUID, NUXT_COOLIFY_GIT_REPOSITORY, and NUXT_COOLIFY_GIT_BRANCH.

Verify, unit. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • apps/master-dashboard/server/utils/coolify.test.ts stubs fetch for create, bulk env, start, 401, and disabled token. Run pnpm --filter master-dashboard exec node --test server/utils/coolify.test.ts.

Verify, live. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on grok-4.6-fast-xhigh at the PR head, per the boot recipe.

  • Lane 1. Regression lane against trunk. pnpm --filter master-dashboard exec node --test server/utils/webhooks.test.ts at trunk and head. If trunk lacks the feature, record that and gate the existing webhook tests plus the new file existing on head. Save t25-3-webhook-regression.png. Pass when webhook tests stay green.
  • Lane 2. Create-app mock returns a uuid. Save t25-3-create-app.png. Pass when the client returns that uuid.
  • Lane 3. Bulk env mock sends runtime flags for DATABASE_URL and build flags for S3_PUBLIC_URL. Save t25-3-bulk-env.png. Pass when the request body marks those keys correctly.
  • Lane 4. Start mock hits POST /applications/{uuid}/start. Save t25-3-start.png. Pass when the method is POST.
  • Lane 5. Empty token throws CoolifyDisabledError before fetch. Save t25-3-disabled.png. Pass when fetch call count is 0.
  • Lane 6. HTTP 401 from Coolify becomes a typed error. Save t25-3-401.png. Pass when the message includes 401.
  • Lane 7. nuxt typecheck. Save t25-3-typecheck.png. Pass when exit is 0.
  • Lane 8. Confirm no call to a real Coolify host in tests. Save t25-3-no-network.png. Pass when the stub never used an http URL other than the fake base.
  • Lane 9. Health check path in the create payload is /robots.txt. Save t25-3-health.png. Pass when that string is in the body.
  • Lane 10. Port 4321 is in ports_exposes. Save t25-3-port.png. Pass when that string is in the body.

Verify, perf. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • Metric. Duration of the coolify.test.ts file at trunk (webhook file as stand-in if the client is missing) and head. The diff-added work is the mocked client suite. The user waits for the test process to exit 0.
  • Probe. node --test server/utils/coolify.test.ts timed at head. Time webhooks.test.ts at trunk first.
  • Baseline. Record the trunk webhook-test seconds first.
  • Rule. Head client tests must finish under 2 seconds. Fail above that. Do not ratio unlike suites.

Review gate. None. T25-3 is not review-gated.

Merge.

  • Root’s clean verdict at the exact head SHA.
  • Bugbot triage done.
  • Rebased onto current trunk after the verdict, patch-id unchanged.
  • The root appends T25-3. The operator squash-merges it.

Run the provisioner from a session API (T25-4)

Section titled “Run the provisioner from a session API (T25-4)”

Depends on. T25-3.

Files.

  • Create apps/master-dashboard/server/utils/provision-tenant.ts.
  • Create apps/master-dashboard/server/utils/provision-tenant-migrate.ts.
  • Create apps/master-dashboard/server/utils/provision-tenant.test.ts.
  • Create apps/master-dashboard/server/api/tenants/[slug]/provision.post.ts.
  • Edit apps/master-dashboard/nuxt.config.ts.
  • Edit apps/master-dashboard/.env.example.
  • Edit Contract.
  • Edit Deploy.

Build.

  • Model the run as a list of named steps in one function. Steps are database, migrate, secrets, coolifyApp, coolifyEnv, coolifyStart. Each step reads current columns, skips if the outcome already exists, then writes. Crash in the middle and the next POST continues.
  • database calls provisionTenantDatabase with NUXT_POSTGRES_ADMIN_URL, encrypts the tenant DSN, stores it in database_url_encrypted. If that column is already set, skip create and keep the stored password.
  • migrate lives in provision-tenant-migrate.ts. That file imports @platform/db only. provision-tenant.ts imports @platform/db/master only. Auth table names collide if one module loads both packages.
  • migrate decrypts that DSN and calls applyTenantMigrations. Do not wait for a Coolify container. That is how the first-deploy skip in Deploy stops being a blocker.
  • secrets generates BETTER_AUTH_SECRET, PLATFORM_API_KEY, and PLATFORM_WEBHOOK_SECRET with openssl-class randomness, encrypts them, and stores the three new columns. Generate each only when its column is null.
  • coolifyApp creates the app when coolify_app_uuid is null. On CoolifyDisabledError, stop after secrets, leave status=provisioning, write activity type tenant.provision_partial.
  • coolifyEnv bulk-sets runtime and build vars from Deploy. SITE_DOMAIN is {slug}.okhema.studio even without DNS. PLATFORM_WEBHOOK_URL is the master /api/webhooks/site. Then coolifyStart and insert deployments as running.
  • POST /api/tenants/:slug/provision requires requireAdminSession. The 2.4 queue is status=provisioning and coolify_app_uuid null. Also accept failed so the detail button can retry. Return 409 if status is active or suspended. Return 202 with the detail payload. Do not put ciphertext in the body.
  • Do not import tenants/*.json. Do not call bootstrapTenant. Do not call createCmsUser. Do not write domains. Do not PATCH Coolify backup lists in this PR. Note that skip in Deploy.

You see.

  • POST provision on a local row without Coolify token returns 202, fills database_url_encrypted, and leaves coolifyAppUuid null.
  • A second POST does not create a second database and still returns 202.
  • GET detail never includes encrypted fields.

Verify, unit. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • provision-tenant.test.ts covers skip-when-present for each step, disabled Coolify, and reject of active. Run pnpm --filter master-dashboard exec node --test server/utils/provision-tenant.test.ts.

Verify, live. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on grok-4.6-fast-xhigh at the PR head, per the boot recipe.

  • Lane 1. Regression lane against trunk. POST /api/tenants still creates a ledger row at trunk and head. If trunk lacks the feature, record that and gate 201 plus status=provisioning. Save t25-4-create-regression.png. Pass when the list contains the new slug.
  • Lane 2. POST /api/tenants/:slug/provision without session. Save t25-4-unauth.png. Pass when status is 401.
  • Lane 3. POST provision with Coolify disabled and a superuser admin URL if the lane has one. Save t25-4-partial.png. Pass when 202, database_url_encrypted is non-null in SQL, and JSON has no encrypted keys. If the lane has no CREATEROLE, pass when the handler returns 500 with permission denied to create role and status stays provisioning.
  • Lane 4. Second POST provision. Save t25-4-retry.png. Pass when 202 and pg_database still has one tenant_<slug>.
  • Lane 5. POST provision on status=active. Save t25-4-active-conflict.png. Pass when status is 409.
  • Lane 6. activity contains tenant.provision_partial or tenant.provision_started. Save t25-4-activity.png. Pass when GET detail shows that type.
  • Lane 7. pnpm turbo run typecheck. Save t25-4-typecheck.png. Pass when exit is 0.
  • Lane 8. Confirm migrate ran by to_regclass('public.site') on the new database when create succeeded. Save t25-4-schema.png. Pass when the value is site. Drop the probe database after the shot.
  • Lane 9. GET /api/tenants/:slug still lists empty domains. Save t25-4-no-domains.png. Pass when domains is [].
  • Lane 10. POST /api/webhooks/site with the new tenant secret succeeds after secrets exist. Save t25-4-webhook-after.png. Pass when status is 200. If secrets were not written, pass when the handler still uses the shared secret and the lane log says so.

Verify, perf. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • Metric. Time of POST /api/tenants/:slug/provision until 202 at head. Trunk has no route, so also time POST /api/tenants on trunk as the user-visible create they already wait for. The diff-added work is database plus migrate plus encrypt.
  • Probe. curl -w %{time_total} for trunk create and head provision, interleaved.
  • Baseline. Record the trunk create time_total first.
  • Rule. Head provision without Coolify must finish under 30 seconds when CREATEROLE works. When it cannot create a role, the 500 must return under 2 seconds. Fail outside those bounds.

Review gate. None. T25-4 is not review-gated.

Merge.

  • Root’s clean verdict at the exact head SHA.
  • Bugbot triage done.
  • Rebased onto current trunk after the verdict, patch-id unchanged.
  • The root appends T25-4. The operator squash-merges it.

Show provision progress on tenant detail (T25-5)

Section titled “Show provision progress on tenant detail (T25-5)”

Depends on. T25-4.

Files.

Build.

  • Add a Provision button on the detail page when status is provisioning or failed. The click POSTs /api/tenants/:slug/provision and refreshes detail.
  • Show deployments[] that T25-4 inserts. Keep the empty copy when the list is empty.
  • Write tenant.provision_failed into activity when the orchestrator throws after the handler started. Set tenants.status=failed only after a Coolify start error, not after CoolifyDisabledError.
  • Tick 2.5 in Taken with what shipped and what stayed out (preview DNS, bootstrap JSON, CMS user, backup list, master Coolify deploy).

You see.

  • An operator with a session clicks Provision and sees a 202 then an activity row without a reload of login.
  • Ciphertext never appears in the rendered HTML.
  • Taken 2.5 describes the partial local path.

Verify, unit. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • No new test runner. Re-run pnpm --filter master-dashboard exec node --test server/utils/provision-tenant.test.ts and pnpm --filter master-dashboard exec nuxt typecheck.

Verify, live. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on grok-4.6-fast-xhigh at the PR head, per the boot recipe.

  • Lane 1. Regression lane against trunk. Open /tenants/[slug] for an existing fixture at trunk and head. If trunk lacks the feature, record that and gate the detail heading plus activity list plus the new Provision button on head. Save t25-5-detail-regression.png. Pass when the slug heading matches and login is not required twice.
  • Lane 2. Click Provision on a provisioning tenant. Save t25-5-click.png. Pass when the button fires POST and the page still shows the slug.
  • Lane 3. After 202, activity mentions provision. Save t25-5-activity.png. Pass when that row is visible.
  • Lane 4. Deployments table empty or one running or queued row. Save t25-5-deployments.png. Pass when no invented hostname appears.
  • Lane 5. View source or network response of GET detail. Save t25-5-no-ciphertext.png. Pass when v1. blobs are absent.
  • Lane 6. Logged-out GET /tenants/[slug] redirects to login. Save t25-5-auth.png. Pass when the login form is shown.
  • Lane 7. Mobile width 390px on the detail page. Save t25-5-mobile.png. Pass when the Provision button stays inside the layout and does not overflow.
  • Lane 8. failed tenant still shows Provision. Save t25-5-retry-failed.png. Pass when the button is present.
  • Lane 9. active tenant has no Provision button. Save t25-5-active-hidden.png. Pass when the control is absent.
  • Lane 10. Logout still returns / to login. Save t25-5-logout.png. Pass when the login page is shown.

Verify, perf. Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked.

  • Metric. Time to first byte of GET /tenants/[slug] at trunk and head. The diff-added work is the extra button markup. The user waits for the slug heading.
  • Probe. curl -o /dev/null -s -w %{time_starttransfer} with the session cookie, trunk then head, interleaved.
  • Baseline. Record the trunk time_starttransfer first.
  • Rule. Head must stay under 1.5 seconds and must not exceed trunk by more than 200 ms. Fail if either bound breaks.

Review gate. The operator reviews before merge.

  • Copy lane 2 screenshots into /tmp/swarm-T25-5/review/t25-5-review-click.png.
  • Record a 30 to 60 second video of the change on a lane VM. Save it as /tmp/swarm-T25-5/review/t25-5-review.mp4.
  • Post the screenshots and the video in chat. Stop at merge-ready. Wait for the operator’s click.

Merge.

  • Root’s clean verdict at the exact head SHA.
  • Bugbot triage done.
  • Rebased onto current trunk after the verdict, patch-id unchanged.
  • The root appends T25-5. The operator squash-merges it after the review click.
  • Every box above is checked with its evidence.
  • Reply to the operator with the report the execution playbook names.

Idempotent migrate is proven on this VM. DATABASE_URL=postgresql://platform:platform@localhost:5432/tenant_demo pnpm --filter @platform/db run migrate printed Migrations applied successfully twice. drizzle.__drizzle_migrations stayed at 3. to_regclass('public.site') stayed site. No branch and no SHA. The command ran on the plan branch before this file existed.

CREATE ROLE is unproven here. platform has rolcreaterole=f. postgres peer auth failed. scripts/provision-tenant-db.sh without PGDATABASE=postgres also failed because psql used database platform, which does not exist. T25-1 and T25-4 lanes must bring a superuser DSN or accept the permission-denied pass.

Coolify HTTP is unproven. This VM has no Coolify token. Public docs list POST /api/v1/applications/private-github-app (and siblings), PATCH /api/v1/applications/{uuid}/envs/bulk, and POST /api/v1/applications/{uuid}/start. State-changing routes require POST. T25-3 tests stay on a fake fetch. A real create stays operator-gated until a token exists.

First Coolify deploy skip is already proven in Freeze-check and Deploy. The plan does not re-run that. T25-4 migrates through the tenant DSN instead of docker exec.

A fifth tenant_status value for each step. The ledger already has provisioning and failed. Extra enum values fight 2.4’s PATCH rules. Columns plus deployments already say how far the run got.

docker exec of migrate.mjs after the first start. That is the freeze-check workaround. The provisioner already has the DSN. Migrate before the container exists.

POST /applications/dockerfile with file contents. Deploy builds from repo root and apps/site-template/Dockerfile. Git create matches that.

Run Coolify from @platform/ops. Ops stays free of HTTP clients except S3. Coolify is a Nitro adapter. Database SQL stays in ops because the shell script already lives there.

Call bootstrapTenant from JSON under tenants/. Architecture already says those files are content, not the ledger. 2.6 still needs a hostname. An empty migrated schema is enough for 2.5.

Block create until Coolify is configured. Task 2.5 may run locally for a while (Taken). Disabled Coolify is a first-class stop after secrets.

Update the Coolify per-database backup list in 2.5. Freeze-check asked for it. The public API for that list is not in the OpenAPI slice we fetched. Leave it as a documented operator step rather than a guessed PATCH.

Delete a tenant. 2.4 deferred it. Cascade vs a live database is a product call. This program does not delete.

Import @platform/db and @platform/db/master in one file. Table names users and sessions collide. Split migrate into its own module.

Auto-mark active when Coolify start returns. /robots.txt is healthy with an empty schema. Status stays provisioning or failed until an operator (or 2.6) decides the site is live.

Backfill coolify_app_uuid for pilot and rb-media inside 2.5. Those apps already exist. Recreating them is the wrong retry. Leave attach-existing as a later operator action, not a T25-4 step.

T25-1 and T25-4 need CREATEROLE. Without it, live proof stops at the denied error. The owner must not weaken SQL to run as platform.

T25-3 may guess git fields (private_key_uuid, project_uuid). A wrong field fails only when a token appears. Keep the client strict. Map fields from one live GET of an existing app (pilot) during operator go, not during this plan.

T25-4 SITE_DOMAIN={slug}.okhema.studio without DNS. The container can be healthy on /robots.txt and still fail public TLS. Do not mark active in 2.5.

Master dashboard is not on Coolify yet. The provisioner is a local Nitro process until that loose Taken item ships. Long Coolify waits must not block the HTTP worker beyond start-accepted.

This repo has no pstack/ on main. The arm step’s git show origin/main:pstack/... will fail. Owners read the same skills from the Cursor plugin cache and record that fallback in the tick.

Shared NUXT_PLATFORM_WEBHOOK_SECRET fallback in T25-2 means a tenant with a new secret still accepts the old shared secret if the column is null. Once the column is set, shared-secret POSTs must 401. Do not leave mixed secrets on a live slug without a cutover note.

A second CREATE ROLE does not rotate the password. If database_url_encrypted is lost while the role remains, the next POST cannot guess the old password. Treat a missing ciphertext plus an existing role as failed with a clear activity payload, not a silent new password.

tenants/README.md and the tenants/ line in AGENTS.md still describe the pre-2.4 JSON ledger. Do not treat that directory as the customer register. Fix those two sentences in T25-5 docs if they still lie.

Read Taken (2.5 and the local-master note), Deploy, Operations, Contract, Datamodel, Architectuur, Freeze-check section D, Taakplan 2.4 (ledger is not provision), packages/ops/README.md, scripts/provision-tenant-db.sh, packages/db/scripts/migrate.mjs, packages/db/src/master/schema.ts, apps/master-dashboard/server/utils/secrets.ts, apps/master-dashboard/server/api/tenants/index.post.ts, apps/master-dashboard/server/api/webhooks/site.post.ts.

T25-3 and T25-4 get pstack/skills/how/SKILL.md on the Coolify adapter and the step list. T25-4 gets pstack/skills/interrogate/SKILL.md if someone wants a queue table instead of the step list. Trail per pstack/skills/show-me-your-work/SKILL.md for the execution owners.