Status: 🟡 Works end-to-end, with manual seams · Audience: GoBuild staff (sales + ops) running onboarding — plus the sections marked [Client], which are safe to share with the new Client.
This is the whole journey from signed contract to trained-and-running — broader than Go-live, which is only the publish-the-site step. The org, owner, entitlements, and staff paths are all real Admin-API endpoints (evidence inline). The seams that still need a person: data import has no tooling (§6), the owner magic-link is generated but not auto-emailed (§4), and there is no self-serve — a GoBuild operator drives every step.
Badge legend: 🟢 works · 🟡 works with a caveat · ⚪ manual / not automated · ❌ broken or missing.
House terms: a Client is the builder company we onboard (a tenant Organization). A Job is a project inside their account. Their customers/contacts are also called Clients inside the product; where it's ambiguous below we say "the builder's Clients."
Sale closed
│
├─▶ 0. Decide isolation: pooled (Starter/Pro) ──▶ create org on a shared hub
│ siloed (Enterprise) ──▶ clone a droplet (separate runbook)
│
├─▶ 1. Provision / create the Organization (Admin API POST /orgs)
├─▶ 2. Set entitlements — which Command Centers (tier default + CP feature editor)
├─▶ 3. Invite the owner via magic link (owner_invite_url)
├─▶ 4. Add staff + assign roles (POST /orgs/{id}/users, or in-app invite)
├─▶ 5. Load data — builder's Clients / Jobs / cost codes (⚠ mostly manual)
├─▶ 6. Connect integrations (QBO, Twilio, Google, Meta…) (in-app OAuth + keys)
├─▶ 7. Brand the site + go live (see Go-live playbook)
└─▶ 8. Train the Client (manual, no tooling)
Everything a GoBuild operator does to a pooled hub funnels through the Control Plane (CP / ops), which holds the hub signing key and calls each hub's signed Admin API (app/routers/admin_api.py). Siloed clients get their own droplet and are administered through the same Admin API surface.
Work top to bottom. Owner = who performs the step. Status = how automated it is today.
| # | Step | Owner | Status | Evidence |
|---|---|---|---|---|
| 0 | Decide pooled vs siloed (drives everything below) | GoBuild | 🟢 | app/models/base.py (OrgIsolation); docs/onboarding_README.md |
| 1a | Pooled: create the org on a shared hub | GoBuild | 🟢 | admin_api.py:485-541 |
| 1b | Siloed: clone a droplet, rotate secrets, register | GoBuild | ⚪ | docs/client_onboarding_checklist.csv (10 phases) |
| 2 | Set the tier + entitlement overrides (Command Centers bought) | GoBuild | 🟢 | entitlements.py:23-40; controlplane/main.py:695-722 |
| 3 | Send the owner the magic-link invite (set-password) | GoBuild | 🟡 | admin_api.py:534-541 |
| 4 | Add staff, assign roles (owner/pm/office/field) | GoBuild / Client | 🟡 | admin_api.py:899-927; app/models/base.py:51-55 |
| 5 | Seed day-one cost codes + Job templates | Automatic | 🟢 | admin_api.py:527-532 |
| 6 | Import the builder's existing Clients / Jobs / cost codes | GoBuild / Client | ❌ | no bulk/CSV importer found |
| 7 | Connect integrations (QBO, Twilio, Google, Meta, Stripe…) | GoBuild / Client | 🟡 | admin_api.py:858; Integration setup |
| 8 | Brand the site + go live (domain, PWA, TLS) | GoBuild + Client | 🟡 | Go-live playbook |
| 9 | Train the Client + smoke-test each channel | GoBuild + Client | ⚪ | scripts/demo-check.sh (readiness sweep only) |
Status: 🟢 · [Audience: sales + ops]
The isolation mode branches the entire runbook, so pin it at the sale.
isolation=pooled) — the Client is a row in a shared multi-tenant hub DB. This is Starter / Pro. Provisioning is one Admin-API call (§1a); minutes, not hours. Pooled orgs that want their own third-party accounts bring their own keys through the per-tenant CredentialResolver — there is no global fallback (see the credential wall-off model).isolation=siloed) — the Client is the sole tenant of its own droplet/DB. This is Enterprise. Provisioning is the clone-a-droplet runbook: snapshot the golden droplet, rotate every inherited secret, insert this Client's keys. That is a separate 10-phase checklist and is not covered step-by-step here.Reference for siloed:
docs/onboarding_README.md,docs/client_onboarding_checklist.csv(10 phases with aDonecolumn),docs/client_values_worksheet.csv(one row per.envvalue — copy per Client), andscripts/provision_client.py(the from-scratch provisioner). Also see Provision a hub for standing up the box a pooled or siloed tenant lands on.
The CP administers both modes through the same Admin API (app/models/base.py, OrgIsolation docstring), so steps 2–9 below apply either way — only where the org lives differs.
Status: 🟢 (pooled) / ⚪ (siloed) · [Audience: ops]
POST /orgs on the target hub (admin_api.py:485-541) creates:
Organization (name, DNS-safe slug, tier, isolation=pooled, status=active). The slug must match ^[a-z0-9-]$; a taken slug is a 409 (admin_api.py:505-514).User (role=owner) with a hashed password — either one you supply or a generated token_urlsafe(12) (admin_api.py:516-523).admin_api.py:527-532). This is the same seed self-serve registration would run, so a brand-new org is usable immediately (see §5).In practice you don't hit the hub directly — the CP org_edit/create flows and the Showcase Go live button call it for you through the signed hubclient (controlplane/hubclient.py).
Follow docs/client_onboarding_checklist.csv. The org itself is still created via the same POST /orgs once the box is up and migrated (alembic upgrade head), phase 9 of that checklist. Everything before phase 9 is droplet + secret-rotation work unique to siloed.
Status: 🟢 · [Audience: sales + ops]
Entitlements decide which Command Centers the Client sees. The gateable features are a fixed set (entitlements.py:23-32):
| Feature key | Command Center / capability |
|---|---|
sales |
Sales / CRM — leads, proposals, estimator, nurture, booking |
marketing |
Marketing — website, blog, email studio |
ai_bar |
Keystone AI assistant |
procurement |
Procurement / purchase orders |
quickbooks |
QuickBooks / Xero sync |
hr |
Team & HR |
payroll |
Payroll |
safety |
Safety / TRIR |
Resolution order: per-org override (org.entitlements[key]) → else the tier default (entitlements.py:58-68). Tier defaults (entitlements.py:35-40):
sales, marketing, ai_bar, procurement, quickbooks, hr.payroll, safety.So most of the time, picking the right tier at create-time is all you do. To sell a Command Center à-la-carte (e.g. a Starter who bought only Sales), pin a per-org override.
How to set overrides (two ways, both land on PATCH /orgs/{id}):
controlplane/main.py:695-722). This is the humane path; use it.entitlements JSON object, shallow-merged, keys mapped to null removed (controlplane/main.py:603-648 → admin_api.py:458-473).Note on the "API-only" worry: entitlements are not API-only in practice — the CP god-mode Features editor is a real UI (
controlplane/main.py:695-722). What's true is there's no self-serve: the Client can't change their own plan; a GoBuild operator does it. Enforcement is live — nav hides ungated features (has_featureJinja global) and routes guard withrequire_feature(entitlements.py:71-85).
Status: 🟡 Link is generated; delivery is manual · [Audience: ops]
create_org returns an owner_invite_url — a set-password magic link, not a temp password (admin_api.py:534-541). It's built from a signed invite token (app/auth/security.py:137) as …/portal/accept-invite?token=…. The Client clicks it and sets their own password.
⚠ Delivery is the seam.
POST /orgsdoes not email the link — it just returns it in the response. Today it surfaces in the Showcase admin prospect page as a clickable "magic link ↗" (showcase/templates/admin_prospect.html:80) for the operator to copy/send by hand. Contrast the in-app team invite, which does auto-send email viaoutbound_email.send(..., "team_invite", …)(app/portal/router.py:1028-1035). So: owner invite = copy-paste manual; staff invites added from inside the product = emailed automatically. This inconsistency is worth closing (see open questions).
create_org also returns a temp_password when no password was supplied — a fallback if the magic link isn't workable, but prefer the link.
Status: 🟡 Two paths, only one auto-emails · [Audience: ops + Client]
Roles are a fixed enum: owner · pm · office · field (app/models/base.py:51-55).
POST /orgs/{id}/users with email, full_name, role (admin_api.py:899-927). Returns a temp_password when none supplied. Like the owner invite, this path does not send an email — you hand off the credential yourself. Update role / active later with PATCH /orgs/{id}/users/{uid} (admin_api.py:930-949); reset a password with .../reset-password (admin_api.py:952-968).team_invite email (app/portal/router.py:1028-1035), and the Keystone AI bar exposes an add_staff action that invites by name/email/role and emails a magic link too (app/services/ai_bar.py:1295-1321). Prefer this — it's self-service for the Client and delivery is automatic.Guidance: seed only the owner during onboarding; let the owner invite their own crew in-app so invites get emailed and roles are the Client's call.
Status: 🟢 seed / ❌ import · [Audience: ops]
What's automatic: on org create the hub seeds a default cost-code library and starter Job templates (admin_api.py:527-532, via app/services/costcodes.py + app/services/templates.py). Every new Client starts with a working chart of cost codes and template Jobs — no action needed.
What's missing: there is no bulk / CSV / spreadsheet importer for a Client's existing book of business — their real Clients (contacts/people), historical or in-flight Jobs, or a custom cost-code list. No such route or service turned up in the codebase. Today the options are:
This is the single biggest onboarding gap and the top open question below.
Status: 🟡 · [Audience: ops + Client] · See Integration setup for the full matrix.
Integrations split by how they're wired:
QBO_ENVIRONMENT=production). Meta (Facebook/Instagram) needs a per-instance Meta app that stays in Dev Mode. These stay dormant until the Client (or you, screen-sharing) completes the OAuth handshake.CredentialResolver — pooled orgs bring their own; there is no global fallback (credential wall-off model). The Admin API can set/clear a provider credential per org (admin_api.py:858-878). For siloed Clients these are .env values on their droplet (see docs/client_values_worksheet.csv).Always smoke-test each connected channel (test email / SMS / payment / e-sign) before calling onboarding done — the readiness sweep
scripts/demo-check.shand the Outbound Email send log are the fastest checks.
Status: 🟡 · [Audience: sales + Client] · Full detail: Go-live playbook.
If the Client came through Showcase, branding (approved variant pages, chat concierge name/KB, PWA app name + theme + monogram) is carried into the live org by the site-import step, and Go live provisions the org + imports the site in one click. Custom-domain attach, on-demand TLS, PWA install, and CRM lead routing are all covered there.
The one hard manual step for the Client is pointing DNS at the hub IP — nothing verifies it for you. Don't repeat the go-live mechanics here; link the Client to that playbook's Client-facing sections.
Status: ⚪ Manual, no tooling · [Audience: sales + Client]
There is no in-product onboarding tour, guided setup, or training module in the codebase. Training today is a live walkthrough / recorded session run by GoBuild. Minimum topics: creating a Job, adding the builder's Clients, the Command Centers they bought (§2), sending the first estimate/invoice, and inviting their crew (§4). Format and duration are undecided — see open questions.
| The Client does | GoBuild does |
|---|---|
| Sets their password via the magic-link invite (§3) | Decides pooled/siloed, creates the org, sets tier + entitlements (§0–2) |
| Invites their own crew in-app (auto-emailed) (§4) | Seeds the owner, hands over the owner link (§3) |
| Completes OAuth connects (QBO, Google, Meta) (§6) | Sets per-tenant keys / rotates siloed secrets (§6) |
| Points DNS at the hub (the one hard manual step) (§7) | Brands + publishes the site, confirms TLS (§7) |
| Runs their business — Jobs, CRM | Trains, smoke-tests every channel (§6, §8) |
| Gap / failure | Symptom | Cause / evidence |
|---|---|---|
| No bulk data import | Client's existing Clients/Jobs must be re-keyed by hand | no importer in codebase (§5) |
| Owner invite not auto-emailed | Operator must copy the magic link and send it manually | POST /orgs returns the link, doesn't send it — admin_api.py:534-541 |
| Staff Admin-API path doesn't email | Users created via POST /orgs/{id}/users get a temp password, no invite mail |
admin_api.py:899-927 (vs in-app which emails) |
| No self-serve | Every step needs a GoBuild operator; Client can't change plan/entitlements | by design — CP-only editors (§2) |
| Slug already taken | 409 on org create |
admin_api.py:505-514 |
| Seed failed silently | New org has no cost codes/templates | best-effort seed swallows errors — admin_api.py:527-532 |
| Pooled integration missing a key | Feature dormant / errors at use | per-tenant BYO, no global fallback — credential wall-off model |
| Client never points DNS | Custom domain never resolves | manual, unverified — see Go-live |
| No training artifact | Inconsistent Client ramp | no in-product onboarding (§8) |
docs/onboarding_README.md + docs/client_onboarding_checklist.csv + docs/client_values_worksheet.csv — the siloed (clone-a-droplet) runbook.app/routers/admin_api.py — the Admin API used by every step (create org, entitlements, users, credentials).