Status: π’ Live & actively developed. Standalone FastAPI service, real content for
6 prospects, working feedback capture, and end-to-end lead + go-live wiring into the
control plane. Last touched in the most recent commits onmain(PWA install-link,
custom-domain attach, estimatorβCRM routing).What it is in one line: GoBuild hand-builds a mock website for a sales prospect,
shares a public link, the prospect browses it and leaves feedback; staff turn that
feedback into a build brief and, on a yes, publish the chosen design as the prospect's
real live site.
Legend: π’ live / working Β· π‘ partial or preview-only Β· βͺ dormant / not wired Β· β broken
Audience tags: π· GoBuild staff (author/admin) Β· π§βπΌ Prospect (public, no login) Β· π οΈ Platform/ops
Source of truth (all paths absolute):
/home/construction-proj/showcase/main.py (804 lines), /home/construction-proj/showcase/models.py/home/construction-proj/showcase/templates/ (home, viewer, estimator, admin, admin_prospect, login)/home/construction-proj/showcase/content/ (mock HTML + manifest.json, gitignored)/home/construction-proj/docker-compose.prod.yml (showcase service), /home/construction-proj/Caddyfile/home/construction-proj/controlplane/main.py (/_provision/lead, line 201)The Showcase is a separate app from the hub β its own FastAPI application
(showcase.main:app) running as the showcase service on port 8002, served at
showcase.gobuild.ca (and showcase.dev.gobuild.ca on the dev droplet). It is not
part of the tenant hub; it only shares the hub image and the control-plane database.
It exists to run the top of the sales funnel: rather than pitch a prospect with a
slide deck, GoBuild builds them a real, browsable mock website (several design
variants), sends a single public link, and lets the prospect click through and react in
place. The captured reactions become an AI-generated build brief, and if the prospect
says yes, the same mock is promoted straight to a live tenant site.
This is now GoBuild's answer to "website builder." The old in-hub website-builder
concept was retired; the Showcase is the productized replacement β mock sites are
hand-authored by the team, not assembled by the client in a visual editor.
β οΈ Caveat / needs owner confirmation: the hub still ships awebsite.pyrouter and
templates/website_dashboard.html/website_custom_edit.html/website_domain.html
(see/home/construction-proj/app/routers/website.py). So "removed from the hub" is
partly true (no client-facing builder) but website-management surfaces still exist.
Confirm exactly what was removed vs. what remains. (See open questions.)
Scale today: 6 prospects Γ 3 design variants each in the manifest
(hillwood, malamar, mt-construction, petvin, sirois, cresmark).
| Audience | Access | What they do |
|---|---|---|
| π§βπΌ Prospect (public) | Share link /p/{token} β no login |
Browse the mock variants, favorite one, react, drop pinned comments, optionally use the estimator |
| π· GoBuild staff (author) | Filesystem access to showcase/content/ |
Hand-author the mock HTML + edit manifest.json |
| π· GoBuild admin | /admin, login via platform_admins (god-mode logins) |
Read feedback, generate the build brief, reply on threads, toggle plugins, publish (go-live), attach domains |
| π οΈ Platform/ops | control-plane (ops/CP droplet) |
Receives go-live / lead / domain provisioning calls from the Showcase |
Prospect identity is just the unguessable share token in the URL
(manifest.json β prospect.token, e.g. hillwood-260981375f). There is no prospect
account; a commenter types a name (defaults to "Guest", main.py:307).
Entry point: GET /p/{token} (main.py:181) renders viewer.html. The token is
resolved against the manifest (_by_token, main.py:102); unknown token β 404.
GET /m/{token}/{variant}/{page} (main.py:231), which serves the self-contained/page/x and x.html) to /m/β¦ routes so the mock'smain.py:247-250);_widget_script, main.py:189)._nav_order / _ordered, main.py:114-145) rather than manifest order.POST /p/{token}/feedback (main.py:287).kind values (see models.py:22):
comment β free-text note on a page/variantreaction β love | like | passfavorite β "this is the variant I want"pin β a comment anchored to an (x, y) spot on the page (px/py, fractions 0β1)role="client" (main.py:307).GET /p/{token}/feedback (main.py:268) returns the JSON list,role="team") so the prospect sees GoBuild's responses inline.Optional visitor surface: the estimator at GET /e/{token} (main.py:428) β a
standalone budget-checker widget (project type β size/finish β ballpark + timeline) that
captures a lead. See Β§5.6.
Auth reuses the god-mode platform_admins table in the shared control-plane DB β no
separate showcase accounts.
POST /admin/login (main.py:784): looks up PlatformAdmin by email,security.verify_password, sets a signed sc_session cookie (httponly, secure,_admin() (main.py:481) verifies the cookie on every admin route and checksis_active.GET /admin (main.py:492): lists prospects from the manifest with aGET /admin/p/{key} (main.py:536, admin_prospect.html): showsPOST /admin/p/{key}/brief (main.py:608): _generate_briefmain.py:550) feeds all feedback (favorites, reactions, comments, pinned notes with ashowcase/content/β¦ to edit. This brief is meant to be handed to ClaudePOST /admin/p/{key}/reply (main.py:760): posts a role="team"GET/POST /p/{token}/plugins[...] (main.py:373, 403): enable/estimator, chat, pwa per prospect (stored in showcase_plugin).POST /admin/p/{key}/golive (main.py:624): see Β§5.6.POST /admin/p/{key}/domain (main.py:714) and QR PNGGET /admin/qr (main.py:696): post-go-live conveniences (attach the client's domain,Feedback + plugin state live in the CP DB tables showcase_feedback and
showcase_plugin (models.py). Only feedback/config is in the DB; the mock HTML is
on disk.
Mock sites are hand-authored self-contained HTML files on disk under
/home/construction-proj/showcase/content/, organized content/{prospect}/{variant}/{page}.html,
and indexed by content/manifest.json. The manifest is the registry the app reads β
it maps each prospect β token β variants β pages β file path:
{ "key": "hillwood", "name": "Hillwood Homes", "token": "hillwood-260981375f",
"variants": [ { "key": "heritage-forest", "name": "Heritage Forest",
"pages": [ { "key": "hw-her-home", "name": "Home",
"file": "hillwood/heritage-forest/hw-her-home.html" } ] } ] }
showcase/content/ tree is gitignored (.gitignore:91) β mock content isdocker-compose.prod.yml:238)._brand_color, main.py:52).Wired end to end. Two lead-related flows:
A. Estimator lead capture β POST /e/{token}/lead (main.py:443):
showcase_feedback row (kind="lead", page="estimator")main.py:457).golive plugin row for the prospect's org_id (main.py:461),CP_LEAD_URL (default http://ops:8001/_provision/lead) signed withsvc-golive (security.sign_session('svc-golive'), main.py:472).main.py:474).The CP side β POST /_provision/lead (controlplane/main.py:201): authenticates the
svc-golive token (or a platform admin), finds the pooled hub FleetInstance, and calls
hubclient.create_lead(...) β hub /_admin/orgs/{org_id}/leads β hub router
POST /orgs/{org_id}/leads (app/routers/admin_api.py:659). So the visitor never
authenticates β the Showcase server does, on their behalf.
B. Go-live (mock β real tenant site) β POST /admin/p/{key}/golive (main.py:624):
reads the chosen variant's pages off disk, POSTs them to CP_GOLIVE_URL
(/_provision/golive) which creates the org on the pooled hub and imports the site as a
folder-site. It carries the chat plugin (seeds the concierge KB from the mock copy via
Gemini, _seed_kb, main.py:345) and the PWA plugin (branded installable icon). On
success it writes the golive plugin row (org_id, subdomain, url) β which is what
later lets estimator leads route to the CRM (flow A step 2). The golive plugin key is
intentionally not in the client-settable PLUGIN_KEYS (main.py:46); it's written
only by the go-live handler.
π‘ Preview-only surfaces to be aware of: the injected chat bubble in the mock is
a static "Preview" panel, not a working AI chat (_widget_script,main.py:202-212);
the PWA pill is a visual badge only. Both become real only after go-live, on the
live tenant site. This is by design, but easy to mistake for a live feature inside the
mock.
showcase in /home/construction-proj/docker-compose.prod.yml:228.image: keystone-hub:latest) but runs a different entrypoint:uvicorn showcase.main:app --host 0.0.0.0 --port 8002.Caddyfile:56 β {$SHOWCASE_DOMAIN:showcase.gobuild.ca} reverse-proxiesshowcase:8002. Dev droplet uses showcase.dev.gobuild.ca via SHOWCASE_DOMAIN.cp_data volume at /app/cp-data (the shared control-planeCP_DATABASE_URLsqlite:////app/cp-data/cp.db) and CP_SECRET for session signing../showcase/content β /app/showcase/contentdocker-compose.prod.yml:238) so authored mocks show up without a rebuild.CP_GOLIVE_URL, CP_LEAD_URL, CP_DOMAIN_URLhttp://ops:8001/_provision/*; overridden in prod to reach the CPmain.py:22-24). HUB_PUBLIC_IP (main.py:25) is where clientsGET /healthz (main.py:169).β οΈ Config coupling to flag: the runtime migration in
lifespan(main.py:148-162)
uses SQLite-onlyPRAGMA table_infoto add therole/px/pycolumns. It's wrapped in
a baretry/except, so on a non-SQLiteCP_DATABASE_URLit would silently no-op β the
app assumes the CP DB is SQLite (which today it is). Note if the CP DB ever moves to
Postgres.
| Item | Status | Evidence |
|---|---|---|
| Chat bubble inside the mock | π‘ preview-only static panel | main.py:202-212 (textContent/innerHTML static "Preview") |
| PWA install pill inside the mock | π‘ visual badge only | main.py:213-222 |
golive plugin |
π’ internal, not client-settable (by design) | not in PLUGIN_KEYS (main.py:46); written only in admin_golive (main.py:683) |
| Runtime column migration | π‘ SQLite-only, silent no-op elsewhere | main.py:152-162 (PRAGMA table_info) |
| Hub "website builder" retirement | βͺ ambiguous β router/templates still present | /home/construction-proj/app/routers/website.py, templates/website_dashboard.html |
| Mock authoring pipeline | βͺ out of app / manual | content gitignored (.gitignore:91); no generator in showcase/ |