Consolidated register uncovered while documenting the platform (2026-07-08). Every
item is code-grounded with file:line evidence. Ordered by severity. This is a
living page — resolve items and move them to the bottom log.
Owner call needed on the items tagged [decision]. The rest are fixes.
DECISIONS.md #1).Common gate: several criticals are explicitly parked on "acceptable because
demo-only". The moment a real paying tenant is onboarded, C1–C4 + H1 + H3 must
be resolved. Treat "first real tenant" as the trigger checklist.
/api/track +/api/chat (unauthenticated, unthrottled, spoofable org_id). → Roadmap #8.TENANT_SECRET_KEY is escrowed off-box → verify first.See the Roadmap & To-Build page for the full prioritized backlog.
gautam@gobuild.ca /gobuild.ca/demo). → Roadmap #12b.| # | Risk | Evidence | Impact |
|---|---|---|---|
| C1 | Fleet god-mode is password-only. TOTP machinery exists and login enforces it if a secret is set, but no enrollment flow ever sets one (admins created with totp_secret=""). |
controlplane/bootstrap.py:41, main.py:1120 |
One leaked CP/admin password = full-fleet god-mode over every client hub. |
| C2 | No offsite backups. update.sh dumps only the keystone DB to the droplet's own disk; DO backups off (backups:False). Not backed up at all: documenso/wiki DBs, MinIO signed contracts, chatwoot, openproject, cp_data (the authoritative audit log). |
controlplane/do_client.py (backups:False); update.sh |
Droplet loss ≈ unrecoverable. Losing the Fernet TENANT_SECRET_KEY = every BYO tenant credential undecryptable. No tested restore. |
| C3 | /_admin god-mode API is internet-reachable over public HTTPS (signature-gated only), not VPC-locked. |
app/routers/admin_api.py; Caddyfile |
Highest external attack surface on the fleet. |
| C4 | Invoice portal has no auth gate at all. The token alone renders the invoice and the Stripe Pay button; no PIN/OTP path is even wired (unlike the client portal). | app/routers/invoice_portal.py (zero portal_otp refs) |
Anyone with a forwarded invoice link can view financials and pay. |
| # | Risk | Evidence | Impact / note |
|---|---|---|---|
| H1 | DeepSeek is China-hosted and may train on inputs. Default deepseek_base_url = api.deepseek.com; customer PII/contracts/financials flow through the text path (Gemini auto-fallback). |
app/integrations/ai/deepseek.py:1-6; docs/DECISIONS.md #1 |
[decision] US-host migration only planned. Sensitive customer-facing paths are already pinned to Gemini; the general text path is not. |
| H2 | Client-portal OTP falls through to no gate when no phone is on file, and is opt-in (both flags default False). Website login always forces OTP, but a raw /client/{token} link gets nothing. |
app/routers/client_portal.py:143-159; preferences.py:101,104 |
[decision] Owner wants OTP as the default — the fall-through would silently void that policy. |
| H3 | QBO OAuth tokens stored plaintext. They grant write access to a Client's books. | model docstring ("future hardening") | Encrypt at rest before more orgs connect. |
| H4 | AI bar executes money/people writes (approve_bill, create_invoice, decide_time_off, add_staff, send_for_signature) with only a confirm-click + pm/office role as guardrail. |
app/services/ai_bar.py |
[decision] No dollar thresholds, owner-tier gate, AI-write audit log, or undo. |
| H5 | Impersonation ("Login as") — any owner/admin can enter any Client account; no consent, no customer notification; only impersonate.start logged (no end/duration). |
controlplane/main.py:741 |
[decision] Trust/compliance gap. |
| H6 | CP_SECRET blast radius — signs CP session cookies and the showcase svc-golive service token; shared into the showcase container. |
controlplane/main.py:201-214 |
Confirm it's distinct from the RS256 keypair per environment. |
| H7 | Inbound SMS webhooks unauthenticated when no token set — sms.verify_signature returns True ("can't verify, so allow"). |
app/integrations/sms |
Ensure the Twilio auth token is always configured in prod. |
| H8 | Voice/Twilio not walled off — reads global Twilio creds; pooled orgs' paid tracking numbers bill GoBuild's master account. | app/routers/marketing_cc.py:303 |
Isolation + cost-attribution gap; no per-tenant entitlement gate on a paid action. |
| # | Risk | Evidence | Note |
|---|---|---|---|
| M1 | OpenProject fragile — no is_configured() guard; a schedule create/edit hard-fails HTTP 502 if OP is unreachable; add_dependency writes rows missing the fields mirror_relations filters on (arrows never mirror). |
app/routers/schedule.py; dependency mirror |
Live & wired, but degrade gracefully + fix the field mismatch. |
| M2 | Xero is vaporware — 0-byte package, no client/router/service — yet sold in the entitlement label "QuickBooks / Xero sync". | app/integrations/xero/__init__.py; entitlements.py:28 |
[decision] Build it or strip "Xero" from all copy. |
| M3 | Lien waivers = two disconnected stores (Documenso LienWaiver rows vs Bill.lien_waiver_status) — a signed waiver can show unsigned. |
Reports & Finance | Pick a source of truth. |
| M4 | Dormant channels shown as live — Ads publish (unbuilt, Launch disabled), Social (off w/o Meta app), Nurture drips (superseded), Web push (no VAPID). | Marketing | Owner chose: badge honestly + "not shippable yet". |
| M5 | Live concierge impersonates the owner to visitors ("can't tell it's AI"); emails leads to hardcoded gautam@gobuild.ca; steers to gobuild.ca/demo. |
live_ai |
[decision] Make sales inbox / demo URL per-org; confirm impersonation posture. |
| M6 | /p/{slug}/projects exposes all Jobs for a phone number — recycled/shared numbers leak across Clients. |
website client login | Scope by verified identity, not phone alone. |
| M7 | CP DB defaults to SQLite on a volume — holds the authoritative audit log + fleet registry. | docker-compose.prod.yml:213 |
Confirm prod points at Postgres. |
| M8 | Key rotation has no runbook — cp_public.pem must update on every hub in lockstep with the CP's cp_private.pem; token constants duplicated; 60s TTL assumes low clock skew. |
security.py:49-51, admin_api.py:30-32 |
Write the rotation procedure. |
| M9 | DNS not idempotent — upsert_dns_a isn't an upsert; retried provisioning can create duplicate A records. |
controlplane/do_client.py:67 |
|
| M10 | No monitoring/alerting for the CP itself, disk-fill, cert-expiry, OOM, Postgres down. | infra | Discovery today = "a customer emails us". |
| M11 | AI retrieval is whole-tenant — any pm/office user can query any Job, pay figure, or performance review from any screen. | app/services/ai_retrieval.py |
[decision] Scope by role/assignment? |
| M12 | Dead / misleading code — PO billed status never set; request_invoice_signature has no caller; pillar/navgroup macros unused; stale #ks-ai-bar selector; dead critical email-protection flag (password_reset/team_invite can be toggled off); Deepgram key with no module. |
various | Cleanup; the email-protection one is a real footgun. |
| M13 | Siloed provisioning is a placeholder — real cloud-init bootstrap commented out; siloed default region tor1 vs the fleet's nyc1 VPC. |
provisioning.py:73-82,99 |
New siloed tenants could land outside the VPC. |
(move items here as they're fixed, with the commit/date)