Status: π‘ Works end-to-end, with manual seams Β· Audience: GoBuild staff (sales + ops). Not customer-facing.
The happy path β approve a mock in Showcase, publish it as a live Client org on the pooled hub, attach a custom domain, wire the CRM, ship a PWA β is real and code-grounded below. Two seams need a human: DNS is the Client's job (nothing verifies it for you), and a folder-site's
domain_statusnever auto-promotes pastpending(Β§4). Both are flagged inline withfile:lineevidence.
Badge legend: π’ works Β· π‘ works with a caveat Β· βͺ manual / not automated Β· β broken or missing.
This playbook covers one journey: a prospect looking at a mock site on showcase.gobuild.ca becomes a paying Client with a live Job-running tenant and their own domain. The pieces are the Showcase service (showcase/), the Control Plane / CP (controlplane/, the ops service), and each hub's signed Admin API (app/routers/admin_api.py).
Prospect Showcase (svc-golive) Control Plane (ops) Pooled hub (Admin API)
β browses mock β β β
ββββ /p/{token} ββββββββββββ€ β β
β approves a variant β β β
β [Go live] ββ POST /_provision/golive ββΆ POST /orgs βββββββββββΆ create Organization + owner
β β β POST /orgs/{id}/site/import ββΆ folder-site on {sub}.gobuild.ca
β ββββββ org_id, url, invite ββββ€ β
β [Attach domain] β POST /_provision/domain ββΆ POST /orgs/{id}/site/domain ββΆ custom_domain, status=pending
β (Client points DNS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΆ A/CNAME β hub IP)
β visits theircompany.com β β Caddy: on-demand TLS ββ ask βββΆ GET /internal/tls-allow β 200
β fills estimator ββ POST /e/{token}/lead ββββββββββββββΆ POST /_provision/lead ββββΆ POST /orgs/{id}/leads β CRM Lead
β taps "Install app" βββ manifest + service worker (PWA) served by the hub by Host header
The CP holds the hub signing key, so every mutation funnels through it β the Showcase never talks to a hub directly (controlplane/main.py:150-154). Showcase β CP calls are authed by the forwarded platform-admin cookie (as a bearer) or the svc-golive service token; both are signed with the shared CP_SECRET (controlplane/main.py:206-215).
Work top to bottom. "Who" = who performs the action.
| # | Step | Who | Status | Evidence |
|---|---|---|---|---|
| 1 | Build the mock site as a Showcase prospect (variants, pages, feedback threads) | GoBuild | π’ | showcase/main.py:181-266 |
| 2 | Configure plugins on the prospect: estimator, chat concierge, PWA | GoBuild | π’ | showcase/main.py:373-426 |
| 3 | Client reviews the mock, leaves feedback, approves a variant | Client | π’ | showcase/main.py:287-315 |
| 4 | Hit Go live β creates the Client org + imports the site | GoBuild | π’ | showcase/main.py:624-693 |
| 5 | Send the owner the magic-link invite (set-password) | GoBuild | π‘ | admin_api.py:534-541 |
| 6 | Attach the Client's custom domain | GoBuild | π‘ | showcase/main.py:714-757 |
| 7 | Client points DNS (A/CNAME) at the hub IP | Client | βͺ | admin_api.py:626-631 |
| 8 | Caddy issues TLS on first HTTPS hit (on-demand, gated) | Automatic | π’ | Caddyfile:1-7,98-110; landing_portal.py:237-253 |
| 9 | Estimator leads flow into the live org's CRM | Automatic | π’ | showcase/main.py:443-476 |
| 10 | Client (and their visitors) install the site as a PWA | Client | π’ | landing_portal.py:281-310,388-389 |
Status: π’ Live Β· [Audience: sales]
A prospect is a folder of self-contained HTML variants under showcase/content/, served read-only to the Client at /p/{token} (showcase/main.py:181-187). The Client can leave pin-point feedback and reply on two-way threads; the team drafts variants and a Claude-Code edit brief from that feedback. Nothing here touches a hub β it is pure content + feedback until someone clicks Go live.
Two plugins get carried into the live site later, so set them up now:
_plugin_config(..., "chat"), showcase/main.py:334-343)._plugin_config(..., "pwa")).Status: π’ Live Β· [Audience: ops]
Clicking Go live on the admin prospect page (showcase/main.py:624) reads the approved variant's pages off disk, then POSTs the whole bundle to the CP at /_provision/golive (CP_GOLIVE_URL, default http://ops:8001/_provision/golive, showcase/main.py:22,665-668).
The CP does two hub calls in order (controlplane/main.py:148-198):
hubclient.create_org β hub POST /orgs (admin_api.py:484-541). Creates the Organization (always isolation=pooled from Showcase), its first owner User, and seeds day-one cost codes + starter templates. Returns a magic-link invite URL (owner_invite_url) β a set-password link, not a temp password (admin_api.py:534-538).hubclient.import_site β hub POST /orgs/{id}/site/import (admin_api.py:547-623). Imports the approved pages as a folder-site on {subdomain}.gobuild.ca. Pages serve pixel-identical raw HTML; inter-page ./slug.html links are rewritten to folder routes (admin_api.py:573-579). The chat + PWA plugin config rides along in the same payload (admin_api.py:594-606).On success the Showcase records the prospect β live org link (org_id, subdomain, url) in the golive plugin row (showcase/main.py:682-690) β this is what lets later estimator leads find the right CRM.
β οΈ Subdomain/substrate collision (fixed, but know it): the org's themed-site substrate auto-slug (from the org name) can collide with the chosen public subdomain; import moves the substrate to a private
{sub}-hubslug so the folder-site owns the host (admin_api.py:582-589). This was the bug fixed in commit1404539.
Status: π‘ Works, but DNS + status promotion are manual Β· [Audience: ops]
Two independent things have to happen for theircompany.com to go live: we record the domain, and the Client points DNS at us.
The Attach domain form (showcase/main.py:714) POSTs to the CP /_provision/domain (CP_DOMAIN_URL), which calls hub POST /orgs/{id}/site/domain (admin_api.py:626-656). This just records custom_domain on the folder and sets domain_status = "pending". Guards: it must look like a domain (contains a dot) and can't already be attached elsewhere (409).
The Client adds an A/CNAME record for their domain to the hub's IP. Nothing in the code checks this β set_site_domain explicitly notes the site "goes live once the client points DNS at the hub and Caddy issues a cert" (admin_api.py:629-631). There is no DNS-verification poller and no email nudge; the ops person confirms by hitting the domain.
Caddy's global on_demand_tls asks the hub before issuing any cert, so Let's Encrypt can't be abused for arbitrary hostnames (Caddyfile:1-7). Partner custom domains hit the catch-all https:// block with tls { on_demand } (Caddyfile:98-110). The ask oracle is GET /internal/tls-allow (landing_portal.py:237-253): it returns 200 only for a real tenant subdomain, a registered partner custom_domain, or a known {label}.gobuild.ca folder/landing site. Because resolve_folder_host matches on custom_domain regardless of status (landing_portal.py:89-107), the site serves and gets a cert as soon as DNS resolves β no manual Caddy edit, no redeploy.
β οΈ
domain_statusnever leavespendingfor folder-sites. The only code that promotes a status to"live"/"secured"is the themed website-builder path (website.py:515), not the folder-site import path. The folder canonical-URL helper only switches to the custom domain when status islive/secured(custom_pages.py:226-227), so canonical / OG / sitemap URLs keep pointing at{sub}.gobuild.caeven after the custom domain is serving. The site works; its SEO metadata is stale. Treat this as a known gap, not a blocker.
Status: π’ Live Β· [Audience: sales]
Once the prospect is linked to a live org (step 2), the estimator on the mock becomes a real lead source. A visitor submitting /e/{token}/lead (showcase/main.py:443-476) always logs the lead as Showcase feedback, and β if the prospect has gone live β also forwards it to the CP /_provision/lead (CP_LEAD_URL), authed with the svc-golive service token (showcase/main.py:465-473). The CP calls hub POST /orgs/{id}/leads (admin_api.py:659+), creating a Lead + Person + lead_created follow-up trigger β same shape as the org's own on-site forms.
Best-effort by design: the forward is wrapped so a CRM failure never fails the visitor (
showcase/main.py:474-475). A dropped forward is silent β the lead still exists in Showcase feedback, but won't appear in the Client's CRM. If a Client reports "missing leads," check the CP/hub logs, not just the CRM.
Status: π’ Live Β· [Audience: sales + Client]
If the PWA plugin was on at go-live, import_site stores pwa: {on, name, theme, monogram} on the org (admin_api.py:600-606). The hub then serves, by Host header, a per-site manifest.webmanifest + a scoped service worker with an auto-branded monogram icon (landing_portal.py:281-310), and injects a π² Install app button via beforeinstallprompt (landing_portal.py:388-389). The Client and their site visitors can add it to a home screen. Admins can also hand out a scannable install link β the Showcase renders a QR of the live URL (showcase/main.py:696-711, commit 719ba1e).
| Client | GoBuild |
|---|---|
| Reviews the mock, leaves feedback, approves a variant | Builds the mock + variants in Showcase |
| Points DNS (A/CNAME) at the hub IP β the one hard manual step | Configures plugins (chat, PWA, estimator) |
| Sets their password via the magic-link invite | Clicks Go live (org + site import) |
| Installs the PWA; runs their business (Jobs, CRM) | Attaches the custom domain, confirms TLS issued |
| β | Confirms leads land in the CRM |
If this is a siloed client (own droplet, own third-party keys) rather than a pooled Showcase go-live, the domain/DNS/secret-rotation runbook is separate: see docs/onboarding_README.md and docs/client_onboarding_checklist.csv (10 phases, Done column).
| Failure | Symptom | Cause / evidence |
|---|---|---|
| No pooled hub registered | Go live returns 400 "No pooled hubβ¦" | controlplane/main.py:171-173 |
| Slug already taken | 409 on org create | admin_api.py:513-514 |
| Subdomain already taken | 409 on site import | admin_api.py:567-569 |
| Org created but import fails | Response has org_id but no site β orphaned org |
controlplane/main.py:194-195 |
| Showcase not logged in as platform admin | CP returns 401 (forwarded cookie invalid) | controlplane/main.py:155-159 |
CP_SECRET mismatch (Showcase vs CP) |
All /_provision/* calls 401 |
shared-secret auth, main.py:206-214 |
| Client never points DNS | Custom domain never resolves; no cert ever issues | manual step, unverified β admin_api.py:629-631 |
| DNS wrong / not propagated | tls-allow returns 404 β Caddy won't issue cert |
landing_portal.py:237-253 |
| Domain attached but stale SEO URLs | Canonical/OG still show {sub}.gobuild.ca |
domain_status stuck pending β custom_pages.py:226-227 |
| Estimator lead not in CRM | Lead in Showcase feedback but not the org's CRM | best-effort forward swallows errors β showcase/main.py:474-475 |
| PWA not installable | No install button / manifest 404 | PWA plugin was off at go-live β admin_api.py:600-606 |
ops app, the /_provision/* service endpoints, and the fleet.docs/onboarding_README.md + docs/client_onboarding_checklist.csv β the siloed (own-droplet) clone runbook.