Skip to content

Prototype To Client Promotion

Prototype-to-Client Tenant Promotion Procedural Runbook

Source Issue: #578
Status: uningested (Point-in-time write-up pending /docs distill alignment)
Target Canonical Spec: docs/runbooks/PROTOTYPE_TO_CLIENT_PROMOTION.md


1. Purpose, Architecture & Paradigm Shift

In SiteSwarm, client acquisition transitions from outbound prospecting directly into engineering through the Pre-Awareness Prototype Lifecycle (codified under Epic #575 and PROTOTYPE_WORKFLOW_AND_PROGRESSIVE_ESCALATION.md). Rather than presenting static mockups or pitch decks, prospective business owners experience a sub-second, interactive edge prototype built from verified business facts at apps/proto-<slug>/.

When a prospect executes a service agreement and pays their onboarding deposit, we do not throw away the prototype, nor do we re-scaffold a greenfield application from scratch. Instead, this runbook defines the procedural, auditable promotion workflow to elevate a signed prospect prototype into a first-class production client tenant workspace at apps/<client-prefix>-<site-slug>/.

flowchart TD
subgraph PrePromotion["1. Prototype Origin & Discovery Intake"]
ProtoApp["Phase 1-3 Prototype\napps/proto-<slug>"] --> IntakeCheck["Post-Discovery Intake Checklist\nCLIENT_INTAKE_CHECKLIST.md §2.1 & §5.3"]
IntakeCheck --> RatifyVibe["Ratify Builder Liberties &\nFinalize VIBE.md Contract"]
end
subgraph GitPromotion["2. Git & Workspace Synchronization"]
RatifyVibe --> GitMv["In-Place Renaming (git mv)\nPreserve Commit Pedigree & Blame"]
GitMv --> SyncManifests["Workspace Synchronization\npackage.json, root pnpm-workspace.yaml,\nswarm.config.ts & wrangler.jsonc"]
end
subgraph CloudflareInfra["3. Cloudflare Tenant Infrastructure"]
SyncManifests --> ProvD1["Dedicated D1 SQLite DB\nMigrate schema.sql & seed.sql"]
ProvD1 --> ProvR2["Dedicated R2 Media Bucket\nIngest CDN Hotlinks -> Client R2"]
ProvR2 --> WireIngress["Platforms Custom Hostname Ingress\nDual Worker Route Binding"]
end
subgraph ProductionReady["4. Zero-Lockout & Quality Verification"]
WireIngress --> ExportCheck["Zero-Lockout Portability Verification\n(swarm export-client / wrangler d1 export)"]
ExportCheck --> MachineGate["Tier 1 & Tier 2 Quality Gates\npnpm run check & pnpm run lint:arch"]
MachineGate --> LiveTenant["Live Production Client Tenant\napps/<client-prefix>-<site-slug>"]
end

Why a Procedural Runbook Over Black-Box Automation

Tenant promotion involves high-stakes infrastructure boundaries: renaming Git directories, binding production Cloudflare D1 databases, provisioning dedicated R2 storage, migrating external CDN assets, and wiring live DNS hostnames without causing downtime or email (MX) disruption. Rather than relying on a rigid, over-constrained compiled CLI, this runbook provides an auditable, step-by-step procedure paired with an autonomous agent guidance recipe that engineers and agent workflows can execute deterministically.


2. Pre-Promotion Verification & Lineage Checklist

Before executing tenant promotion, the engineer or agent must verify the following prerequisites:

  • Commercial Gate: Master Services Agreement (MSA) signed, deposit received, and tenant billing schedule locked.
  • Discovery Synthesis Completed: Post-discovery intake checklist (CLIENT_INTAKE_CHECKLIST.md) completed, including Know-Your-Customer (KYC) identity records, authorized technical DNS contact, and domain inventory.
  • Prototype Working Tree Clean: Prototype directory apps/proto-<slug>/ exists on main (or a dedicated feature branch) with a clean git working tree (git status shows 0 uncommitted changes).
  • Tenant Naming Agreement: Target client prefix and site slug confirmed per monorepo convention: apps/<client-prefix>-<site-slug> (e.g., apps/clk-construction, apps/rcc-stjohn).

3. Step-by-Step Tenant Promotion Procedure

Step 1: In-Place Git Directory Renaming (Preserving Authorship)

[!CAUTION] Zero Sibling Copying & History Preservation: Never use cp -r or manual directory copying to duplicate the prototype into a new client folder. Copying breaks git blame, severs commit history, obscures architectural decisions made during prototyping, and causes file duplication. Promotion must always occur via git mv.

Execute the in-place Git move from the monorepo root:

Terminal window
# 1. Ensure you are on the target promotion branch
git checkout -b feature/tenant-promote-<client-prefix>-<site-slug>
# 2. Rename the directory using git mv
git mv apps/proto-<slug> apps/<client-prefix>-<site-slug>
# 3. Stage the directory rename
git commit -m "chore(tenants): promote proto-<slug> to <client-prefix>-<site-slug> [lineage: #578]"

Git automatically tracks this operation as a file rename (preserving 100% of the commit log, line-by-line blame, and original author attribution).


Step 2: Workspace Manifest & Configuration Synchronization

After moving the directory, synchronize the monorepo package graph, root tooling, and app manifests.

2.1 Update App package.json

Edit apps/<client-prefix>-<site-slug>/package.json to reflect the new client package identity:

{
"name": "@siteswarm/app-<client-prefix>-<site-slug>",
"description": "Production tenant frontend for <Client Brand Name>",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "wrangler dev",
"check": "astro check"
}
}

2.2 Verify Root Workspace Registration

The root pnpm-workspace.yaml includes 'apps/*'. Verify that pnpm recognizes the promoted package:

Terminal window
# Re-link the workspace graph
pnpm install

2.3 Register Root Convenience Scripts in package.json

Add development and build shortcut scripts to the root package.json under "scripts":

{
"scripts": {
"dev:<client-prefix>-<site-slug>": "pnpm --filter=@siteswarm/app-<client-prefix>-<site-slug> run dev",
"build:<client-prefix>-<site-slug>": "pnpm --filter=@siteswarm/app-<client-prefix>-<site-slug> run build"
}
}

2.4 Synchronize App Governance Manifest (swarm.config.ts)

Create or update apps/<client-prefix>-<site-slug>/swarm.config.ts using @siteswarm/governance:

import { defineAppConfig } from "@siteswarm/governance";
export default defineAppConfig({
appId: "<client-prefix>-<site-slug>",
name: "<Client Official Brand / DBA Name>",
brand: {
primaryColor: "#<HEX>",
accentColor: "#<HEX>",
fontFamilyHeading: "'<Display Font>', serif",
fontFamilyBody: "'<Body Font>', sans-serif",
logoAssetPath: "public/favicon.svg",
},
deployment: {
productionDomain: "www.<client-domain>.com",
stagingDomain: "<client-prefix>-<site-slug>-staging.<platform-domain>",
cloudPlatform: "cloudflare",
},
capabilities: {
health: {
type: "horizontal",
version: "1.0.0",
targets: ["src/pages/api/health.ts"],
options: {
endpoint: "/api/health",
},
},
"dynamic-seo": {
type: "horizontal",
version: "1.0.0",
targets: ["src/layouts/BaseLayout.astro"],
options: {
schemaType: "LocalBusiness",
enableBreadcrumbs: true,
},
},
critique: {
type: "horizontal",
version: "0.1.0",
targets: ["src/layouts/BaseLayout.astro"],
options: {
enabledEnvironments: ["development", "preview", "staging"],
requireAuth: false,
},
},
},
});

2.5 Update Cloudflare Worker Configuration (wrangler.jsonc)

Update apps/<client-prefix>-<site-slug>/wrangler.jsonc:

  • Set the top-level Worker name: "name": "<client-prefix>-<site-slug>"
  • Configure env.staging and env.production blocks
  • Set "APP_ID": "<client-prefix>-<site-slug>" in vars
  • Disable session KV: ensure Astro adapter declares session: false in astro.config.mjs

Step 3: Dedicated Cloudflare Infrastructure Provisioning

SiteSwarm adheres strictly to tenant isolation. Each client tenant receives dedicated Cloudflare D1 SQLite database(s) and dedicated R2 media bucket(s). Multi-tenant shared tables are strictly forbidden for client business content.

3.1 Provision Dedicated Cloudflare D1 Databases

Provision separate SQLite databases for production and staging:

Terminal window
# Provision production D1 database
wrangler d1 create <client-prefix>-<site-slug>-prod-d1
# Provision staging D1 database
wrangler d1 create <client-prefix>-<site-slug>-staging-d1

Record the output database_id UUIDs returned by Wrangler.

3.2 Migrate Prototype Schema & Seed Data

If the prototype reached Phase 3 escalation and generated a relational schema (schema.sql) and verified seed data (seed.sql):

Terminal window
# 1. Apply schema to staging and production
wrangler d1 execute <client-prefix>-<site-slug>-staging-d1 --remote --file=./schema.sql
wrangler d1 execute <client-prefix>-<site-slug>-prod-d1 --remote --file=./schema.sql
# 2. Seed initial verified business records into staging and production
wrangler d1 execute <client-prefix>-<site-slug>-staging-d1 --remote --file=./seed.sql
wrangler d1 execute <client-prefix>-<site-slug>-prod-d1 --remote --file=./seed.sql

3.3 Provision Dedicated Cloudflare R2 Media Buckets

Provision isolated object storage buckets for client media uploads:

Terminal window
# Provision production R2 bucket
wrangler r2 bucket create <client-prefix>-<site-slug>-media-prod
# Provision staging R2 bucket
wrangler r2 bucket create <client-prefix>-<site-slug>-media-staging

3.4 Bind Infrastructure in wrangler.jsonc

In the client app (or Worker B CMS if utilizing EmDash split architecture per ADR-0003), bind the provisioned databases and buckets:

{
"d1_databases": [
{
"binding": "DB",
"database_name": "<client-prefix>-<site-slug>-prod-d1",
"database_id": "<GENERATED_PROD_D1_UUID>"
}
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "<client-prefix>-<site-slug>-media-prod"
}
],
"env": {
"staging": {
"d1_databases": [
{
"binding": "DB",
"database_name": "<client-prefix>-<site-slug>-staging-d1",
"database_id": "<GENERATED_STAGING_D1_UUID>"
}
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "<client-prefix>-<site-slug>-media-staging"
}
]
}
}
}

Step 4: Remote Asset Migration Checklist & R2 Ingestion

In the prototype phase, media assets may have been hotlinked from external CDNs (Unsplash, prospect web presences, Yelp, or social media) with aspect-ratio SVG fallbacks per PROTO_BASELINE_REQUIREMENTS.md. In production client tenants, third-party hotlinking is strictly prohibited. All external media must be audited, ingested into the tenant’s dedicated R2 bucket, and served from edge storage.

4.1 Execute External Asset Audit

Audit all external image and media URLs across the tenant application:

Terminal window
grep -rnE 'https?://[^"\x27\s>]+\.(jpg|jpeg|png|webp|svg|avif|gif)' apps/<client-prefix>-<site-slug>/src/

Also check prospect.json and component props for remote image links.

4.2 Ingest Assets into Client R2 Storage

For each external asset:

  1. Download the asset locally:
    Terminal window
    curl -sSL "<remote_cdn_url>" -o /tmp/asset-name.webp
  2. Upload the asset directly to the client’s dedicated R2 bucket:
    Terminal window
    wrangler r2 object put <client-prefix>-<site-slug>-media-prod/uploads/<asset-name>.webp --file=/tmp/asset-name.webp
    wrangler r2 object put <client-prefix>-<site-slug>-media-staging/uploads/<asset-name>.webp --file=/tmp/asset-name.webp
  3. Update the markup or CMS seed record to reference the tenant-hosted media path (e.g., /media/uploads/<asset-name>.webp or client media CDN domain).

Re-run the audit to ensure zero external media URLs remain in production code:

Terminal window
# Must return 0 lines matching external unmanaged CDNs
grep -rnE 'https?://(images\.unsplash\.com|cdn\.[a-z0-9]+\.com)' apps/<client-prefix>-<site-slug>/src/

Step 5: Cloudflare for Platforms Custom Domain Ingress Connection

Client domain onboarding follows CLOUDFLARE_FOR_PLATFORMS_AND_DOMAIN_INGRESS.md to route traffic to the client’s Worker without requiring domain transfer or nameserver delegation.

flowchart LR
ClientDNS["Client DNS Registrar\n(GoDaddy, Route 53, Namecheap)\nCNAME www -> <client>-ingress.<platform-domain>"] --> PlatformZone["SiteSwarm Platform Zone\nCustom Hostname: www.<client-domain>.com\nAuto TLS & DCV Validation"]
PlatformZone --> DualRoute{"Dual Worker Route Binding"}
DualRoute -- "Grey Cloud / Non-CF" --> CustomRoute["Route: www.<client-domain>.com/*"]
DualRoute -- "Orange Cloud / O2O" --> IngressRoute["Route: <client>-ingress.<platform-domain>/*"]
CustomRoute --> ClientWorker["Worker A: apps/<client-prefix>-<site-slug>"]
IngressRoute --> ClientWorker

5.1 Create Dedicated CNAME Target on Platform Zone

On the SiteSwarm platform Cloudflare zone:

  • Create a proxied (Orange Cloud) CNAME record: <client-prefix>-<site-slug>-ingress.<platform-domain> $\to$ fallback.<platform-domain>

5.2 Register Custom Hostname with Zero-Downtime Pre-Validation

  1. In SiteSwarm’s Cloudflare Zone under Custom Hostnames, register www.<client-domain>.com.
  2. Cloudflare outputs a Domain Control Validation (DCV) TXT record (e.g., _acme-challenge.www.<client-domain>.com).
  3. Have the client add the DCV TXT record at their existing registrar while their old website remains live.
  4. Verify that SSL Status transitions to Active before making any traffic changes.

5.3 Configure Dual Worker Route Binding in wrangler.jsonc

To ensure total immunity against client proxy toggles (Grey Cloud DNS-Only vs. Orange-to-Orange O2O), bind both routes in apps/<client-prefix>-<site-slug>/wrangler.jsonc:

{
"env": {
"staging": {
"routes": [
{
"pattern": "<client-prefix>-<site-slug>-staging.<platform-domain>/*",
"zone_name": "<platform-domain>"
}
]
},
"production": {
"routes": [
{
"pattern": "www.<client-domain>.com/*",
"custom_domain": true
},
{
"pattern": "<client-prefix>-<site-slug>-ingress.<platform-domain>/*",
"zone_name": "<platform-domain>"
}
]
}
}
}

5.4 Execute Production DNS Cutover

  1. Have the client point their www record as a CNAME pointing to <client-prefix>-<site-slug>-ingress.<platform-domain>.
  2. For the zone apex (@ / clientbusiness.com):
    • If using a registrar supporting ALIAS / ANAME or CNAME flattening (Cloudflare, Route 53, Porkbun), flatten @ to <client-prefix>-<site-slug>-ingress.<platform-domain>.
    • If on GoDaddy / Namecheap, configure registrar root redirect: https://clientbusiness.com $\to$ https://www.clientbusiness.com.
  3. Verify that the client’s MX, SPF, DKIM, and DMARC records remain untouched at their authoritative DNS registrar.

4. Zero-Lockout Data Portability & Clean Export Guarantee (#207)

SiteSwarm’s core contract is that clients retain 100% ownership of their data. Promoted client tenants must never be locked into proprietary schemas or inseparable multi-tenant databases.

Verification of Data Portability

Upon promotion, verify that the tenant can be cleanly exported under CLIENT_OFFBOARDING.md:

  1. Atomic D1 SQLite Export: Verify that a clean, single-tenant SQLite export can be produced without manual data slicing:
    Terminal window
    wrangler d1 export <client-prefix>-<site-slug>-prod-d1 --remote --output=/tmp/<client>-export.sql
    head -n 20 /tmp/<client>-export.sql
  2. Dedicated R2 Media Archive: Verify that the media bucket can be synced or exported directly:
    Terminal window
    wrangler r2 object list <client-prefix>-<site-slug>-media-prod
  3. Execution Time SLA: Data export must execute in under 5 minutes with zero schema refactoring. When pnpm swarm export-client <client-slug> (codified under Issue #204 & #207) is executed, the promoted tenant produces a self-contained archive bundle with SQLite database and all media assets.

5. Quality Verification & Quality Gate Runbook

Before deploying the promoted client tenant to production or closing the promotion milestone, execute the full quality verification suite:

1. Monorepo Architectural Linting (Tier 1 Machine Gate)

Terminal window
# Verify monorepo governance and app types
pnpm run check
pnpm run lint:arch

Must pass with 0 errors.

Terminal window
npx tsx .agents/skills/docs/scripts/docs.ts audit

Must pass with exit code 0 and verify zero broken relative links.

3. Anti-Leakage & Privacy Audit

Ensure no internal platform terminology or prototype lineage artifacts leak into public visitor markup:

Terminal window
grep -riE "SiteSwarm|EmDash|Worker B|apps/proto-|prospect\.json" apps/<client-prefix>-<site-slug>/src/

Must return 0 matches in public visitor templates.

4. Mobile Core Web Vitals Audit

Deploy to preview/staging Worker and audit via Lighthouse:

  • Performance: $\ge$ 98 (target 100/100)
  • Cumulative Layout Shift (CLS): 0.000
  • Time to First Byte (TTFB): < 150ms

6. Anti-Patterns & Operational Guardrails

Anti-Pattern Severity Impact Corrective Mandate
Directory Copying (cp -r) Critical Destroys git commit lineage and author attribution. Always use git mv apps/proto-<slug> apps/<client-prefix>-<site-slug>.
Multi-Tenant Database Sharing Critical Violates Zero-Lockout Data Portability; exposes cross-client data risks. Provision dedicated D1 database and R2 bucket per tenant.
Unmanaged CDN Hotlinks High Subject to external CDN rate limits, 404s, CORS failures, and link rot. Ingest all media into tenant R2 bucket during Step 4.
Single-Route Binding High Site breaks if client flips Orange Cloud / Grey Cloud toggle in their DNS. Always bind dual routes (www.<domain>/* + <client>-ingress.<platform-domain>/*).
Nameserver Hijacking High Breaks client’s existing corporate email (MX records) during onboarding. Use Cloudflare for Platforms Custom Hostnames; never migrate client nameservers.
Orphaned Prototype Preview Workers Medium Unused Cloudflare Workers accumulating on platform account. Decommission prototype worker script (wrangler delete proto-<slug>) once production tenant is active.

🔍 Founder Trimming & Distillation Notes

Notes for distillation into canonical docs/runbooks/PROTOTYPE_TO_CLIENT_PROMOTION.md:

  • When the /prototype promote automation recipe (#444) is implemented, the step-by-step CLI commands can be paired with interactive prompts while retaining manual shell fallbacks.
  • The dual-route binding example is synchronized with docs/architecture/CLOUDFLARE_FOR_PLATFORMS_AND_DOMAIN_INGRESS.md and requires no divergent configuration.
  • Asset ingestion can integrate with the automated media ingestion pipeline (#599).