User Manual · updated 2026-06-27

Operate the CMS Platform with evidence in view.

This manual now includes the 2026-06-27 production-deployment addendum: backend, CMS, and web are live on GCP Cloud Run run.app with Cloud SQL PG17, Upstash Valkey TLS, Secret Manager, and declarative Terraform IaC. Live shared-table RLS has also been provisioned on Cloud SQL and wired into the backend. The screenshots below are still the existing 2026-06-16 local visual captures plus the 2026-06-21 governed payment screenshot; this pass adds deployment status, not fresh production visual proof.

3Cloud Run services live
41RLS tables FORCE-enabled
3domain certs provisioning
0new screenshots
Addendum · 2026-06-27

Production deployment status — GCP Cloud Run live, RLS wired (no new screenshots)

The platform now has a managed production deployment path: GCP Cloud Run for backend, CMS, and web; Cloud SQL PG17 via Auth-Proxy unix socket; Upstash Valkey over TLS; Google Secret Manager; and Cloud Run domain mappings for api, www, and admin. This manual update records the deployment state and operator boundary; it does not replace the existing local/manual screenshots.

LIVE · Cloud Run

Runtime and IaC authority

Backend boots in prd, GraphQL returned HTTP 200, and Cloud SQL + Upstash connections were observed. CMS and web are healthy on run.app. Terraform in infra/terraform/envs/gcp-cloudrun/ is the declarative authority; the live backend was imported into a 0-destroy reconcile.

LIVE · RLS provisioned

Shared-table RLS is no longer just local proof

The live Cloud SQL database now has cms_tenant_app as a non-superuser app role, ENABLE+FORCE RLS on 41 tables, and verify-rls PASS. Backend env/secrets now wire SCHEMA_TIER_APP_USER / password through Secret Manager.

Pending

Custom-domain and public-domain checks

Google-managed certificates for api, www, and admin are still provisioning. Until they finish, this manual does not claim end-to-end browser proof on custom domains or public-domain /mcp cross-tenant proof.

Not claimed

Deferred runtime surfaces

Worker runtime, AGE/GraphRAG, multi-region, and UAT sign-off remain outside this pass. Existing screenshots stay predecessor/local evidence, except the payment donor screenshot which keeps its governed browser-full-integration tier.

Authority: .agents/specs/production-deployment-topology/{review.md,adr/ADR-DEPLOY-003-gcp-cloudrun-cloudsql-cloudflare.md}, .agents/specs/{SPECS.md,NEXT_STEPS.md,RTM.md,ISSUE_LOG.md}, and infra/terraform/envs/gcp-cloudrun/. Branding: refreshed with the provisional NAELT CIS red/gray/orange palette.
Addendum · 2026-06-24

Resilience & fail-closed hardening (no new screenshots)

A run of backend/test-tier prod-readiness lanes shipped as PRs against dev (#107/#108/#110/#111/#112/#113), hardening the platform's failure behavior with executable chaos & fail-closed coverage and one real wire-up fix. These change backend behavior, not operator screens, so the screenshots below still stand; this is a spec/PR-cited content note, not a fresh-runtime regen. For operators the practical effect is: ingest/payment webhooks dedup safely under concurrent floods, asset upload & AI generation fail closed (no orphan records, no leaked DB connections), and AI-quota enforcement is proven on the real transaction path.

  • Webhook-flood dedup under race (REQ-RCC-006, PR #113) — proven on governed PostgreSQL: 8 concurrent IngestMessage for the same provider message → exactly one draft + one log persist; the rest dedup with no orphan. Closes the last adjacent-dependency chaos gap.
  • AI-generation transaction leak fixed (PR #110) — an invalid generation type used to return without rolling back its open transaction, slowly leaking pooled DB connections; now fixed and regression-guarded.
  • Fail-closed coverage locked (PRs #104/#107/#108) — transaction-scoped AI-quota enforcement, GraphRAG degrade-when-embedding-provider-down, and asset storage-down fail-closed (no orphan asset record).
  • Decision-logic coverage (PRs #111/#112) — recommendation ranking (behavior-weight × time-decay) and MCP connection-pool LRU eviction, both previously unguarded.
Evidence Source: spec/PR-cited tests (no runtime screenshots this addendum). Coverage Tier: unit + property + hand-killed mutation + self-contained & governed-PostgreSQL chaos/resilience. Readiness State: local proof only; NO governed-bundle browser, live external provider/OAuth/AI, hosted-Postgres, multi-node/real-image HA, DNS/TLS/deploy/UAT/production. Authority: .agents/specs/resilience-chaos-coverage/review.md, ISSUE_LOG.md (CHAOS-COVERAGE-EXT-001 Resolved), backend/test/TESTS.md, PRs #104/#107/#108/#110/#111/#112/#113.
Addendum · 2026-06-18

Production-readiness hardening (no new screenshots)

A backend/config-tier hardening lane shipped as PR #71 against dev, promoted from a repo-wide mock/stub/false-green audit (prod-readiness-wireup-hardening). It changes security/wire-up behavior, not operator screens, so the screenshots below still stand; this is a spec/PR-cited content note, not a fresh-runtime regen.

  • Per-tenant DB connections now fail closed in production. Site (tenant) database connections previously could fall back to default credentials and non-TLS in production; they now reject default user/password and require TLS (require/verify-ca/verify-full) when ENV=production, matching the platform-DB validation. Dev/local behavior unchanged.
  • AI provider keys are server-only. The CMS no longer reads NEXT_PUBLIC_*_API_KEY (which would ship secrets in the browser bundle); generation routes through the backend BYOK path, and the admin endpoint fallback now fails closed on Vercel-style production markers.
  • Real-dependency health proof. An empty placeholder test was replaced with a real chaos/resilience test proving a downed Postgres surfaces as HTTP 503 (unhealthy), bounded (no hang).
  • Docs reconciled. Module guides that under-stated implemented OAuth (Google/Facebook/Microsoft/Cognito + SAML + LDAP), storage (S3/GCS/Azure/R2/local), and real data-binding maturity were corrected.
  • Reliability proven under fault injection (PRs #72–#74). New chaos/resilience tests prove the platform degrades gracefully and bounded, never hangs: connection-pool exhaustion stays within its limit and recovers; a transient tenant-database outage is not cached and routing self-heals when the DB returns; and a hung AI provider fails with a bounded, typed error (finite client timeout).
  • Input sanitizer + fail-closed guards locked (PRs #75–#77). The name→tenant-ID/database-name sanitizer and the production credential/TLS fail-closed guards gained property-based + wiring tests so a future change can't silently weaken the only thing standing between user input and a database name, or re-open an insecure connection path.
Evidence Source: spec/PR-cited tests (no runtime screenshots this addendum). Coverage Tier: unit + property-based + hand-killed mutation (backend) + vitest/tsc (cms) + self-contained & governed-PostgreSQL chaos/resilience. Readiness State: local proof only; NO governed-bundle browser, live external provider/OAuth/AI, hosted-Postgres, multi-node/real-image HA, DNS/TLS/deploy/UAT/production. Authority: .agents/specs/{prod-readiness-wireup-hardening,resilience-chaos-coverage}/review.md, RTM.md, PRs #71–#80.
Addendum · 2026-06-21

Payments / Donations / E-commerce (browser-proven)

A generic, site-scoped payment / donation / e-commerce module shipped as PR #86 against dev (payment-plugin). Donation is one purpose preset of a generic Payment* core. Below is the live donor page rendered from DB-driven page data against the real backend.

Public donor form — PaymentForm (DB-driven)

The seeded NAELT /site/naelt/support/donate page renders the generic PaymentForm (the donation-form manifest preset) entirely from DB page data: preset amounts, custom amount, payer fields, and a provider-driven submit (Stripe redirect / 藍新金流 NewebPay form-post). No secrets touch the browser — the server resolves the site's BYOK provider by siteID.

Public donor PaymentForm rendered from DB-driven page data
Evidence Source: governed multi-tenant web env, real backend (MSW off), Playwright 2/2. Coverage Tier: browser-full-integration. Readiness State: PASS for DB→manifest→PaymentForm render + live seeded presets; NO live external-provider settlement.

CMS admin — Donation & Payment + Products & Services

The CMS Settings page gains a Donation & Payment tab (provider catalog, guided setup wizard, masked BYOK secret rotate, donation/order list + ezPay 電子發票 status) and a Products & Services tab (catalog CRUD, SKU variations, Udemy-style digital-content authoring: public preview vs paid). Admin GraphQL is RBAC-gated (site.payment.* / site.catalog.*).

Evidence Source: governed cms-e2e bundle, real /api/auth/login + real GraphQL (MSW off), Playwright 3/3 (live provider catalog, catalog tab, viewer-denied). Coverage Tier: browser-full-integration. Readiness State: PASS for admin tabs; admin-initiated refund + live settlement are residual (ISSUE_LOG.md PAY-REFUND-001).

Capability 8 — Payments / Donations / E-commerce. Generic public createPaymentCheckout(purpose) + provider-registry drivers (Stripe Checkout one-time + subscription; 藍新金流 NewebPay MPG 2.0 + 定期定額) + ezPay 電子發票 auto-issue + BYOK per-site secrets + product/service catalog + digital entitlements (auto-grant on paid, fail-closed access); the donation-form page-builder component renders the generic PaymentForm; one mount switch PAYMENT_MODULE_ENABLED. Authority: .agents/specs/payment-plugin/review.md, PR #86.

Worked Examples · 2026-06-18

Platform Capabilities — End-to-End Examples

Detailed, contract-grounded worked examples for the platform's authoring & runtime capabilities, using the showcase tenant as the canonical example (its home + a /components gallery page exercise all 11 page-builder components). Evidence tier: illustrative worked examples grounded in real GraphQL/route/manifest contracts — backend/CLI-lane evidence (commands & payloads), not live-demo screenshots; claim-cap per capability below. Authority for readiness remains each spec's review.md.

1. Page Builder (manifest-driven, DB-backed, no rebuild)

Pages are arrays of sections; each section names a component from the component-manifest SSOT (componentManifests GraphQL ← backend/internal/componentmanifest/manifests.json) plus its props. The CMS Pages builder edits sections and saves the whole section array via updatePageSections(id, sections: [JSON!]!). Public routes are force-dynamic, so adding an already-registered component to a page is DB-data-only — no image rebuild.

# Showcase home is built from these manifest-backed sections (backend/seeds/showcase-pages.json):
HeroSection · FeatureGrid · ContentList · EventSection · ImpactStats · CTASection
# The /components gallery page additionally demonstrates:
ResourceGrid · ContentSection · EnhancedContentList · EnhancedEventCalendar

# Save an edited page (full-array replace) — from the CMS page builder:
mutation { updatePageSections(id: "<pageId>", sections: [ /* the full ordered section array */ ]) { id } }

Claim-cap: manifest-validated section authoring; adding registered components is runtime-proven DB-only. NOT a new-component rebuild path. Authority: shadcn-component-library-a2ui-authoring/review.md.

2. AI Content Generation (backend BYOK)

Generation runs server-side through generateAIContent(prompt, providerId); the CMS never holds provider secrets in the browser (see the 2026-06-18 security hardening). Each site declares its own provider profiles (Google Vertex/Gemini, AWS Bedrock, Azure AI/OpenAI, OpenAI, Anthropic, TensorZero, OpenCode) with BYOK secret metadata + a usage ledger.

mutation { generateAIContent(prompt: "Draft a launch blog post about our new studio", providerId: "") { content model tokensUsed } }
# providerId "" = use the site's default profile; the backend resolves the BYOK key, never the browser.

Claim-cap: backend-routed generation contract; NO live external-provider call in this manual. Authority: site-byok-ai-provider-settings/review.md.

3. Content Management (types · draft/publish · submissions · RBAC)

Content is modeled by content types and moves through a draft → published → unpublished status lifecycle (status-scoped listing). Public forms submit into a governed submission workflow (submit → approve/reject) reviewed in the CMS. Every operation is permission-checked (e.g. site.content.*, site.social.read/manage).

# Public site form section (showcase /contact) → backend-issued submissionId → CMS review queue (approve/reject).
# Content list/detail pages render DB content by contentType (article/event) via ContentList/ContentSection.

Claim-cap: draft/publish breadth + submission workflow proven at unit/backend tier (LAUNCH-007); CMS review is backend-backed where the governed bundle runs.

4. MCP & Agent-Friendly Surfaces

Each site exposes agent-friendly read surfaces: a Model Context Protocol endpoint at /mcp (list/get/search contents), an MCP discovery doc at /.well-known/mcp.json, and per-site + platform llms.txt. Non-VIP tenants' reads are confined by D2 Row-Level Security (routed non-superuser role) when SCHEMA_TIER_APP_USER is enabled.

# Discover + read a site over MCP (HTTP/JSON-RPC):
GET /.well-known/mcp.json        # server description
POST /mcp  → tools: list_contents(site) · get_content(site, slug) · search_contents(site)
GET /site/showcase/llms.txt      # agent-friendly site summary

Claim-cap: /mcp read path is backend-backed on the full local stack + RLS-confined as a non-superuser role on governed PG; cloud-deploy/UAT remains owner/infra (SPTR-LIVE-WIRING-001). Authority: schema-per-tenant-routing-rls/review.md.

5. Agent Skills (per-site declarative skill templates)

Sites define declarative skill/prompt templates (internal/skilltmpl; seeds skills-platform.json/skills-naelt.json) used to convert/optimize content — no script/code execution, no sandbox. Templates are versioned and managed via upsertSkillTemplate (create ⇒ v1; update ⇒ version bump; published templates immutable; cross-site fail-closed; @requirePermission("site.social.manage")).

mutation { upsertSkillTemplate(input: { id: 0, kind: "post-conversion", name: "LINE→blog", body: "..." }) { id version kind } }
# id:0 → creates version 1; id>0 → updates + bumps version.

Claim-cap: declarative template contract (no code execution); service + RBAC proven at enttest tier. Authority: messaging-ingest-social-publishing/review.md.

6. A2UI (agent-authored pages via the manifest)

The A2UI agent (backend/internal/adk) authors page-builder sections from the same component-manifest catalog, gated by a manifest-validation step + a design-contract no-invented-facts honesty boundary + an untrusted-content boundary. It proposes manifest-valid sections that you save through the normal updatePageSections path — so agent output is held to the exact same contract as human page-builder edits.

# A2UI flow: catalog prompt (componentManifests) → agent proposes sections → manifest validation gate
#            → design-contract honesty check → human review → updatePageSections(save).

Claim-cap: manifest-validated authoring + honesty boundary proven at unit/contract + dev-web browser tier; NO live-LLM round-trip claim. Authority: shadcn-component-library-a2ui-authoring/review.md.

7. Social Media — Complete Flow (ingest → AI convert → review → publish)

A platform-agnostic, integration-first pipeline (messaging-ingest-social-publishing): inbound messages arrive on a single canonical webhook, are normalized, converted to a post draft via the site's AI provider + a declarative skill template, queued for review, then published through pluggable publisher drivers. Site-scoped RBAC throughout (site.social.read/manage/publish/secret); secrets are site-BYOK.

# 1) INGEST — providers self-register; one canonical inbound route per platform/site:
POST /api/webhooks/messaging/{platform}/{site}    # e.g. line, telegram, whatsapp (signature-verified, fail-closed)

# 2) CONVERT — normalized message → post draft via SiteAIProviderResolver + per-site skill template (no sandbox).
# 3) REVIEW — draft sits in the CMS "Messaging & Social" review queue:
mutation { updatePostDraft(input: { id: "<draftId>", blocks: [...], metadata: {...} }) { id status version } }   # published drafts are immutable
# 4) PUBLISH — through a pluggable SocialPublisher driver (e.g. Facebook Graph):
mutation { publishPostDraft(id: "<draftId>") { id status } }   # @requirePermission("site.social.publish")

Claim-cap: repo-local integrated feature + governed cms-e2e backend-backed Messaging & Social browser smoke (T16.6); NO real LINE/Telegram/WhatsApp delivery, real Facebook publish, real OAuth, or live-AI quality. Authority: messaging-ingest-social-publishing/review.md.

Evidence Source: real GraphQL/route/manifest contracts + spec review.md (no new runtime screenshots — worked-example/contract tier). Coverage Tier: per-capability claim-caps above (mix of backend-backed, unit/contract, and runtime-backed). Readiness State: local proof only; NO hosted-Postgres, live external provider/OAuth/AI, DNS/TLS/deploy/UAT/production. Authority: .agents/specs/SPECS.md, RTM.md, and the cited per-spec review.md.
Addendum · 2026-06-17

Backend lanes shipped since this pass (no new screenshots)

Two backend/API-tier lanes shipped as PRs against dev after the 2026-06-16 runtime capture below. They add backend behavior, not new operator screens, so the screenshots on this page still stand; this is a spec/PR-cited content note, not a fresh-runtime regen.

  • Spawn now provisions the VIP database (PR #68). Previously a spawned VIP site was a control-plane record pointing at a database that did not exist (which is why earlier backup proofs had to hand-create cms_showcase). SpawnSite now creates the physical cms_<slug> database and migrates the schema into it for VIP sites; a non-VIP (SCHEMA/D2) site stays record-only by design. Fail-closed: if provisioning fails the site record is rolled back, so no dangling registry entry is left.
  • Messaging & Social draft/template editing API (PR #69). The updatePostDraft (edit a draft's blocks/metadata before publish — a published post is immutable) and upsertSkillTemplate (create, or update + version-bump, a skill template) GraphQL operations are now implemented, both gated on the site.social.manage permission. The in-CMS editor UI for these is a tracked follow-up.
Evidence Source: spec/PR-cited backend tests (no runtime screenshots this addendum). Coverage Tier: unit / property / mutation / enttest + governed-PostgreSQL integration (spawn). Readiness State: backend behavior proven locally; the governed-bundle backend-backed browser smoke for the Messaging & Social tab is a tracked residual. NO DNS/TLS/deploy/UAT/production. Authority: .agents/specs/RTM.md (2026-06-17 lanes 1+2 rollup), PRs #68/#69.

Getting Started / Starter Assets

Use the tracked seed/demo data first. Do not invent one-off manual examples.

Seed data (4 sites)

backend/seeds/{platform,naelt,showcase,tenant-demo}-*.json provide pages, content, submission types, themes, navigation, and skills. tenant-demo carries "tenancyTier":"schema" so it seeds as a non-VIP site. Seed with python scripts/ops.py db seed {platform|naelt|showcase|tenant-demo}.

E2E sample data

scripts/seed_e2e_data.py / scripts/seed_e2e_data.sql document canonical roles and sample tenants for repeatable test/demo flows (8 runtime users: platform_admin, site_admin, editor, viewer, MFA, two tenant admins). Passwords marked MOCK-DEV-ONLY-* are never production credentials.

Fresh assets

Current local screenshots and CLI transcripts live in docs/manual/assets/ as *-manual-2026-06-16.png and *-cli-2026-06-16.txt; the 2026-06-27 update adds deployment/RLS status without new screenshots.

Core Operator Flows

Follow these routes on the local runtime unless a production target has been explicitly named and provisioned.

CMS dashboard orientation

Open http://localhost:23000, authenticate through the configured flow, and verify dashboard scope. Platform admins see the platform control-plane shell with platform-site metrics (now 4 total sites), Recent Activity, and Quick Actions; site admins see a site-scoped workspace. Scope authority comes from the authenticated role and current site/tenant context, not the visual cues.

CMS platform dashboard current runtime screenshot
Evidence Source: live CMS runtime, CMS auth API cookie bootstrap. Coverage Tier: hybrid. Readiness State: PASS for dashboard visibility, not full browser-login proof.

Manifest-driven page builder (Pages)

Open http://localhost:23000/pages. The list is the entry to the manifest-driven page builder: per-page Layout (default / content-detail / event-detail), Status, search-by-title/slug, status and layout filters, and Preview/Edit/Delete per row. Adding an already-registered platform component to a page is a DB-data change — no frontend rebuild.

CMS Pages / manifest-driven page builder current runtime screenshot
Evidence Source: live CMS runtime screenshot (cookie bootstrap). Coverage Tier: hybrid. Readiness State: PASS for the seeded Pages surface; live page-save/publish and A2UI apply are separate workflow proofs.

Public platform homepage (shadcn / Tailwind v4)

Open http://localhost:23001/site/platform. The seed-backed marketing surface renders on the shadcn/Tailwind-v4 foundation: gradient hero, feature grids, onboarding steps, platform metrics, and CTA sections.

Platform homepage current runtime screenshot
Evidence Source: live public screenshot. Coverage Tier: local browser route evidence. Readiness State: PASS for styled public render; production visual/SEO/CDN remains separate.

Non-VIP site — tenant-demo (SCHEMA tier · D2 shared-table RLS) New

Open http://localhost:23001/site/tenant-demo. This fourth seeded site is non-VIP: its content lives in the shared platform tables and is isolated by PostgreSQL Row-Level Security keyed on the site slug (the "D2" model), rather than in a dedicated database. It renders identically to a VIP site — proving the isolation model is invisible to the end user.

tenant-demo non-VIP schema-tier public site current runtime screenshot
Evidence Source: live public screenshot of the seeded non-VIP site. Coverage Tier: local browser route evidence. Readiness State: PASS for seeded non-VIP render; live Cloud SQL RLS is now provisioned; custom-domain public read/MCP proof remains separate (see Host Preflight).

Showcase site (third seeded site)

Open http://localhost:23001/site/showcase — "Showcase Studio", a distinct DB-driven demo site proving multi-site delivery from one codebase via site config/theme/page seeds.

Showcase site home current runtime screenshot
Evidence Source: live public screenshot. Coverage Tier: local browser route evidence. Readiness State: PASS for seeded multi-site render.

Platform contact flow

Open http://localhost:23001/site/platform/contact to inspect the platform public submission route.

Platform contact current runtime screenshot
Evidence Source: live public screenshot. Route rendering only; submit/readback breadth is not upgraded by this screenshot.

NAELT volunteer registration

Open http://localhost:23001/site/naelt/volunteer/register. The seeded manifest renders Traditional Chinese field groups (志工報名) and required fields.

NAELT volunteer form current runtime screenshot
Evidence Source: live public screenshot. Coverage Tier: route/form rendering. Submit success/admin review is a separate workflow proof.

Tenant Lifecycle & DR admin

The platform decides each site's data-isolation architecture from a single VIP flag: VIP ⇒ database-per-tenant (the site gets its own physical database); non-VIP ⇒ shared-table Row-Level Security (the "D2" model — shared database and schema, isolated by RLS keyed on the site, enforced at runtime via a non-superuser application role). VIP is platform-controlled today.

Platform super-admin — "Sites & DR" (CMS /platform/sites)

Platform super-admins open http://localhost:23000/platform/sites to manage every site from one table. Each row shows the site, VIP star, tier (SCHEMA or DATABASE), base URL, and custom-domain verification state. Per-row actions:

  • Set / Remove VIP — opens a confirm dialog that explains it auto-migrates the architecture: making a site VIP moves it onto its own physical database; removing VIP downgrades it back to the shared schema + RLS. The migration runs a fail-safe pg_dump/restore sequence (provision the target, copy, verify, then drop the source last). Note tenant-demo shows Set VIP while the three DATABASE sites show Remove VIP.
  • Close — DR-safe decommission. It requires typing the site slug to confirm and backs the site up before dropping it. No accidental default-true confirmation.
  • Verify — verifies a pending custom domain (checks the required DNS-TXT record).

The Spawn site and Backup (configure / run / restore-test) actions render disabled — those operations are not available yet.

Platform Sites & DR admin table with 4 seeded sites including a SCHEMA-tier non-VIP site
Evidence Source: live CMS runtime screenshot, hybrid auth (cookie bootstrap), showing the Sites & DR table with 4 real seeded sites — three DATABASE/VIP and one SCHEMA/non-VIP (tenant-demo). Coverage Tier: mocked-GraphQL component tier + this hybrid backend-backed capture. Readiness State: PASS for the admin surface; not a full backend-backed browser e2e.

Tenant admin — "DR & Domain" (CMS /settings/dr)

A site/tenant admin opens http://localhost:23000/settings/dr to manage their own site. Tier and VIP are shown read-only (platform-controlled; to change them, contact the platform). The panel also shows the base URL and the custom-domain DNS-TXT verification state, plus a URL-settings form: choose how the site is reached — a subdomain (slug.platform), a path fallback (platform/slug), or an optional custom domain — and save it.

Note: all four seeded sites currently set their domain to none, so the pending-domain "Verify" affordance is still not seed-exercised; and the per-tenant /settings/dr panel is not captured this pass (see Gaps).

Host Preflight & Deploy Operator Flow New

Backend / CLI surface (no browser). Before pointing the non-VIP read path at any PostgreSQL host, you confirm the host can satisfy the D2 model, then run a single idempotent provisioning script. Evidence here is command transcripts, not screenshots.

1 · Capability preflight — migrate db-doctor

Run against any candidate Postgres to confirm it can run the D2 read path (create a non-superuser app role and have FORCE RLS actually confine it) and whether it has pgvector + Apache AGE for runtime GraphRAG (advisory — their absence does not block D2). db-doctor inspect is read-only; the default mode runs a throwaway role round-trip. Transcript against the governed PG15:

$ migrate db-doctor
ℹ️  [INFO] server-version       PostgreSQL 15.18
✅ [PASS] privileged-role-can-create-app-role
✅ [PASS] app-role-is-non-superuser
✅ [PASS] app-role-non-bypassrls
✅ [PASS] force-rls-confines-app-role
⚠️  [WARN] tls                  NOT using TLS — set sslmode=require for a hosted DB
ℹ️  [INFO] max-connections      max_connections=100
⚠️  [WARN] extension:age        age NOT available — runtime GraphRAG cannot run here
⚠️  [WARN] extension:vector     vector NOT available — runtime GraphRAG cannot run here

✅ HOST OK for the D2 shared-table-RLS read path (advisory WARN items do not block D2).
Evidence Source: real CLI transcript (docs/manual/assets/host-doctor-cli-2026-06-16.txt) against the governed PG15. Coverage Tier: backend-tool-cli / runtime-backed. Readiness State: the host is D2-capable; GraphRAG is advisory-unavailable on this DB (no pgvector/AGE) — exactly the host-selection signal the doctor exists to surface.

2 · Provision — scripts/provision_schema_tier_rls.py

One idempotent command provisions the non-superuser app role, seeds the non-VIP site(s), installs the shared-table RLS, and verifies isolation. Preview the exact plan first with --dry-run (prints commands + a secret-redacted DSN, executes nothing):

$ provision_schema_tier_rls.py --dry-run tenant-demo
D2 shared-table RLS on host=db.example.com … password=*** sslmode=require | app_role=cms_tenant_app app_password=***

--dry-run: the following idempotent steps WOULD run (nothing executed):
  ▶ 1/4 provision non-superuser app role:  migrate provision-app-role
  ▶ 2/4 seed schema-tier site 'tenant-demo': migrate migrate tenant-demo
  ▶ 3/4 install shared-table RLS + grant:    migrate rls-rollout-shared
  ▶ 4/4 verify RLS isolation:                migrate verify-rls tenant-demo

The runtime env contract is documented in .env.schema-tier.example; the routed /mcp public read path turns on only when SCHEMA_TIER_APP_USER is set at backend startup (default-off / reversible). After deploy, a post-deploy /mcp cross-tenant UAT smoke (env MCP_UAT_BASE_URL) confirms isolation against the real URL.

Evidence Source: real CLI transcript (docs/manual/assets/provision-dryrun-cli-2026-06-16.txt). Coverage Tier: backend-tool-cli / runtime-backed locally. Readiness State: the operator flow was runtime-backed on a governed PG15 and has now been applied to live Cloud SQL; public-domain MCP proof remains pending.

Source Manifest

These are the exact inputs used for this manual refresh. RTM.md is traceability context only; spec-local reviews and runtime reports remain proof authority.

SourceHow it was used
.agents/specs/{SPECS,NEXT_STEPS,ISSUE_LOG,RTM}.md2026-06-27 rollup — Cloud Run deployment, live Cloud SQL RLS follow-up, and prior D2/manual evidence. RTM is rollup only, not readiness authority.
.agents/specs/schema-per-tenant-routing-rls/{tasks.md (Slice 8), review.md, adr/ADR-RLS-002-NON-VIP-CANONICAL-D2.md}The D2 (non-VIP) canonical model + the host-preflight / dry-run / UAT-smoke / transaction-pool-safety deliverables documented in the Host Preflight section. RLS is only enforced when the app connects as a non-superuser role.
.agents/specs/tenant-lifecycle-dr-admin/{design-cms-ui.md, requirements.md, tasks.md}DR admin UI shape for the platform Sites & DR table and tenant DR & Domain panel. (No review.md exists for this spec — fell back to the design/requirements/tasks chain, as the guide allows.)
backend/seeds/{platform,naelt,showcase,tenant-demo}-*.jsonCanonical seed/demo/sample data for the 4 seeded sites (the new tenant-demo carries tenancyTier:schema).
docs/manual/assets/*-2026-06-16.{png,txt}9 fresh screenshots + 2 CLI transcripts captured this pass from the running local stack via scripts/capture_manual_2026_06_16.mjs and the migrate CLI.
docs/MANUAL_GENERATION_GUIDE.md + the review guidesManual workflow and source-manifest rules (2026-06-27 note added for Cloud Run/RLS addendum, no new screenshots).
.agents/specs/production-deployment-topology/{review.md,adr/ADR-DEPLOY-003-gcp-cloudrun-cloudsql-cloudflare.md} + infra/terraform/envs/gcp-cloudrun/Production deployment authority: Cloud Run runtime live on run.app, Cloud SQL/Upstash connectivity, declarative Terraform IaC, and live shared-table RLS provisioning/wiring claim boundary.
docs/*FEATURE*.mdNo matching files exist in this repo for this pass (manifest fallback to the spec/report chain, as the guide allows).

Gaps And Changes Since Last Check

Compared against the 2026-06-15 manual pass.

Gaps resolved this pass (2026-06-16)

  • Non-VIP / SCHEMA tier now visually exercised: seeded a fourth site tenant-demo on the SCHEMA (non-VIP) tier, so the DR admin table now shows the SCHEMA badge and the Set VIP (vs Remove VIP) distinction with real data, and the non-VIP D2 public site renders at /site/tenant-demo.
  • Host-preflight operator tooling documented: the new migrate db-doctor capability probe (incl. pgvector/AGE detection) and the provision_schema_tier_rls.py --dry-run plan are now shown with real CLI transcripts.
  • Seed command fixed: scripts/ops.py db seed invoked the old single-file go run cmd/migrate/main.go (broken after the migrate package was split into multiple files); corrected to go run ./cmd/migrate so seeding works again.
  • All public routes still 200 against the real backend, now including /site/tenant-demo (6 routes).

Visual gaps remaining

  • Pending-domain "Verify" + /settings/dr panel: all four seeded sites set domain to none, so the custom-domain pending/Verify state is still not seed-exercised, and the per-tenant DR & Domain panel is not captured this pass. Add a site with a pending custom-domain challenge.
  • Backup / spawn disabled: the backup and spawn actions render disabled (not yet built).
  • GraphRAG-capable host not shown: the governed test DB lacks pgvector/AGE, so db-doctor shows them as advisory WARN; a host with both installed is not captured.
  • Hybrid CMS captures + dev overlay: CMS screenshots use auth-API cookie bootstrap (hybrid), not full browser-login; a Next dev "1 Issue" overlay badge appears in CMS captures (dev overlay, not a product defect).
High

Production target

target_id remains undecided; backend host is not provisioned; DNS/TLS/deployment binding is absent for app/API hostnames.

High

Production visual/SEO/CDN

Current screenshots are local runtime captures; custom-domain browser proof, CDN/cache invalidation, crawler breadth, and UAT remain separate despite Cloud Run being live.

Medium

Public-domain D2/MCP proof

The live Cloud SQL RLS layer is provisioned and wired, but custom-domain /mcp cross-tenant proof still waits for the API certificate and at least two non-VIP tenants.

Medium

DR admin proof tier

The tenant-lifecycle DR admin UI is proven at mocked-GraphQL component tier + this hybrid backend-backed capture — not a full backend-backed browser e2e.

Medium

Browser-login breadth

CMS dashboard / page-builder / DR-admin captures use auth-API cookie injection; hybrid evidence, not full browser-login.

Medium

Live page-save / A2UI apply

The page-builder screenshot shows the seeded Pages surface; live page-save/publish and A2UI agent apply are separate workflow proofs not captured here.