A productized end-to-end pipeline for generating and maintaining client websites — a HOFMI trainee can run the build, a non-technical client can request changes from inside their own site, and Codesmith handles the work behind a 24-hour internal-review gate.
Why this exists, and the constraint window.
Brynn runs HOFMI ministry development + Transformate consultancy. There is an immediate batch of HOFMI ministry websites to (re)build, plus an external pipeline starting with a Witbank veterinarian. Today, building and maintaining client websites is bespoke work that consumes Brynn personally — every change request goes through him. This doesn't scale to the HOFMI cohort and burns ministry hours that should go elsewhere.
This spec defines a productized client-website platform that:
MVP must be ready in days, not weeks (HOFMI ministry websites blocked on this). Each ministry site through the pipeline = ~2 days of human-in-loop work; the platform itself unblocks that throughput.
Generation and maintenance are different problems with different lifecycles. They share infrastructure but ship as distinct codebases.
| Sub-product | Repo | Role |
|---|---|---|
| site-studio v2 | brynnclaw/site-studio (existing) |
Generates new client sites end-to-end. Outputs OD-native HTML+DESIGN.md projects. |
| site-keeper | brynnclaw/site-keeper (NEW) |
Auth'd client portal. Maintenance flow: comment → Codesmith → review → preview → approve → live. |
Orchestrating documentation (this spec, the trainee playbook, runbooks) lives in brynnclaw/website-build-playbook.
Load-bearing values — everything downstream must respect these.
Codesmith output never reaches the client without a human pass. Warden / Brynn / Matt / Gian review every preview before it goes to client_review. 24h SLA communicated to clients up-front; predictable timing > variable speed.
Comments-on-an-existing-preview = a new change_request row, NOT a free revision. The portal explicitly tells the client "approving the previous preview, opening this as a separate change." Visible in the billing summary too. This is the unit-economics gate — without it, scope mutates mid-execution and the agent eats unbilled compute.
Per-client GitHub repo (brynnclaw/site-<slug>) holds everything: HTML/CSS, brand kit, CLAUDE.md, AGENTS.md. Codesmith reads the repo + the change_request row. Nothing lives in agent working memory between requests. Portal displays "current state of the PR," not "current state of the agent." If Codesmith is replaced tomorrow by another runtime, every site keeps working without migration.
Implementation invariants. Drift here = compounding pain later.
Same Cloudflare Pages project per client. main branch deploys to production. agent/<change-request-id> branches deploy to preview URLs. Env vars defined identically for preview and production environments from day 1, before client #1. Drift is structurally impossible.
If a change requires a new secret, the state machine pauses with blocked_on_human_secret and emails Brynn. The agent does not create env vars itself; that's a human-only action.
Open Design writes comments to its SQLite at /app/.od. A 30-second polling sync mirrors new/changed comments to Supabase Postgres. The 24h SLA absorbs the 30s lag trivially. OD stays unmodified (no fork to maintain) while Supabase remains the source of truth for the business workflow (Realtime, Edge Function triggers, billing meter, audit log).
Codesmith now operates as a persistent agent with its own workspace, memory, and identity (per project_codesmith_persistent_workspace.md, 2026-05-06). Codesmith is reserved for development work — building features, refactoring, multi-session coding tasks where continuity matters.
For per-client website maintenance, the platform spawns a separate, stateless ephemeral agent — working name site-edit-worker (final name TBD). Sonnet 4.6, fresh process per change_request, no /home/ workspace, no MEMORY.md, no conversation history retention. Reads only the change_request row, the per-client GitHub repo, and the element screenshot — nothing else.
The bridge runtime routes by task_type:
| task_type | Worker | Lifecycle |
|---|---|---|
codesmith_session | Codesmith (persistent identity, /home/codesmith) | Sessions resume across days |
site_edit | site-edit-worker (ephemeral, stateless) | Single-shot per change_request |
This separation prevents cross-client information leakage, keeps Codesmith's persistent workspace clean of high-volume routine work, and cleanly enforces Principle 3.3 (agent stateless between requests) for the maintenance flow.
Extends the existing 13-stage pipeline. Biggest shift: Stage 7 retargets output from Astro to OD-native HTML+DESIGN.md projects that Open Design can open and edit directly.
| Stage | Purpose | Phase |
|---|---|---|
| 0 — Discovery + brief | Client interview, niche identification | MVP |
| 0.5 — Niche research | Pain mining, competitor grading, persona gen | Phase 2 |
| 1 — Brand kit | Generate / import DESIGN.md for the client | MVP (manual) |
| 2 — Content acquisition | Harvest existing client content (FB page, old site, photos) | Phase 2 |
| 3 — Copy extract + rewrite | Existing | MVP |
| 4 — Image gen (KIE) | Existing | MVP |
| 5 — Animation extraction | Existing | MVP |
| 6 — SEO + structured data | Existing | MVP |
| 6.5 — AOE | Sitemap + schema for agent crawlers | Phase 3 |
| 7 — Project assembly (OD-native) | HTML files + DESIGN.md to per-client repo. Was Astro; now OD-native. | MVP |
| 8 — OD composition + impeccable audit | Trainee opens OD with project pre-loaded; refines interactively. Runs impeccable audit against output before progressing — ban violations block Stage 9. | MVP |
| 9 — CF Pages deploy | Cloudflare Pages project per client; custom domain bound | MVP |
| 10 — Portal provisioning | Create Supabase row for client; send first-login email | MVP |
pnpm site-studio init <slug> walks the brief + runs stages 1–7. KIE image gen takes minutes; CLI is the right surface.brynnclaw/site-<slug>/
├── README.md — client-facing handoff doc
├── CLAUDE.md — agent operating context for Codesmith
├── AGENTS.md — change-request handling rules for Codesmith
├── DESIGN.md — brand kit (OD-readable)
├── pages/
│ ├── index.html
│ └── ...
├── assets/ — images, fonts (OD generates / fetches)
├── partials/ — header.html, footer.html (shared, included)
└── deploy/
└── cloudflare-pages.yaml
main branch = live. agent/<change-request-id> branches = previews. CF Pages auto-deploys both.
Next.js 16 + Self-hosted Supabase. Single public surface (CF tunnel), everything else Tailscale-internal.
One OD container on HOFMI-TEAM-1 holding all client projects. Auth shell reverse-proxies authenticated user to OD, scoped to their project via path/cookie. Cheap, fast to ship, fits the HOFMI cohort.
Orchestrator spawns ephemeral OD container on client login. Cold-start screen tells client: "Spinning up a full sandbox of your site so your edits never touch live..." — turns the 10-30s wait into a trust-building moment. No data migration (projects are git-backed).
Skipping topology B (always-on per-client) — pays for idle.
| Component | Host | Notes |
|---|---|---|
| site-keeper Next.js shell | HOFMI-TEAM-1 (CF tunnel) | Only public surface |
| Self-hosted Supabase (full stack) | HOFMI-TEAM-1 | ~3-4GB RAM; sovereignty pattern |
| Open Design instance(s) | HOFMI-TEAM-1 (Tailscale only) | Topology A: one container; C: ephemeral |
| Codesmith bridge | HOFMI-TEAM-1 | Reuse current bridge runtime |
| Overflow capacity | hofmi-app-1 (100.105.87.117) | Currently underutilized; available when load increases |
GoTrue magic-link primary; OAuth optional. Per-tenant claim in JWT (org_id) used by Postgres RLS to enforce tenant isolation. One-app multi-tenant — clients = organizations in one site-keeper deployment, not subdomain-per-client.
client — sees only their own org's projects, comments, change requestsreviewer — Matt + Gian; sees all orgs' change requests in internal_review stateoperator — Brynn + Warden; full access including new-engagement kickoffadmin — schema migrations, billing meter access, system configClient clicks any element on their site preview, leaves a comment. OD's edit-mode bridge handles element selection + position + selector + screenshot. We adopted OD's engine; we did not build it.
MVP routing rule: ALL changes route to agent + review. Self-serve direct-edit lane (text/image/link via manualEditPatch) reserved for Phase 2 once the system has earned trust.
Quality control on every edit: the site-edit-worker invokes the impeccable skill against the diff before transitioning to internal_review. New ban violations introduced by an edit must be self-fixed or flagged on escalation. Reviewer sees the impeccable report alongside the preview.
The full end-to-end flow including the 2-min scoping pass and blocked-on-topup gate that protect unit economics.
failed — Codesmith couldn't apply patch (selector ambiguous, source malformed). Falls back to manual queue.escalated_to_manual — internal review rejected Codesmith's output. Human handles → resumes at internal_review.cancelled — client cancelled (no charge), or Brynn cancelled (no charge).stale_archived — client_review hit 14 days no response.Two billing modes per client. Estimate shown before approval — the email itself is the cost gate.
Client pays a monthly retainer for N hours of platform time. Each change_request deducts its actual time. Hours reset monthly. Email at client_review: "this change took 1.5 hours; deducted from your 10-hour monthly retainer; 6.2 hours remaining this month."
Client purchases credits up-front (e.g., $50 ≈ 2 hours). Each change_request deducts cost. Credits don't expire. Email at client_review: "this change cost $35; deducted from your $50 credit balance; $15 remaining."
clients.currencyProduces estimated time (minutes), estimated cost (in client currency), and risk flags ("looks like a structural change," "needs new env var," etc.).
| Patch kind | Estimated time |
|---|---|
| Text edit (<200 chars, not H1) | 5 min |
| H1 / hero text edit | 15 min |
| Link edit | 5 min |
| Image swap (alt + src) | 10 min |
| Style edit (color, spacing) | 30 min |
| Outer HTML / structural | 60 min |
| New section | 90 min |
| Multi-element / "redesign this page" | 180+ min, flagged for manual quote |
Phase 2: Sonnet pre-classifies the request based on comment text + element + project history.
Schema fields exist in MVP; logic wired in Phase 3:
| Tier | SLA | Review depth | Pricing |
|---|---|---|---|
starter | 24h | Full review | Pay-per-credit |
pro | 24h | Full review | Monthly retainer |
rush | 4h | Lighter review | Premium retainer |
enterprise | Instant | Dedicated agent, no review | Top-tier retainer |
Implementer-facing. Skim unless you're writing the migration.
-- Tenancy
CREATE TABLE clients (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
slug text UNIQUE NOT NULL,
name text NOT NULL,
domain text,
currency text NOT NULL CHECK (currency IN ('ZAR','USD')),
billing_mode text NOT NULL CHECK (billing_mode IN ('retainer','credit')),
retainer_hours_per_month numeric,
retainer_used_minutes_this_period int DEFAULT 0,
retainer_period_resets_at timestamptz,
credit_balance_minor int DEFAULT 0,
subscription_tier text DEFAULT 'starter',
github_repo text NOT NULL,
od_project_id text,
created_at timestamptz DEFAULT now()
);
CREATE TABLE users (
id uuid PRIMARY KEY,
email text UNIQUE NOT NULL,
client_id uuid REFERENCES clients(id),
role text NOT NULL CHECK (role IN ('client','reviewer','operator','admin')),
created_at timestamptz DEFAULT now()
);
CREATE TYPE change_request_status AS ENUM (
'submitted','queued','scoping','scoped','blocked_on_topup','blocked_on_human_secret',
'processing','internal_review','client_review',
'approved','live','done_replaced',
'cancelled','failed','escalated_to_manual','stale_archived'
);
CREATE TABLE change_requests (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id),
page_path text NOT NULL,
od_comment_id text,
od_element_selector text,
od_element_screenshot_url text,
client_note text NOT NULL,
status change_request_status NOT NULL DEFAULT 'submitted',
estimated_minutes int,
estimated_cost_minor int,
actual_minutes int,
actual_cost_minor int,
billing_source text,
estimate_shown_to_client_at timestamptz,
estimate_approved_by_client_at timestamptz,
reviewer_user_id uuid REFERENCES users(id),
reviewer_decision text,
reviewer_decision_at timestamptz,
agent_branch text,
preview_url text,
pr_url text,
parent_change_request_id uuid REFERENCES change_requests(id),
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE INDEX ON change_requests (client_id, status, created_at DESC);
CREATE INDEX ON change_requests (status, updated_at DESC);
CREATE TABLE billing_meter (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id),
change_request_id uuid REFERENCES change_requests(id),
minutes int NOT NULL,
cost_minor int NOT NULL,
currency text NOT NULL,
source text NOT NULL,
occurred_at timestamptz DEFAULT now()
);
CREATE TABLE audit_log (
id bigserial PRIMARY KEY,
actor_user_id uuid REFERENCES users(id),
actor_role text,
action text NOT NULL,
target_table text,
target_id uuid,
payload jsonb,
occurred_at timestamptz DEFAULT now()
);
RLS shape: clients see only their own rows via app.current_client_id JWT claim; reviewers see internal_review across clients; operators & admins see all. Production policies will be more granular.
Exactly 4 client-facing triggers + 2 conditional ones. Every email is a tax on attention.
| Trigger | When | Content |
|---|---|---|
client_review |
Preview ready + estimate approved | Preview URL · before/after screenshots · time estimate (hours if ≥30 min, else minutes) · cost line · billing source · Approve / Comment buttons |
live |
After client approves; merged + deployed | "Your update is live" + link to live URL |
stale_d3 |
client_review unactioned 3 days from email send | Gentle reminder + same content as client_review |
stale_d7 |
client_review unactioned 7 days from email send | Final reminder + "we'll auto-archive at day 14" |
blocked_on_topup |
Conditional — balance too low | "Needs N more hours / $X to proceed" |
blocked_on_human_secret |
Conditional — Brynn-only | "Codesmith needs new env var: <name>" |
NOT emailed: submitted, queued, scoping, processing, internal_review, rejected_by_internal, failed. The in-portal Realtime indicator handles those.
Three integration points. OD is the editor, deploy mechanism, and (eventually) the per-client container.
clients.od_project_id. Topology C: ephemeral per-session containers spawned via Coolify. Project files mounted from per-client GitHub repo (clone on container start)./api/comments. A 30-second polling sync (Node.js) reads new/changed comments → upserts to change_requests in Supabase. The 30s lag is invisible against the 24h SLA.applyManualEditPatch(source, patch) for trivial patches. For structural changes Codesmith does its own editing (branching off the per-client repo) and commits.Pin docker image to a specific SHA (docker.io/vanjayak/open-design@sha256:...), not :latest. Per FLEETSEC doctrine: update only after release-notes review + 1-2 week soak. Sentinel watches the OD release feed.
Skills + repos the platform consumes (not platform code — inputs).
| Reference | Type | Where it's used |
|---|---|---|
impeccable (pbakaus skill) |
Skill | Stage 8 OD composition audit (generation) + every site-edit-worker invocation (maintenance). 7 absolute bans + design discipline. |
| robinstickel/ |
Repo | Linked from each per-client CLAUDE.md. Trainees skim relevant principles during Stage 0 brief. site-edit-worker references it for project context. The "design principles GitHub repo Brynn was looking for." |
| bergside/ |
Repo | Library of pre-built design-system SKILL.md files compatible with OD's skills protocol. Mineable for industry-pack design systems in site-studio v2 (Stage 1 brand-kit). |
| VoltAgent/ |
Repo | 69+ public brand DESIGN.md files (Stripe / Linear / Vercel / Apple). Reference content for Stage 1 brand-kit. |
frontend-design (Anthropic skill) |
Skill | Reserved for bespoke track premium client work, not the playbook track. |
shadcn/ui |
Components | For the site-keeper portal's own UI (operator/reviewer/client surfaces), not for the generated client sites. |
These references live in their upstream repos and are linked from per-client CLAUDE.md files so the site-edit-worker can pull principles into project context on every invocation — without retaining state itself.
The bridge runtime routes change_request work to a stateless ephemeral agent (site-edit-worker), distinct from the persistent Codesmith dev-work lane. See §4.4 for the lock.
claude-sonnet-4-6)/home/<agent>/ workspace. No memory between invocations.change_requests.status transition to processing, POSTs to the bridge with task_type=site_edit. Bridge router dispatches a fresh worker.change_request row (id, page_path, element selector, screenshot URL, client note, estimated minutes)CLAUDE.md, AGENTS.md, source files in pages/, partials/, assets/)agent/<change-request-id> branch in the per-client repochange_request row: agent_branch, preview_url, pr_url, actual_minutes, status → internal_reviewmain, branch agent/<change-request-id>applyManualEditPatch for trivial patches, or direct file edits for structural changes)impeccable audit on the diff — auto-fix catchable violations; flag the rest with impeccable.violations on the change_request rowinternal_reviewCodesmith is not used for client-website-platform change_request work. Codesmith retains its existing role: development work on Brynn's platforms (arkon, arkon-os, arkonhelm, etc.) where multi-session continuity, named identity, and persistent workspace are valuable. Per project_codesmith_persistent_workspace.md (2026-05-06): Codesmith has /home/codesmith/, MEMORY.md, synced identity.
Why not unify: cross-tenant leakage risk if one persistent agent handles all clients; Codesmith's workspace would balloon with high-volume routine edits; Principle 3.3 (agent stateless between requests) is cleanly enforced when the maintenance agent literally cannot retain state.
Minor extension to dispatch by task_type:
// pseudocode
async function dispatch(taskType: string, payload: any) {
switch (taskType) {
case 'codesmith_session':
return spawnCodesmith(payload); // existing path
case 'site_edit':
return spawnSiteEditWorker(payload); // NEW — ephemeral, stateless
default:
throw new Error(`unknown task_type: ${taskType}`);
}
}
spawnSiteEditWorker mints a fresh Claude Agent SDK worker, no /home/ directory, no MEMORY.md, no identity sync. The system prompt is built from §12.1 inputs only; nothing carried over from prior invocations.
failed → manual queueWhat's in, what's deferred. Push back here if anything needs to flip.
scoped → processingreviewer + ~30 min trainingDefer to writing-plans.
eu.hofmi.org / na.hofmi.org or per-domain?| Risk | Mitigation |
|---|---|
| OD 0.6.0 docker image lands later than expected | MVP can ship with 0.5.0; CF Pages deploy stage stubbed manually for first 2-3 sites until 0.6.0 |
| Codesmith time estimates systematically wrong | Heuristic for MVP; instrument actual times; tune table after 10 change_requests; Sonnet-estimator in Phase 2 |
| Clients abuse the "comment on preview = new request" gate | Make billing line visible BEFORE approval, in the email, in the portal. Document policy in onboarding email. First 5 clients agree explicitly. |
| Bridge-Warden goes down during peak review | Matt + Gian as backup reviewers; 24h SLA absorbs most outages; tightened alert thresholds catch drift faster |
| Site-edit-worker leaks information across clients | Stateless guarantee enforced by spawning fresh worker per request, no /home/ workspace, no memory persistence; bridge router dispatches site_edit separately from Codesmith's codesmith_session; audit log records every read against change_requests to detect anomalies |
impeccable skill version drift introduces false ban violations |
Pin impeccable skill version in each per-client repo's CLAUDE.md; review impeccable releases monthly; Stage 8 trainee audit catches deploy-blockers before client ever sees a violation report |
| Supabase self-host corruption / data loss | Daily Postgres pg_dump → encrypted R2 (extends warden_backups pattern); test restore monthly |
| OD release introduces a breaking change to comments contract | Pin to specific SHA; review release notes 1-2 weeks before bumping; staging container test |
| Client unhappy after a change goes live | One-click git revert rollback in portal; preserves previous live state in <60s |
| Costs exceed $50/client/mo target | Monitor unit economics monthly; first sign of drift = investigate before scaling client count |
After spec approval, file the umbrella WI under transformate tenant and decompose into 9 implementation sub-WIs in writing-plans phase.
Title: Build client-website-platform (site-studio v2 + site-keeper portal)
Status: proposal → approved (after Brynn reads spec)
Tenant: transformate
Description: reference this spec path · MVP-in-days for HOFMI batch · two repos to create + extend · dependencies: self-hosted Supabase, OD 0.6.0+, CF Pages access, Resend/SMTP
task_type dispatch) + ephemeral site-edit-worker (Sonnet 4.6, stateless) + impeccable invocation in workflow