Skip to content

Work · Internal ops platform · Marketing agency · the agency behind CS-01 and CS-18

Agency onboarding platform + client scorecard

Onboarding a new client was a 90-minute checklist someone ran by hand

An internal admin plus a client-facing portal, running as one application across two subdomains, that takes a signed studio from sales handoff to a fully provisioned CRM sub-account. Its centerpiece: a nine-point client scorecard the agency edits once, rendered live inside every studio’s own CRM dashboard through a signed embed.

  • Astro
  • PostgreSQL
  • Drizzle
  • Vercel
  • GoHighLevel
  • Role · sole designer & builder
  • 8 min
portals on one deployment — agency admin + client intake
2Scope
endpoints · 25 test files, fail-closed verified
33Scope
branded lifecycle emails, strategy call to receipt
20Scope
System size
System flow · 9 stages · 44 components reconstructed from the build record

The situation

The agency signs studios. Every time one signed, somebody opened a document and worked down it: set up the CRM sub-account, chase the intake information, wire the pipelines, remember what had already been done. It took about ninety minutes of a person's attention per client, and it lived in that person's head between steps.

There was no view across studios. If you wanted to know how many were mid-onboarding, or which one had been stuck for three weeks, you asked someone. And there was nothing branded the agency could put in front of a client to show progress, so status arrived as screenshots and phone calls.

They also wanted a scorecard: a nine-point read on each studio's marketing setup that the agency grades and the client can see. Not in an email. Inside the studio's own CRM dashboard, where they already log in every day.

What was broken

  • Onboarding ran as a manual checklist per client, with sub-account setup, intake collection and pipeline wiring all done by hand.
  • Progress lived in someone's head, so there was no cross-studio view and no way to see what was blocked or on whom.
  • Client-facing status was screenshots and phone calls, which meant the agency transcribed the client's own corrections back in by hand.
  • Automation was hardcoded to one account's record ids, so the same code could not run against a freshly provisioned sub-account.
  • Nothing recorded who was blocking a step, so a stalled onboarding looked identical to an active one.
  • Sales deals stalled on the board with no mechanism to retire them, so the pipeline slowly filled with dead rows.

Constraints

  • Two products, an agency admin and a client portal, had to run from one deployment across two subdomains.
  • The studio owner had to complete intake with no account and no login, from a link.
  • The vendor owns sub-account provisioning through its own hosted checkout, so the application could only react and reconcile.
  • Creating CRM workflows programmatically is only possible through the vendor's unsupported internal API, which carries real account risk.
  • The client portal must never be indexed, and a client session must only ever resolve its own studio.
  • Some CRM behaviour is undocumented or wrong in the docs, so the integration had to be written against observed behaviour.

FIG 01 · System Flow

How the system actually runs

Every component below is reconstructed from the project’s own build records. Nothing is added to make it look bigger.

components
44
components
stages
9
stages
decisions & guards
9
decisions & guards
AI
0
AI
human handoffs
1
human handoffs
platforms
5
platforms
  1. Stage 1, Access & tenancy: The host router, token kinds and auth that keep two products safely inside one deployment.
    1. Host-based router (Guard, Astro middleware). A pure, I/O-free decision function maps the request host to admin, portal, or nothing — and an unrecognised host resolves to null, which 404s every path. Connects to Agency admin host when admin host; Client portal host when portal host; Three token kinds when unknown host → 404.
    2. Agency admin host (Source, Astro (SSR)). The agency’s internal surface — pipelines board, onboarding tracker, metrics — with every client-portal path explicitly 404’d on this host. Connects to Admin & client auth.
    3. Client portal host (Source, Astro (SSR)). The client-facing surface — start, welcome, the tokenized intake wizard, the tracker and corrections — with admin paths and admin APIs 404’d and a noindex header applied. Connects to Admin & client auth; Intake wizard when public intake path.
    4. Three token kinds (Guard, JWT (HS256, one secret)). Session (admin, 7 days), client (portal, 30 days) and intake (URL-borne, 14 days) tokens share a signing secret but carry distinct kind claims, alongside a short-lived magic-link kind. Connects to Admin & client auth; Tokenized intake link when distinct kind claim.
    5. Admin & client auth (Guard, Astro API + argon2id). Two gates: the agency signs in by a constant-time, non-short-circuiting password compare or an email magic link whose allowlist is re-checked at redemption, and the studio owner signs in with argon2id hashing, a five-failures-per-fifteen-minutes lockout and single-use expiring reset tokens. Connects to Admin onboarding tracker when agency session; Pipelines board when agency session; Client tracker & corrections when client session, own studio only; Nine-point score when the agency grades by hand.
  2. Stage 2, Sales capture & pipeline: Lead capture, booking, and the orchestrator that owns every stage move.
    1. Landing-page opt-in (Capture, Astro API endpoint). The public lead-capture endpoint — origin-restricted CORS plus a honeypot — upserts the contact into the CRM, tags the lead, stamps its source, and opens an opportunity on a two-stage pre-booking pipeline holding leads who have not yet booked. Connects to Book strategy call; Eight-stage sales pipeline when call booked.
    2. Book strategy call (Calendar, Astro API + CRM calendar). Free calendar slots are fetched server-side and proxied to the page so the integration token never reaches the browser; booking creates the appointment and, best-effort, advances the pre-booking opportunity to its booked stage. Connects to CRM webhook receiver when appointment event.
    3. Stateless orchestrator (Workflow, TypeScript over the CRM API). A single module owning every stage move, tag, field write, task and campaign enrolment; it keeps no state of its own — every contact’s state lives in CRM custom fields. Connects to Resolve-by-name; Eight-stage sales pipeline; 20 lifecycle emails when exclusive enrolment.
    4. Resolve-by-name (API / service, CRM API + in-process cache). Pipelines, stages, workflows and contact custom fields are looked up by their human name at runtime and cached for five minutes; no raw CRM id lives in the code. If a named workflow does not exist yet, enrolment returns false and logs which one to build while the stage move and tags still apply. Connects to Eight-stage sales pipeline when stage + tags apply either way.
    5. Eight-stage sales pipeline (CRM record, GoHighLevel pipeline). Call booked, no-show, done or pending next call, next call scheduled, its no-show, deciding, won, long-term nurture — each transition driven by an appointment or pipeline event, and a second no-show creating a task for the agency principal to phone the prospect personally. Connects to Daily stage sweep when stage ages out; Won → client when won — client.
  3. Stage 3, Signed → onboarding: The moment a won deal becomes a studio record, a milestone set and a tokenized intake link.
    1. Won → client (Workflow, Orchestrator). One routine moves the deal to won, tags the contact as a client, removes every sales campaign, stamps the signed date, creates the onboarding record, enrols the onboarding campaign and opens a fulfilment task. Connects to Create studio record.
    2. Create studio record (Data store, PostgreSQL + Drizzle). Creates the studio row and all seven milestone rows in one transaction, mints a unique studio key, generates the intake token, writes the intake URL back to the CRM contact and seeds an empty scorecard — idempotent on the CRM contact id and wrapped so any failure returns null rather than breaking the sales stage move. Connects to Studios & milestones; Tokenized intake link; Score storage & mirror when seed an empty score.
    3. Tokenized intake link (Guard, JWT (14-day)). The intake link is a signed 14-day token carrying the studio key and its own kind claim; expired or malformed tokens land on a dedicated expiry page rather than an error. Connects to Intake wizard.
  4. Stage 4, Provisioning & reconcile: Two routes to a live CRM sub-account, and the reconciliation that makes them converge on one.
    1. Self-serve checkout (Source, CRM hosted checkout). For self-serve signups the CRM’s own checkout creates the sub-account, loads the snapshot, enrols the plan and enables rebilling; the platform only ever reacts to that. Connects to Signup webhook; Reconcile sub-account when welcome-page poll.
    2. Reconcile sub-account (Scheduled job, TypeScript + Postgres). Verifies the location is genuinely activated on the right plan, enriches its name and email by paging the account list, then links or creates exactly one studio. Connects to One studio per account; Send-once gate.
    3. One studio per account (Data store, Postgres partial unique index). A partial unique index on the CRM location id, only where non-null, makes duplicate creation a constraint violation — which the code detects by walking the error’s cause chain and then re-reads the winning row. Connects to Create studio record when winner re-read.
    4. Send-once gate (Guard, Postgres timestamp). The CRM handoff — link the contact, write the intake URL, enrol the onboarding campaign — happens once and is gated on a sent-at timestamp; if the contact does not exist in the CRM yet it no-ops and the cron retries. Connects to Tokenized intake link when intake URL written; 20 lifecycle emails when a CRM workflow sends the welcome; Reconcile backstop cron when contact not in CRM yet — retry.
    5. Agency-API provisioning (API / service, CRM agency API). A direct sub-account creation call built from the latest intake submission, with a business-address-to-tax-address fallback and an early return if the studio is already linked — deliberately no longer wired into the intake path and reachable only from an admin retry button. Connects to Reconcile sub-account.
  5. Stage 5, Client intake portal: The wizard the studio owner fills in, and everything that protects it.
    1. Intake wizard (Capture, Astro page (token-scoped)). The multi-step intake form the studio owner opens straight from the tokenized link with no login, autosaved server-side against the token rather than in the browser. Connects to Validated upload; Submit intake.
    2. Validated upload (API / service, Astro API endpoint). Logo and document uploads are size-capped, MIME-allowlisted, and then actually decoded to check their magic bytes — a container claiming to be an image but isn’t is rejected outright. Connects to Submit intake when magic bytes valid.
    3. Submit intake (Workflow, Astro API + Postgres). Validates server-side, writes the submission to Postgres first, advances the intake milestone, and only then syncs to the CRM and provisions the client login — CRM failures become warnings on the response, never lost data. Connects to Submissions & corrections when Postgres first; Messaging-compliance branch; Provision client login; Advance milestone when intake milestone auto-done.
    4. Provision client login (Workflow, argon2 + CRM workflow). On first submit the app mints a readable temporary password with ambiguous glyphs removed, stores only its hash, writes the plaintext to a CRM contact field, and lets a dedicated CRM workflow deliver it and then clear the field — returning null if a password already exists. Connects to Admin & client auth when credentials delivered by a CRM workflow; Studio live.
    5. Messaging-compliance branch (Decision, Intake step + CRM field). The intake asks whether the studio has a federal tax id and branches into a standard registration, a sole-proprietor fallback, or a pending state — with a monotonic guard so a live registration can never regress to pending on a correction resubmit. Connects to Advance milestone.
  6. Stage 6, Data layer: The Postgres tables that are the system of record for onboarding.
    1. Studios & milestones (Data store, PostgreSQL). One row per studio — key, name, legal and address fields, timezone, brand JSON, domain, lifecycle stage, CRM location and contact ids, registration type, intake-link sent-at — and seven milestone rows beneath it: welcome, intake, account setup, messaging compliance, branding, build, go-live, each with a status, an owner, an entered-at stamp and an ETA, cascade-deleted with the studio. Connects to Admin onboarding tracker when board status derived; Client tracker & corrections.
    2. Submissions & corrections (Data store, PostgreSQL (jsonb)). The autosaved draft keyed by token, the immutable submitted payload keyed by studio with a submitted-at stamp, and a corrections table recording each correction’s scope, target, reason category, message, who raised it, status and resolved-at stamp.
  7. Stage 7, Milestones & corrections: The seven-step tracker, the correction loop, the go-live flip and the boards.
    1. Advance milestone (Workflow, Astro API + Postgres). The admin sets a milestone’s status, owner and ETA; the intake milestone completes itself automatically on submit. Connects to Studios & milestones; Go-live flip when milestone 7 done.
    2. Flag a correction (Workflow, Astro API + Postgres + email). Raising a correction inserts the row, forces the field reference to null for milestone-scope corrections, flips the milestone to "needs you" with the client as owner, resolves the owner’s email and notifies them. Connects to Submissions & corrections; Client tracker & corrections when ownership moves to the client.
    3. Client tracker & corrections (Human, Astro pages (client-gated)). The studio’s own view of its seven milestones and percentage progress, plus the list of what needs amending and the resubmit path out of it — all gated on a client session that only ever resolves its own studio. Connects to Submit intake when resubmit.
    4. Go-live flip (Outcome, PostgreSQL). Completing the seventh milestone flips the studio’s lifecycle stage to live — a pure database operation with no CRM calls at all. Connects to Admin onboarding tracker; Studio live.
    5. Admin onboarding tracker (Report, Astro page). The agency’s cross-studio milestone board, with correction, advance, provisioning-retry and link-resend actions attached — each studio deriving to ready, blocked, aging past three weeks, or active, and feeding KPIs for studios in onboarding, average days to live, blocked or stalled, go-lives this week, and what needs the agency’s action. Connects to Agency-API provisioning when manual provisioning retry; Flag a correction; Advance milestone.
    6. Pipelines board (Report, Astro page + polled beacon). A kanban of live CRM opportunities with per-card signed scorecard links and appointment-date enrichment, refreshed by polling a lightweight "something changed" beacon rather than refetching the board. Connects to HMAC-signed link when per-card link.
    7. Studio live (Outcome, Platform + CRM). A provisioned sub-account, a completed intake, seven milestones done, a client login, and a scorecard rendering inside the studio’s own dashboard.
  8. Stage 8, Scorecard: The nine-point score — how it is stored, signed, embedded and revoked.
    1. Nine-point score (Report, TypeScript model). Nine graded elements in three groups of three — the core system, paid and active-search acquisition, and passive organic presence — each graded empty, red, amber or green with free-text comments, graded by hand rather than derived. Connects to Score storage & mirror.
    2. Score storage & mirror (CRM record, CRM custom values + fields). Each studio’s score is one JSON custom value in the agency’s own CRM location, all of them readable in a single list call; the nine states are additionally mirrored into nine per-contact dropdown fields, with the mirror deliberately skipped at conversion while the score is still empty. Connects to HMAC-signed link.
    3. HMAC-signed link (Guard, HMAC-SHA256). The widget URL carries the client key plus a base64url HMAC of it; verification splits on the last separator, length-checks and compares in constant time. Connects to Embedded scorecard when signature valid.
    4. Embedded scorecard (Report, Astro route in an iframe). A public, read-only, noindex route rendering the score as an interaction-disabled graphic, embedded as a custom-HTML widget inside each studio’s own CRM dashboard — and the only route exempted from the deployment’s deny-framing header. Connects to Studio live.
  9. Stage 9, Webhooks, crons & assurance: Real-time events, scheduled sweeps, lifecycle email, the isolated risk and the tests.
    1. CRM webhook receiver (Trigger, Astro API endpoints). Receives appointment, pipeline and conversion events authenticated by a shared secret compared in constant time — and if the secret is not configured it logs once and rejects everything. Two legacy lead receivers alongside it verify a real body HMAC, hex-decoded, length-checked and timing-safe. Connects to Normalize & dedupe.
    2. Normalize & dedupe (Guard, TypeScript + TTL map). Maps the CRM’s native payload into three internal event shapes — tolerating two misspelled vendor field names, ignoring a status field that describes the slot rather than the appointment, and trimming trailing spaces out of custom-data keys — then collapses re-deliveries on a key built from the event’s own shape, held in a self-pruning ten-minute map. Connects to Stateless orchestrator when not a re-delivery.
    3. Daily stage sweep (Scheduled job, Vercel cron (daily)). Four age-based rules retire stalled opportunities — two no-show stages after five days, a pending-next-call stage after a week, the deciding stage after three weeks — each writing a reason, moving the deal to long-term nurture with a recurring monthly touch, and re-stamping the entered-at timestamp, with per-rule error isolation and a structured run report. Connects to Eight-stage sales pipeline when moved to long-term nurture.
    4. Signup webhook (Trigger, Astro API endpoint). The checkout push, secret-authenticated across three possible carriers because the vendor may drop headers, which recovers the contact id from the email when the vendor’s merge field arrives empty, discards the location id when it resolves to the agency’s own account, and always answers 200 once authorized so the vendor does not hammer retries. Connects to Reconcile sub-account.
    5. Reconcile backstop cron (Scheduled job, Vercel cron (minutes)). Pages the account list, skips anything not activated on the right plan, and skips only studios that are both linked and already emailed — so a "created but never emailed" partial failure is repaired on the next pass. Connects to Reconcile sub-account when backstop, every few minutes.
    6. 20 lifecycle emails (Message, JSON definitions → renderer → CRM). Twenty branded emails across eight campaigns — call confirmation and reminder, two no-show drips, a pending-next-step drip, a five-touch conversion drip, three onboarding emails, a recurring long-term monthly, and a payment receipt — authored as structured JSON, rendered to reviewable HTML previews and pushed by name.
    7. Quarantined risk module (Guard, Node scripts outside the app). Five decisions were recorded before any code existed, and the one that carries real risk — using the vendor’s unsupported internal API to create workflows the public API cannot — is confined to a clearly marked toolchain living entirely outside the application source, with only two scripts ever touching a harvested token and everything built to draft for a human to publish. Connects to 20 lifecycle emails when workflows built to draft.
    8. Fail-closed test suite (Report, Vitest). Twenty-five test files whose security-relevant cases assert the invariants directly — unknown host denied, a client token rejected by the admin gate, a client token resolving only its own studio, submissions persisted before any CRM call, the enumeration-free uniform responses, one studio per CRM account enforced at the database level, and the idempotency of every retry path. Connects to Host-based router when asserts default-deny; One studio per account when asserts one studio per account; Submit intake when asserts no data loss; Admin & client auth when asserts no cross-studio access.
0/16

A studio owner buys the self-serve plan at 10pm on a Sunday — nobody at the agency is awake, and onboarding starts anyway.

Read this system as text
  1. Stage 01

    Access & tenancy

    The host router, token kinds and auth that keep two products safely inside one deployment.

    • Host-based router · Guard · Astro middleware — A pure, I/O-free decision function maps the request host to admin, portal, or nothing — and an unrecognised host resolves to null, which 404s every path. → Agency admin host (admin host), Client portal host (portal host), Three token kinds (unknown host → 404)
    • Agency admin host · Source · Astro (SSR) — The agency’s internal surface — pipelines board, onboarding tracker, metrics — with every client-portal path explicitly 404’d on this host. → Admin & client auth
    • Client portal host · Source · Astro (SSR) — The client-facing surface — start, welcome, the tokenized intake wizard, the tracker and corrections — with admin paths and admin APIs 404’d and a noindex header applied. → Admin & client auth, Intake wizard (public intake path)
    • Three token kinds · Guard · JWT (HS256, one secret) — Session (admin, 7 days), client (portal, 30 days) and intake (URL-borne, 14 days) tokens share a signing secret but carry distinct kind claims, alongside a short-lived magic-link kind. → Admin & client auth, Tokenized intake link (distinct kind claim)
    • Admin & client auth · Guard · Astro API + argon2id — Two gates: the agency signs in by a constant-time, non-short-circuiting password compare or an email magic link whose allowlist is re-checked at redemption, and the studio owner signs in with argon2id hashing, a five-failures-per-fifteen-minutes lockout and single-use expiring reset tokens. → Admin onboarding tracker (agency session), Pipelines board (agency session), Client tracker & corrections (client session, own studio only), Nine-point score (the agency grades by hand)
  2. Stage 02

    Sales capture & pipeline

    Lead capture, booking, and the orchestrator that owns every stage move.

    • Landing-page opt-in · Capture · Astro API endpoint — The public lead-capture endpoint — origin-restricted CORS plus a honeypot — upserts the contact into the CRM, tags the lead, stamps its source, and opens an opportunity on a two-stage pre-booking pipeline holding leads who have not yet booked. → Book strategy call, Eight-stage sales pipeline (call booked)
    • Book strategy call · Calendar · Astro API + CRM calendar — Free calendar slots are fetched server-side and proxied to the page so the integration token never reaches the browser; booking creates the appointment and, best-effort, advances the pre-booking opportunity to its booked stage. → CRM webhook receiver (appointment event)
    • Stateless orchestrator · Workflow · TypeScript over the CRM API — A single module owning every stage move, tag, field write, task and campaign enrolment; it keeps no state of its own — every contact’s state lives in CRM custom fields. → Resolve-by-name, Eight-stage sales pipeline, 20 lifecycle emails (exclusive enrolment)
    • Resolve-by-name · API / service · CRM API + in-process cache — Pipelines, stages, workflows and contact custom fields are looked up by their human name at runtime and cached for five minutes; no raw CRM id lives in the code. If a named workflow does not exist yet, enrolment returns false and logs which one to build while the stage move and tags still apply. → Eight-stage sales pipeline (stage + tags apply either way)
    • Eight-stage sales pipeline · CRM record · GoHighLevel pipeline — Call booked, no-show, done or pending next call, next call scheduled, its no-show, deciding, won, long-term nurture — each transition driven by an appointment or pipeline event, and a second no-show creating a task for the agency principal to phone the prospect personally. → Daily stage sweep (stage ages out), Won → client (won — client)
  3. Stage 03

    Signed → onboarding

    The moment a won deal becomes a studio record, a milestone set and a tokenized intake link.

    • Won → client · Workflow · Orchestrator — One routine moves the deal to won, tags the contact as a client, removes every sales campaign, stamps the signed date, creates the onboarding record, enrols the onboarding campaign and opens a fulfilment task. → Create studio record
    • Create studio record · Data store · PostgreSQL + Drizzle — Creates the studio row and all seven milestone rows in one transaction, mints a unique studio key, generates the intake token, writes the intake URL back to the CRM contact and seeds an empty scorecard — idempotent on the CRM contact id and wrapped so any failure returns null rather than breaking the sales stage move. → Studios & milestones, Tokenized intake link, Score storage & mirror (seed an empty score)
    • Tokenized intake link · Guard · JWT (14-day) — The intake link is a signed 14-day token carrying the studio key and its own kind claim; expired or malformed tokens land on a dedicated expiry page rather than an error. → Intake wizard
  4. Stage 04

    Provisioning & reconcile

    Two routes to a live CRM sub-account, and the reconciliation that makes them converge on one.

    • Self-serve checkout · Source · CRM hosted checkout — For self-serve signups the CRM’s own checkout creates the sub-account, loads the snapshot, enrols the plan and enables rebilling; the platform only ever reacts to that. → Signup webhook, Reconcile sub-account (welcome-page poll)
    • Reconcile sub-account · Scheduled job · TypeScript + Postgres — Verifies the location is genuinely activated on the right plan, enriches its name and email by paging the account list, then links or creates exactly one studio. → One studio per account, Send-once gate
    • One studio per account · Data store · Postgres partial unique index — A partial unique index on the CRM location id, only where non-null, makes duplicate creation a constraint violation — which the code detects by walking the error’s cause chain and then re-reads the winning row. → Create studio record (winner re-read)
    • Send-once gate · Guard · Postgres timestamp — The CRM handoff — link the contact, write the intake URL, enrol the onboarding campaign — happens once and is gated on a sent-at timestamp; if the contact does not exist in the CRM yet it no-ops and the cron retries. → Tokenized intake link (intake URL written), 20 lifecycle emails (a CRM workflow sends the welcome), Reconcile backstop cron (contact not in CRM yet — retry)
    • Agency-API provisioning · API / service · CRM agency API — A direct sub-account creation call built from the latest intake submission, with a business-address-to-tax-address fallback and an early return if the studio is already linked — deliberately no longer wired into the intake path and reachable only from an admin retry button. → Reconcile sub-account
  5. Stage 05

    Client intake portal

    The wizard the studio owner fills in, and everything that protects it.

    • Intake wizard · Capture · Astro page (token-scoped) — The multi-step intake form the studio owner opens straight from the tokenized link with no login, autosaved server-side against the token rather than in the browser. → Validated upload, Submit intake
    • Validated upload · API / service · Astro API endpoint — Logo and document uploads are size-capped, MIME-allowlisted, and then actually decoded to check their magic bytes — a container claiming to be an image but isn’t is rejected outright. → Submit intake (magic bytes valid)
    • Submit intake · Workflow · Astro API + Postgres — Validates server-side, writes the submission to Postgres first, advances the intake milestone, and only then syncs to the CRM and provisions the client login — CRM failures become warnings on the response, never lost data. → Submissions & corrections (Postgres first), Messaging-compliance branch, Provision client login, Advance milestone (intake milestone auto-done)
    • Provision client login · Workflow · argon2 + CRM workflow — On first submit the app mints a readable temporary password with ambiguous glyphs removed, stores only its hash, writes the plaintext to a CRM contact field, and lets a dedicated CRM workflow deliver it and then clear the field — returning null if a password already exists. → Admin & client auth (credentials delivered by a CRM workflow), Studio live
    • Messaging-compliance branch · Decision · Intake step + CRM field — The intake asks whether the studio has a federal tax id and branches into a standard registration, a sole-proprietor fallback, or a pending state — with a monotonic guard so a live registration can never regress to pending on a correction resubmit. → Advance milestone
  6. Stage 06

    Data layer

    The Postgres tables that are the system of record for onboarding.

    • Studios & milestones · Data store · PostgreSQL — One row per studio — key, name, legal and address fields, timezone, brand JSON, domain, lifecycle stage, CRM location and contact ids, registration type, intake-link sent-at — and seven milestone rows beneath it: welcome, intake, account setup, messaging compliance, branding, build, go-live, each with a status, an owner, an entered-at stamp and an ETA, cascade-deleted with the studio. → Admin onboarding tracker (board status derived), Client tracker & corrections
    • Submissions & corrections · Data store · PostgreSQL (jsonb) — The autosaved draft keyed by token, the immutable submitted payload keyed by studio with a submitted-at stamp, and a corrections table recording each correction’s scope, target, reason category, message, who raised it, status and resolved-at stamp.
  7. Stage 07

    Milestones & corrections

    The seven-step tracker, the correction loop, the go-live flip and the boards.

    • Advance milestone · Workflow · Astro API + Postgres — The admin sets a milestone’s status, owner and ETA; the intake milestone completes itself automatically on submit. → Studios & milestones, Go-live flip (milestone 7 done)
    • Flag a correction · Workflow · Astro API + Postgres + email — Raising a correction inserts the row, forces the field reference to null for milestone-scope corrections, flips the milestone to "needs you" with the client as owner, resolves the owner’s email and notifies them. → Submissions & corrections, Client tracker & corrections (ownership moves to the client)
    • Client tracker & corrections · Human · Astro pages (client-gated) — The studio’s own view of its seven milestones and percentage progress, plus the list of what needs amending and the resubmit path out of it — all gated on a client session that only ever resolves its own studio. → Submit intake (resubmit)
    • Go-live flip · Outcome · PostgreSQL — Completing the seventh milestone flips the studio’s lifecycle stage to live — a pure database operation with no CRM calls at all. → Admin onboarding tracker, Studio live
    • Admin onboarding tracker · Report · Astro page — The agency’s cross-studio milestone board, with correction, advance, provisioning-retry and link-resend actions attached — each studio deriving to ready, blocked, aging past three weeks, or active, and feeding KPIs for studios in onboarding, average days to live, blocked or stalled, go-lives this week, and what needs the agency’s action. → Agency-API provisioning (manual provisioning retry), Flag a correction, Advance milestone
    • Pipelines board · Report · Astro page + polled beacon — A kanban of live CRM opportunities with per-card signed scorecard links and appointment-date enrichment, refreshed by polling a lightweight "something changed" beacon rather than refetching the board. → HMAC-signed link (per-card link)
    • Studio live · Outcome · Platform + CRM — A provisioned sub-account, a completed intake, seven milestones done, a client login, and a scorecard rendering inside the studio’s own dashboard.
  8. Stage 08

    Scorecard

    The nine-point score — how it is stored, signed, embedded and revoked.

    • Nine-point score · Report · TypeScript model — Nine graded elements in three groups of three — the core system, paid and active-search acquisition, and passive organic presence — each graded empty, red, amber or green with free-text comments, graded by hand rather than derived. → Score storage & mirror
    • Score storage & mirror · CRM record · CRM custom values + fields — Each studio’s score is one JSON custom value in the agency’s own CRM location, all of them readable in a single list call; the nine states are additionally mirrored into nine per-contact dropdown fields, with the mirror deliberately skipped at conversion while the score is still empty. → HMAC-signed link
    • HMAC-signed link · Guard · HMAC-SHA256 — The widget URL carries the client key plus a base64url HMAC of it; verification splits on the last separator, length-checks and compares in constant time. → Embedded scorecard (signature valid)
    • Embedded scorecard · Report · Astro route in an iframe — A public, read-only, noindex route rendering the score as an interaction-disabled graphic, embedded as a custom-HTML widget inside each studio’s own CRM dashboard — and the only route exempted from the deployment’s deny-framing header. → Studio live
  9. Stage 09

    Webhooks, crons & assurance

    Real-time events, scheduled sweeps, lifecycle email, the isolated risk and the tests.

    • CRM webhook receiver · Trigger · Astro API endpoints — Receives appointment, pipeline and conversion events authenticated by a shared secret compared in constant time — and if the secret is not configured it logs once and rejects everything. Two legacy lead receivers alongside it verify a real body HMAC, hex-decoded, length-checked and timing-safe. → Normalize & dedupe
    • Normalize & dedupe · Guard · TypeScript + TTL map — Maps the CRM’s native payload into three internal event shapes — tolerating two misspelled vendor field names, ignoring a status field that describes the slot rather than the appointment, and trimming trailing spaces out of custom-data keys — then collapses re-deliveries on a key built from the event’s own shape, held in a self-pruning ten-minute map. → Stateless orchestrator (not a re-delivery)
    • Daily stage sweep · Scheduled job · Vercel cron (daily) — Four age-based rules retire stalled opportunities — two no-show stages after five days, a pending-next-call stage after a week, the deciding stage after three weeks — each writing a reason, moving the deal to long-term nurture with a recurring monthly touch, and re-stamping the entered-at timestamp, with per-rule error isolation and a structured run report. → Eight-stage sales pipeline (moved to long-term nurture)
    • Signup webhook · Trigger · Astro API endpoint — The checkout push, secret-authenticated across three possible carriers because the vendor may drop headers, which recovers the contact id from the email when the vendor’s merge field arrives empty, discards the location id when it resolves to the agency’s own account, and always answers 200 once authorized so the vendor does not hammer retries. → Reconcile sub-account
    • Reconcile backstop cron · Scheduled job · Vercel cron (minutes) — Pages the account list, skips anything not activated on the right plan, and skips only studios that are both linked and already emailed — so a "created but never emailed" partial failure is repaired on the next pass. → Reconcile sub-account (backstop, every few minutes)
    • 20 lifecycle emails · Message · JSON definitions → renderer → CRM — Twenty branded emails across eight campaigns — call confirmation and reminder, two no-show drips, a pending-next-step drip, a five-touch conversion drip, three onboarding emails, a recurring long-term monthly, and a payment receipt — authored as structured JSON, rendered to reviewable HTML previews and pushed by name.
    • Quarantined risk module · Guard · Node scripts outside the app — Five decisions were recorded before any code existed, and the one that carries real risk — using the vendor’s unsupported internal API to create workflows the public API cannot — is confined to a clearly marked toolchain living entirely outside the application source, with only two scripts ever touching a harvested token and everything built to draft for a human to publish. → 20 lifecycle emails (workflows built to draft)
    • Fail-closed test suite · Report · Vitest — Twenty-five test files whose security-relevant cases assert the invariants directly — unknown host denied, a client token rejected by the admin gate, a client token resolving only its own studio, submissions persisted before any CRM call, the enumeration-free uniform responses, one studio per CRM account enforced at the database level, and the idempotency of every retry path. → Host-based router (asserts default-deny), One studio per account (asserts one studio per account), Submit intake (asserts no data loss), Admin & client auth (asserts no cross-studio access)

Platforms · Astro · PostgreSQL · Drizzle · Vercel · GoHighLevel

What I built

Two products, one deployment, default-deny

A pure, I/O-free function maps the request host to admin, portal, or nothing. An unrecognised host resolves to null, which 404s every path. On the admin host every client-portal path is explicitly 404'd; on the portal host every admin path and admin API is 404'd and a noindex header applied. Without default-deny at the host layer, a portal request could reach an admin handler.

The admin and client auth gates both fail closed. If neither admin credential path is configured, login is disabled entirely rather than left open.

One orchestrator owning the state machine

A single stateless module owns every stage move, tag, field write, task and campaign enrolment. It keeps no state of its own, because every contact's state already lives in CRM custom fields. CRM workflows are demoted to sending the message rather than deciding the state, which means there is one place to reason about the machine.

Nothing in the code holds a raw CRM id. Pipelines, stages, workflows and contact custom fields are resolved by their human name at runtime and cached briefly. That is what makes the platform replicable: the same deployment runs against any new sub-account.

Sign to studio, in one transaction

When a deal is won, one routine moves the stage, tags the contact as a client, removes every sales campaign, stamps the signed date, creates the onboarding record, enrols the onboarding campaign and opens a fulfilment task. Doing it as one ordered routine is what stops a signed client still receiving sales drips.

Creating the studio row and all seven milestone rows happens in one transaction, mints a unique studio key, generates the intake token, writes the intake URL back to the CRM contact, and seeds an empty scorecard.

Provisioning, and the honest version of it

The live path is the vendor's own hosted checkout. It creates the sub-account, loads the snapshot, enrols the plan and enables rebilling, and the platform reacts to that. There is deliberately no headless enrolment: the vendor owns provisioning and the application owns reconciliation.

A reconciler verifies the location is genuinely activated on the right plan, enriches its name and email by paging the account list, and then links or creates exactly one studio. A partial unique index on the CRM location id, only where non-null, makes duplicate creation a constraint violation. That converts a distributed race between a page poll and a cron into a database invariant, and the losing side walks the error's cause chain and re-reads the winner's row.

The direct agency-API provisioning call still exists, but it is no longer wired into the intake path. It was creating a duplicate blank location alongside the one the checkout had already made. Rather than delete it, I demoted it to a manual repair tool reachable only from an admin retry button.

The intake portal

The studio owner opens the wizard straight from the tokenized link with no login. On submit the app validates server-side, writes the submission to Postgres first, advances the intake milestone, and only then syncs to the CRM. CRM failures become warnings on the response, never lost data.

Onboarding is modelled in eight Postgres tables. Drafts and submissions are kept separate, and a dedicated corrections table records each correction's scope, target, reason category, message, who raised it, status and resolved-at stamp. So a correction cycle produces a new submission rather than mutating history, and the studio amends its own intake without the agency re-keying it.

The scorecard

Nine graded elements in three groups of three, covering the core system, paid and active-search acquisition, and passive organic presence. Each is graded empty, red, amber or green with free-text comments, and graded by hand rather than derived.

Each studio's score is one JSON custom value in the agency's own CRM location, so the whole board is one read. The widget URL carries the client key plus an HMAC of it, verified in constant time, because without the signature anyone could read any studio's scorecard by guessing keys. It renders as a read-only, noindex graphic embedded in the studio dashboard. The agency edits once and every dashboard reflects it with nothing to re-push.

Edge cases and failure handling

  • The forgot-password endpoint answers identically whether or not the email exists, and performs no match-dependent work, so both the enumeration oracle and the timing oracle close. The same uniform-response rule covers magic-link requests and the onboarding-status lookup.
  • The welcome page polls every few seconds, so the CRM handoff is gated on a sent-at timestamp. Without it every poll would re-enrol the client and re-send the email.
  • Uploads are size-capped, MIME-allowlisted, and then actually decoded to check their magic bytes, because extension and content type are both attacker-controlled.
  • Webhook re-deliveries are collapsed on a key built from the event's own shape, held in a self-pruning ten-minute map. A generic dedupe would swallow real transitions, which matters because the no-show counter is not idempotent.
  • The signup webhook discards the location id when it resolves to the agency's own account. Without that, the vendor's merge field would have collided every self-serve buyer onto a single studio record.
  • The go-live flip is a pure database operation with no CRM calls at all, chosen so the most consequential state change cannot be blocked by a third-party outage.
  • Idempotency for webhooks, the opportunity cache and the login throttle is in-process, which holds across warm instances and is not claimed to hold across them.

How I verified it

Twenty-five test files assert the invariants directly rather than testing around them: an unknown host is denied, a client token is rejected by the admin gate, a client token resolves only its own studio, submissions are persisted before any CRM call, the uniform enumeration-free responses hold, one studio per CRM account is enforced at the database level, and every retry path is idempotent.

The twenty lifecycle emails were authored as structured JSON and rendered to reviewable HTML previews, so copy was reviewed before it ever existed in the CRM, and re-pushing is idempotent by name.

Five decisions were recorded in writing before any code existed. The one carrying real risk, using the vendor's unsupported internal API to create workflows the public API cannot, is confined to a marked toolchain living entirely outside the application source.

What changed

Both portals are live on the agency's own custom domain, running as one deployment across two hosts. A studio signs, the studio record and its seven milestones are created in one transaction, and a tokenized intake link goes out without anyone opening a checklist.

The agency has one tracker across every studio.

The studio owner sees their own milestones and progress in a branded portal, raises and clears corrections there, and fixes their own data so the agency never transcribes it back in.

The nine-point scorecard renders live inside each client's own CRM dashboard behind a signed embed. Twenty branded lifecycle emails ship across eight campaigns, from strategy call and reminder through two no-show drips to a receipt.

The agency owns the decision log too, including the one risk explicitly accepted and quarantined outside the application source.

Before / after

  • Before
  • ✕ Onboarding = a 90-minute checklist run by hand
  • ✕ No view across studios; progress lived in someone’s head
  • ✕ Client-facing status = screenshots and phone calls
  • ✕ Automation hardcoded to one account’s IDs
  • After
  • ✓ Sub-account provisioned and intake link sent from the admin
  • ✓ One tracker across every studio — milestones and pipelines
  • ✓ A branded scorecard live inside each client’s own dashboard
  • ✓ Pipelines resolved by name — same code, any sub-account

ScopeFigures on this page describe what was built, covered or verified — not a measured lift.

Have something close to this? Book a call. I’ll tell you if I’ve already built it.

One Manila business day to reply. All client work is under NDA; what you see here is redrawn architecture with real metrics.

Book