← Multi-tenant roadmap

Phase 4 beta verification — 2026-08-27

Verified against beta running bebb0a83 (PRs #316#319 plus #329/#332/#342), beta environment

All checks executed 2026-08-27 (headless, API and DNS only — no screenshots)

Verdict

A new tenant went from zero to a live managed subdomain without a manual infrastructure step

Sixteen checks passed. Tenant “Real Success Academy” (slug rsa) was created through the platform API, its managed subdomain rsa.beta.skillpixel.vn was provisioned by the GitHub Actions workflow dispatched from the backend, and the host served HTTPS at the end of the run. No findings were filed. Three items were not exercisable on beta and are listed under Not verified on beta — chiefly custom-domain activation itself, which is deliberately not supported on beta.

16Checks passed
0Findings filed
3Not verified
~2Minutes to provision

What was verified

One row per ticket. The custom-domain tickets are covered by tests only on beta — see Not verified on beta.

TicketScopeResultEvidence
SP-617 Domain ownership verification via our own TXT challenge (PR #316) TESTS ONLY Section 5
SP-618 Firebase authorized domains and the 75/90 cap (PR #317) PASS Check 11
SP-619 Cloudflare custom hostnames and canonical-host rules (PR #318) PARTIAL Check 16
SP-477 Custom-domain activation behind a readiness checklist (PR #319) TESTS ONLY Section 5
SP-633 Managed-subdomain provisioning workflow (PR #329) PASS Section 3
SP-634 Dispatch and status from platform-admin as a GitHub App (PR #332) PASS Checks 5–6
SP-636 Managed-subdomain zone enforcement (PR #342) PASS Checks 1, 3

Checks

Executed in order against the beta backend, which was running the head of main (bebb0a83). Tenant id cfd2ab30-5ca9-480d-9f41-74bdeef568f5, domain id 6e292fcc-8c7b-48f7-9787-de41947b64c3.

#StepCallResultEvidence
1 Managed zone is fixed per environment GET /platform/domains/managed-zone PASS 200, {"zone":"beta.skillpixel.vn","label_pattern":"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$"}
2 Create the tenant POST /platform/tenants PASS 201, “Real Success Academy”, slug rsa
3 A managed subdomain outside the zone is rejected POST .../domains with rsa.skillpixel.vn PASS 422 “Managed subdomains must be a single label under beta.skillpixel.vn, e.g. acme.beta.skillpixel.vn”
4 Add the in-zone managed subdomain POST .../domains with rsa.beta.skillpixel.vn PASS 201, primary
5 Dispatch provisioning as the GitHub App POST .../domains/{id}/provision {action: add} PASS 202, run_name “Provision rsa.beta.skillpixel.vn (add, beta)”
6 Status endpoint finds the run by its run name GET .../provisioning-status PASS no run yet → in_progresscompleted/success, with the run URL
7 Workflow run completes with every step green GitHub Actions run 33108126792 PASS success in about 2 minutes — see Section 3
8 Activate the domain PATCH .../domains/{id} ACTIVE PASS 200, status ACTIVE, verified_at 2026-08-27T19:23:55Z, no activation_warning
9 Add the tenant admin POST .../memberships PASS 201, skillpixeldeveloper@gmail.com with TENANT_ADMIN
10 Activate the tenant (gate passes with a domain and an admin present) PATCH /platform/tenants/{id} ACTIVE PASS 200, status ACTIVE
11 Firebase authorized domain was added by the workflow Identity Toolkit config, project skill-pixel-beta PASS rsa.beta.skillpixel.vn present in the authorized-domain list
12 DigitalOcean app domain reaches phase ACTIVE DO app lms-beta-frontend domain list PASS ALIAS domain phase ACTIVE, certificate issued
13 Cloudflare record is a DNS-only CNAME to the DO app dig rsa.beta.skillpixel.vn PASS CNAME lms-beta-frontend-8n8v8.ondigitalocean.app
14 TLS certificate matches the host Certificate presented on the TLS handshake PASS CN rsa.beta.skillpixel.vn, Google Trust Services WE1, issued via DigitalOcean
15 The host serves the app over HTTPS GET https://rsa.beta.skillpixel.vn PASS HTTP 200
16 Bootstrap resolves the tenant by host and reports the canonical URL GET /bootstrap with X-Tenant-Host PASS slug rsa, canonical_url https://rsa.beta.skillpixel.vn, canonical_redirect false

The provisioning run

One workflow_dispatch of provision-tenant-domain.yml, dispatched by the backend with a one-hour GitHub App installation token.

GitHub Actions

Run 33108126792 — success in about two minutes

Inputshostname rsa.beta.skillpixel.vn, environment beta, action add.
Run nameProvision rsa.beta.skillpixel.vn (add, beta) — deterministic, which is how provisioning-status finds the run without storing a run id.
DispatcherGitHub App managed-domain-provisioner (org skill-pixel, Actions: Read and write, installed on lms-platform only). The backend mints an app JWT, exchanges it for an installation token, and dispatches.
  • Validate inputs. Hostname shape and environment accepted.
  • Install tooling. doctl and yq available.
  • Resolve the app. DigitalOcean app lms-beta-frontend located for the beta environment.
  • Upsert the Cloudflare record. DNS-only CNAME to the app's ondigitalocean.app host (runbook §5).
  • Add the DigitalOcean domain. ALIAS domain appended to the app spec (runbook §4).
  • Wait for the domain and certificate. Polled until the domain reached phase ACTIVE with a certificate.
  • Add the Firebase authorized domain. Identity Toolkit config updated for skill-pixel-beta (runbook §7).
  • Summary. Step summary printed with the resulting hostname and app.

The workflow is idempotent: re-running add for an existing hostname is a no-op on each of the three systems. The platform-admin domain row and the activation click stay manual by design.

Not verified on beta

Three items could not be exercised on this environment. They are covered by unit and end-to-end tests and are not counted as failures.

Custom-domain activation with real Cloudflare and Firebase

NOT EXERCISED SP-617 SP-619 SP-477

Custom domains are intentionally not supported on beta. The split-host beta setup cannot serve a customer-owned domain end to end, so the Cloudflare environment is left unset there and activation is blocked with a clear 503 instead of a silent skip. TXT ownership verification still works on beta, since it only needs a DNS lookup.

Covered by unit tests for the TXT challenge, the Firebase client and cap, the Cloudflare custom-hostname client and the canonical-host rules, plus the SP-477 end-to-end test over the readiness checklist. First real exercise happens on production once the production prerequisites on the roadmap are in place.

The remove workflow path

NOT EXERCISEDSP-633

Only action: add was run. The remove path (delete the Cloudflare record, the DigitalOcean domain and the Firebase authorized domain) is best-effort by design — each step tolerates an already-absent resource — and was not dispatched here.

Production

NOT EXERCISED

Nothing in Phase 4 is in a production release yet; production is still on v0.0.39. The workflow's prod environment path, the skillpixel.vn managed zone and the production GitHub App secrets have not been exercised.

Test residue and cleanup

State left on beta, recorded so a later reader does not mistake it for real data.

State left on beta

The rsa tenant and its subdomain were left live

  • Tenant. “Real Success Academy” (rsa, cfd2ab30-5ca9-480d-9f41-74bdeef568f5) ACTIVE, with skillpixeldeveloper@gmail.com as tenant admin. Empty catalogue.
  • Domain. rsa.beta.skillpixel.vn ACTIVE and verified, primary. Cloudflare CNAME, DigitalOcean ALIAS domain and Firebase authorized domain all left in place — this is the second live beta tenant host and is useful as a standing smoke target.
  • Removal. To tear it down, dispatch the same workflow with action: remove from platform-admin, then delete the domain row and the tenant.

Method

Checks were API-driven: curl against the beta Cloud Run API with a Firebase ID token minted for the beta platform-admin account. DNS was checked with dig, the certificate was read from the TLS handshake, and the GitHub Actions run was inspected through the REST API. No browser session and no screenshots — every observable in this report is an HTTP response, a DNS answer or a certificate.

Verified by Claude (Fable 5) on 2026-08-27.