Skip to content

Work · Website + CRM build · Martial-arts academy · same client as CS-01, via agency

Replicable studio website + CRM template

One studio needed a site. Every next studio needed the same one.

A server-rendered marketing and booking site wired directly into the studio’s CRM — the CRM stays the system of record, the site is a thin validated layer — then generalized into a fork-per-client template: a single completed intake doc plus a nine-phase build produces the next studio’s live site.

  • Astro
  • Vercel
  • GoHighLevel
  • Tailwind
  • Playwright
  • Role · sole designer & builder
  • 8 min
pages · 17 endpoints, live on the studio’s domain
26Scope
build phases from intake doc to a new studio live
9Scope
operator CLIs shipped with the site
5Scope
System size
System flow · 10 stages · 44 components reconstructed from the build record

The situation

The agency needed a marketing and booking site live for one studio location. Then they needed the same system for the next locations, and the ones after that, without rebuilding it each time. A site that only works for one client is a project. A site that forks is an asset.

The second requirement mattered as much as the first. Studio staff had to be able to change trial deadlines, prices and copy themselves, without a developer and without a redeploy. If every small edit queues behind an engineer, the site stops reflecting the business within a month.

The CRM was already the studio's system of record and had to stay that way. So the site is a thin validated layer over it rather than a second database that slowly disagrees with the first.

What was broken

  • The studio had no site, and every future location would have meant starting again from scratch.
  • CRM calls were scattered wherever they were needed, so the fatal-versus-non-fatal boundary got re-argued at every call site.
  • Copy and deadline changes queued behind a developer and a redeploy.
  • Two forms posted from the browser straight to a public CRM webhook with no protection at all.
  • Handover happened as a phone call rather than a document, so nothing about the system outlived the conversation.
  • Nothing was watching production. When bookings later faltered, the failure had already been running for weeks.

Constraints

  • The CRM stays the system of record, and the site may not become a competing one.
  • Staff-editable settings had to live in the CRM, not in environment variables, so nothing needing a redeploy could be a business setting.
  • The platform's free-slot API hides a class slot after its first booking, which is wrong for class calendars that hold many students.
  • Three of the nine replication phases genuinely cannot be automated, so they had to become human sign-off gates rather than pretend automation.
  • Serverless runtime means in-memory idempotency only holds across warm instances, which had to be stated rather than implied away.
  • Consent language had to be captured at the point of collection, because that is the only version defensible later.

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
10
stages
decisions & guards
12
decisions & guards
AI
0
AI
human handoffs
1
human handoffs
platforms
5
platforms
  1. Stage 1, Acquisition surfaces: Every page a prospect can arrive on, and how the page they landed on is remembered.
    1. Marketing & SEO pages (Source, Astro (SSR)). The public site — home, program pages, contact, reviews, legal — plus a primary local page and neighbouring-city pages each carrying local-business, breadcrumb and FAQ structured data. Connects to Opt-in form; CRM chat widget.
    2. Six ad landing pages (Source, Astro (chromeless layout)). Six channel- and audience-specific landers over shared components, each stamping its own lead source, all noindex with a canonical pointing at the organic page and excluded from the sitemap. Connects to Opt-in form when per-channel lead source.
    3. Funnel & campaign pages (Source, Astro + CRM custom value). A post-opt-in booking funnel page that personalises and prefills from query parameters, an in-studio QR page stamping a walk-in source, and a win-back campaign page whose offer deadline is read at request time from a CRM custom value — all stripped of navigation, footer, chat and sticky CTAs. Connects to Program survey.
    4. CRM chat widget (Source, GoHighLevel chat + workflow). The CRM chat widget on the highest-traffic pages; a CRM workflow fires on a chat contact being created and posts it into the site’s own lead endpoint with a shared secret header. Connects to Single lead endpoint when server-to-server, secret header.
    5. Blog + internal CMS (API / service, Astro + CRM blogs + blob storage). A blog rendered from the CRM’s own blog store with post bodies in blob storage sanitised against an allowlist, plus a token-gated editor iframed into the CRM, and a server-rendered sitemap for posts created after build time. Connects to Marketing & SEO pages.
  2. Stage 2, Lead capture: The single server endpoint every opt-in goes through, and the layers protecting it.
    1. Opt-in form (Capture, Astro component). The shared opt-in form across every marketing surface, carrying explicit SMS-consent and opt-out language and a marketing-consent field. Connects to Single lead endpoint.
    2. Single lead endpoint (API / service, Astro API endpoint). One server endpoint every opt-in flows through — home, program pages, contact, QR, ads and chat — which upserts the contact, writes lead-source and page fields, applies tags and opens a lead-acquisition opportunity, deduplicated by a daily hash of email, trainee and source. Connects to Four anti-abuse layers; Contact upsert; Funnel & campaign pages when redirect with prefill.
    3. Four anti-abuse layers (Guard, Astro API endpoints). Schema validation, a honeypot field, a minimum dwell time and a per-IP rate bucket, in that order — with honeypot and dwell failures returning a successful-looking response carrying a discard marker. Connects to Single lead endpoint when silent discard on honeypot / dwell.
  3. Stage 3, Program qualification: Turning "who is this for and how old are they" into one of five programs.
    1. Program survey (Decision, Astro component). Two questions — booking for yourself or someone else, then an age tier — resolving to one of five programs, each mapping to two different calendars depending on whether this is a first trial or a re-enrolment. Connects to Booking state machine.
    2. Booking state machine (Capture, Astro component). A six-state client-side flow — survey, date picker, slot picker, trainee form, success, error — all pre-rendered into the DOM with the controller toggling visibility. Connects to Availability endpoint; Booking endpoint.
  4. Stage 4, Availability: Producing slots that are genuinely bookable, against a calendar API that hides them.
    1. Availability endpoint (API / service, Astro API endpoint). Takes a program, a flow and a date range capped at five weeks, resolves the right calendar, fetches calendar config, existing events and free slots in parallel, and serves the result behind a short CDN cache with a longer stale-while-revalidate window. Connects to Slot template generator.
    2. Slot template generator (Calendar, Pure TypeScript). Generates candidate slots from one typed weekly schedule minus a blackout list, enforcing a minimum lead time before the next bookable class — the same schedule data that renders the public schedule page. Connects to Class-capacity reconciler.
    3. Class-capacity reconciler (Guard, TypeScript). Class calendars accept many bookings per slot, but the platform’s free-slot API stops returning a slot after the first booking — so an empty template slot still needs free-slot confirmation, while a partly-booked slot stays open as long as confirmed events are under capacity. Connects to Booking state machine when genuinely bookable slots; Booking endpoint when slot taken → 3 alternates.
  5. Stage 5, Booking: Creating the appointment, and deciding what may and may not fail after it.
    1. Booking endpoint (API / service, Astro API endpoint). Validates the payload, runs the same four anti-abuse layers, re-validates capacity at submit time, and on a lost slot returns a typed error together with the next three available slots over a two-week window; logs are redacted to a hashed email plus presence flags. Connects to Four anti-abuse layers; Class-capacity reconciler when re-validate at submit; Create appointment.
    2. Create appointment (Calendar, CRM calendar API). Computes an end time by program, writes the appointment with a structured title, and passes both a capacity override and an existing event’s assigned staff member because the platform rejects the booking otherwise; then writes a structured appointment note with a correctly formatted local date. Connects to Trial vs re-enrolment; Pre-trial reminders when native appointment trigger.
    3. Trial vs re-enrolment (Decision, Orchestration module). A dispatcher splits first-trial bookings from re-enrolment bookings into two entirely separate handlers touching different pipelines, with cross-contamination ruled out by design. Connects to Trial conversion pipeline when first trial; Re-enrolment pipeline when re-enrolment.
    4. Trial booked (Outcome, Site + CRM). A confirmed appointment on the right program calendar, a per-trainee opportunity with its data, the acquisition record consolidated and won, nurture exited, and reminders scheduled. Connects to Trial-credit pipeline when first attendance.
  6. Stage 6, CRM writes: Contacts, trainee identity, and the three pipelines the site drives.
    1. Contact upsert (CRM record, CRM contacts API). Upserts by email and phone, sets the native source attribute to the marketing channel — then re-writes it on a second call, because the upsert endpoint only sets that attribute on creation — and writes lead source, opt-in page, last page and trainee-key fields with funnel and source tags. Connects to Trainee identity key.
    2. Trainee identity key (Guard, Pure TypeScript). A deterministic, diacritic-stripped key derived from first name plus either a self marker or a date of birth, with an explicit rule never to compare on first and last name directly. Connects to Trial conversion pipeline; Trial-credit pipeline; Trainee card resolver.
    3. Trial conversion pipeline (CRM record, GoHighLevel pipeline). One opportunity per trainee carrying trainee name, date of birth, program, last appointment id and start time, a local-format appointment date and an appointment history; an existing opportunity for that trainee key is moved rather than duplicated. Connects to Nurture exit; Trial booked.
    4. Re-enrolment pipeline (CRM record, GoHighLevel pipeline). The win-back path touches only its own pipeline — an existing trainee moves to a booked stage, while a new trainee causes the imported former-student household record to be deleted and replaced with one opportunity per person. Connects to Nurture exit.
    5. Trial-credit pipeline (CRM record, GoHighLevel pipeline). On first attendance a credit-monitoring opportunity is created or refreshed, the trial-pass credit count is initialised from a staff-editable default, and a long-lived rebooking token is minted onto the record; each subsequent attendance decrements the count behind an idempotency guard. Connects to Rebooking entry when magic link minted at activation.
    6. Staff-editable settings (CRM record, CRM custom values). Seventeen declared settings — trial-pass size, seven pipeline-timer durations, the webhook base URL and secret, the campaign deadline, page URL and offer name, the brand name and two revenue values — fetched behind a short server cache that swallows refresh errors and falls back to the last known value. Connects to Funnel & campaign pages when deadline read at request time; Trial-credit pipeline when trial-pass size.
  7. Stage 7, Reminders & replies: What happens between booking and attending — and when a human must take over.
    1. Pre-trial reminders (Message, GoHighLevel workflow). A reminder ladder running from days before the class down to minutes before it, fired by the platform’s native appointment trigger rather than by the site. Connects to Inbound-reply handover when customer replies; Customer cancellation when cancel link.
    2. Nurture exit (Guard, Orchestration module). Booking removes the contact from the relevant nurture campaigns — a dedicated exit call per flow, so a trial booking clears trial nurture and a re-enrolment booking clears the campaign’s own sequences. Connects to Trial booked when chase sequences stopped.
    3. Inbound-reply handover (Staff alert, CRM workflow + conversations). Any inbound reply removes the contact from six nurture drips — deliberately excluding the pre-trial reminders and the confirmation sequence, which are logistics for a class already booked — and applies a tag that is the staff work queue, cleared by hand once a human answers. Connects to Booking diagnostic register when half of replies unanswered.
    4. Customer cancellation (API / service, Astro API endpoint). A signed cancel token, verified in constant time and cross-checked against the appointment and contact it claims, sets the CRM appointment to cancelled and dispatches the cancellation handler, deduplicated for a week. Connects to Status transition table.
    5. Status transition table (Decision, Declarative config). The one table-driven path in the system: appointment status maps to a stage move per pipeline, every move guarded by which stages it is allowed to move from — with the credit pipeline deliberately returning a cancelled booking to its active stage because the trial pass is still valid. Connects to Trial conversion pipeline when showed / no-show; Trial-credit pipeline when cancelled → back to active.
  8. Stage 8, Self-service rebooking: The customer-owned path to book again, change program, add a family member, or cancel.
    1. Rebooking entry (Source, Astro page + two endpoints). Two ways in — a long-lived signed magic link minted at trial activation and merged into reminder messages, or an email lookup that is rate-limited per IP per hour and returns a generic not-found on a miss. Connects to Trainee card resolver.
    2. Trainee card resolver (Decision, Pure TypeScript). Merges open credit records with all-status trial records into one card per trainee, resolved by a four-level priority: enrolled, active with credits remaining, trial complete, or booked-but-not-yet-attended. Connects to Short-lived action tokens.
    3. Short-lived action tokens (Guard, HMAC). Each actionable card carries its own fifteen-minute token, plus one contact-scoped token using a sentinel trainee key that authorises adding a new family member — so the long-lived magic link never appears in any subsequent request body. Connects to Self-service actions when 15-minute scoped token.
    4. Self-service actions (Workflow, Astro API endpoints). Four customer-owned actions — book another class against a remaining credit, permanently change a trainee’s program, add a new family member’s first trial, and cancel — each verifying the token, cross-checking that the contact and trainee in the body match the token’s own payload, and resolving the parent’s details server-side so no personal data travels in the request. Connects to Create appointment when book another / add a person; Trial-credit pipeline when credit consumed; Trial conversion pipeline when new family member.
  9. Stage 9, CRM backflow: Webhooks reconciling CRM-side and agent-side state back into the site’s model.
    1. Webhook ingress guards (Guard, Astro API endpoints). Every inbound webhook shares one gate — a shared-secret header compared in constant time and never logged, defensive normalisation that tolerates a misspelled platform field name and rejects any value still containing an unresolved merge tag, schema validation, a one-minute idempotency window, and always an HTTP 200 so the platform does not retry forever. Connects to Three backflow handlers; AI-agent booking backflow when concierge booked natively.
    2. Three backflow handlers (Trigger, Astro webhook endpoints). Three handlers listen back to the CRM: trial-stage moves activate a pass, mark a lead lost or close out an enrolment with its value and date; credit-stage moves decrement on attendance, zero a lost trial or close a won one; and appointment-status changes mirror a status set in the calendar UI onto the owning opportunity, normalising the several spellings of "no show". Connects to Trial-credit pipeline when attended → decrement; Trial conversion pipeline when won / lost; Status transition table when status set in the calendar UI.
    3. AI-agent booking backflow (Trigger, Astro webhook endpoint). Bridges the gap left when the SMS booking concierge books through the platform’s native action, bypassing the site — it re-derives the program from the trainee’s age rather than trusting the agent’s calendar choice, re-fetches the appointment by id because the merge field carries a locale-formatted date, warns on win-back-tagged and off-segment contacts, then runs the same booking orchestration and campaign exits as a website booking. Connects to Trial conversion pipeline when agent booking reconciled; Ghost-appointment risk when any failure branch.
    4. Ghost-appointment risk (Guard, Documented defect). Every failure branch of the agent backflow — invalid age, invalid input, contact not found, appointment not found, or any orchestration throw — returns 200 while the appointment already exists on the calendar, producing an appointment with no pipeline record. Connects to Booking diagnostic register.
  10. Stage 10, Replication & assurance: The orchestrator, the operator tooling, the replication kit, and the audits.
    1. Single CRM orchestrator (Workflow, TypeScript module). Declared as the only thing endpoints may call into for CRM operations — it exposes the lifecycle handlers (opt-in, booking, attendance, credit decrement, cancellation, status change) and nothing bypasses it. Connects to Contact upsert; Trial conversion pipeline; Trial-credit pipeline; Nurture exit.
    2. Idempotency store (Data store, In-memory TTL map). A bounded, sweeping map with per-surface keys and windows — a day for lead submissions, a week for cancellations, a minute for webhooks — with an unimplemented durable backend declared in the environment schema. Connects to Single lead endpoint; Customer cancellation; Webhook ingress guards.
    3. Three signing schemes (Guard, HMAC-SHA256). Rebooking, cancellation and admin access all use the same stateless pattern — an HMAC over a pipe-joined payload, base64url-encoded, compared in constant time — with different payloads, different keys and very different lifetimes, and the admin token deliberately carrying no user data at all. Connects to Rebooking entry; Customer cancellation; Blog + internal CMS when admin scope.
    4. Declarative CRM schema (Data store, TypeScript config module). A single source-of-truth file declaring every pipeline, stage, contact and opportunity custom field, custom value, workflow, tag and environment variable the system expects — and a key-gated health endpoint that compares that declaration against the live account and reports missing variables, pipeline drift and a live settings read. Connects to Five operator CLIs; Replication kit; Booking diagnostic register when drift check.
    5. Five operator CLIs (Scheduled job, Node scripts). An onboarding CLI with checklist, discover and provision modes; a blog-onboarding CLI that is dry-run by default and always prints a manual fallback; an intake validator that rejects placeholder values; a read-only messaging-health audit; and a rescue tool for stranded records. Connects to Replication kit; Booking diagnostic register.
    6. Replication kit (Report, 21-document corpus). Nine phases from preflight to handoff, three human sign-off gates, a 13-part client intake questionnaire, and seven hard rules — no silent invention, no production promotion, respect project memory, settings in the CRM not in environment variables, PII discipline, idempotent re-runs, halt on any failed phase.
    7. Adversarial security audit (Report, Three parallel browser agents). Three parallel adversarial agents — one capturing every outbound request from live pages, one fetching and grepping the shipped bundle for credentials and source maps, one throwing fourteen adversarial payloads at the booking and personalisation surfaces — plus a static review, producing a ranked finding list with zero test data created. Connects to Single lead endpoint when browser→CRM leak closed by a server door.
    8. Booking diagnostic register (Report, Five-day audit). A seventeen-finding register graded critical to low, tracing a booking collapse to duplicate nurture enrolment, duplicate appointment reminders, timers three to four times too long, unanswered inbound replies, and — the meta-cause — nothing watching production at all. Connects to Replication kit when do not clone the reference build yet.
0/23

A parent clicks a paid ad for kids classes, opts in, books a trial — then rebooks and adds a sibling a fortnight later.

Read this system as text
  1. Stage 01

    Acquisition surfaces

    Every page a prospect can arrive on, and how the page they landed on is remembered.

    • Marketing & SEO pages · Source · Astro (SSR) — The public site — home, program pages, contact, reviews, legal — plus a primary local page and neighbouring-city pages each carrying local-business, breadcrumb and FAQ structured data. → Opt-in form, CRM chat widget
    • Six ad landing pages · Source · Astro (chromeless layout) — Six channel- and audience-specific landers over shared components, each stamping its own lead source, all noindex with a canonical pointing at the organic page and excluded from the sitemap. → Opt-in form (per-channel lead source)
    • Funnel & campaign pages · Source · Astro + CRM custom value — A post-opt-in booking funnel page that personalises and prefills from query parameters, an in-studio QR page stamping a walk-in source, and a win-back campaign page whose offer deadline is read at request time from a CRM custom value — all stripped of navigation, footer, chat and sticky CTAs. → Program survey
    • CRM chat widget · Source · GoHighLevel chat + workflow — The CRM chat widget on the highest-traffic pages; a CRM workflow fires on a chat contact being created and posts it into the site’s own lead endpoint with a shared secret header. → Single lead endpoint (server-to-server, secret header)
    • Blog + internal CMS · API / service · Astro + CRM blogs + blob storage — A blog rendered from the CRM’s own blog store with post bodies in blob storage sanitised against an allowlist, plus a token-gated editor iframed into the CRM, and a server-rendered sitemap for posts created after build time. → Marketing & SEO pages
  2. Stage 02

    Lead capture

    The single server endpoint every opt-in goes through, and the layers protecting it.

    • Opt-in form · Capture · Astro component — The shared opt-in form across every marketing surface, carrying explicit SMS-consent and opt-out language and a marketing-consent field. → Single lead endpoint
    • Single lead endpoint · API / service · Astro API endpoint — One server endpoint every opt-in flows through — home, program pages, contact, QR, ads and chat — which upserts the contact, writes lead-source and page fields, applies tags and opens a lead-acquisition opportunity, deduplicated by a daily hash of email, trainee and source. → Four anti-abuse layers, Contact upsert, Funnel & campaign pages (redirect with prefill)
    • Four anti-abuse layers · Guard · Astro API endpoints — Schema validation, a honeypot field, a minimum dwell time and a per-IP rate bucket, in that order — with honeypot and dwell failures returning a successful-looking response carrying a discard marker. → Single lead endpoint (silent discard on honeypot / dwell)
  3. Stage 03

    Program qualification

    Turning "who is this for and how old are they" into one of five programs.

    • Program survey · Decision · Astro component — Two questions — booking for yourself or someone else, then an age tier — resolving to one of five programs, each mapping to two different calendars depending on whether this is a first trial or a re-enrolment. → Booking state machine
    • Booking state machine · Capture · Astro component — A six-state client-side flow — survey, date picker, slot picker, trainee form, success, error — all pre-rendered into the DOM with the controller toggling visibility. → Availability endpoint, Booking endpoint
  4. Stage 04

    Availability

    Producing slots that are genuinely bookable, against a calendar API that hides them.

    • Availability endpoint · API / service · Astro API endpoint — Takes a program, a flow and a date range capped at five weeks, resolves the right calendar, fetches calendar config, existing events and free slots in parallel, and serves the result behind a short CDN cache with a longer stale-while-revalidate window. → Slot template generator
    • Slot template generator · Calendar · Pure TypeScript — Generates candidate slots from one typed weekly schedule minus a blackout list, enforcing a minimum lead time before the next bookable class — the same schedule data that renders the public schedule page. → Class-capacity reconciler
    • Class-capacity reconciler · Guard · TypeScript — Class calendars accept many bookings per slot, but the platform’s free-slot API stops returning a slot after the first booking — so an empty template slot still needs free-slot confirmation, while a partly-booked slot stays open as long as confirmed events are under capacity. → Booking state machine (genuinely bookable slots), Booking endpoint (slot taken → 3 alternates)
  5. Stage 05

    Booking

    Creating the appointment, and deciding what may and may not fail after it.

    • Booking endpoint · API / service · Astro API endpoint — Validates the payload, runs the same four anti-abuse layers, re-validates capacity at submit time, and on a lost slot returns a typed error together with the next three available slots over a two-week window; logs are redacted to a hashed email plus presence flags. → Four anti-abuse layers, Class-capacity reconciler (re-validate at submit), Create appointment
    • Create appointment · Calendar · CRM calendar API — Computes an end time by program, writes the appointment with a structured title, and passes both a capacity override and an existing event’s assigned staff member because the platform rejects the booking otherwise; then writes a structured appointment note with a correctly formatted local date. → Trial vs re-enrolment, Pre-trial reminders (native appointment trigger)
    • Trial vs re-enrolment · Decision · Orchestration module — A dispatcher splits first-trial bookings from re-enrolment bookings into two entirely separate handlers touching different pipelines, with cross-contamination ruled out by design. → Trial conversion pipeline (first trial), Re-enrolment pipeline (re-enrolment)
    • Trial booked · Outcome · Site + CRM — A confirmed appointment on the right program calendar, a per-trainee opportunity with its data, the acquisition record consolidated and won, nurture exited, and reminders scheduled. → Trial-credit pipeline (first attendance)
  6. Stage 06

    CRM writes

    Contacts, trainee identity, and the three pipelines the site drives.

    • Contact upsert · CRM record · CRM contacts API — Upserts by email and phone, sets the native source attribute to the marketing channel — then re-writes it on a second call, because the upsert endpoint only sets that attribute on creation — and writes lead source, opt-in page, last page and trainee-key fields with funnel and source tags. → Trainee identity key
    • Trainee identity key · Guard · Pure TypeScript — A deterministic, diacritic-stripped key derived from first name plus either a self marker or a date of birth, with an explicit rule never to compare on first and last name directly. → Trial conversion pipeline, Trial-credit pipeline, Trainee card resolver
    • Trial conversion pipeline · CRM record · GoHighLevel pipeline — One opportunity per trainee carrying trainee name, date of birth, program, last appointment id and start time, a local-format appointment date and an appointment history; an existing opportunity for that trainee key is moved rather than duplicated. → Nurture exit, Trial booked
    • Re-enrolment pipeline · CRM record · GoHighLevel pipeline — The win-back path touches only its own pipeline — an existing trainee moves to a booked stage, while a new trainee causes the imported former-student household record to be deleted and replaced with one opportunity per person. → Nurture exit
    • Trial-credit pipeline · CRM record · GoHighLevel pipeline — On first attendance a credit-monitoring opportunity is created or refreshed, the trial-pass credit count is initialised from a staff-editable default, and a long-lived rebooking token is minted onto the record; each subsequent attendance decrements the count behind an idempotency guard. → Rebooking entry (magic link minted at activation)
    • Staff-editable settings · CRM record · CRM custom values — Seventeen declared settings — trial-pass size, seven pipeline-timer durations, the webhook base URL and secret, the campaign deadline, page URL and offer name, the brand name and two revenue values — fetched behind a short server cache that swallows refresh errors and falls back to the last known value. → Funnel & campaign pages (deadline read at request time), Trial-credit pipeline (trial-pass size)
  7. Stage 07

    Reminders & replies

    What happens between booking and attending — and when a human must take over.

    • Pre-trial reminders · Message · GoHighLevel workflow — A reminder ladder running from days before the class down to minutes before it, fired by the platform’s native appointment trigger rather than by the site. → Inbound-reply handover (customer replies), Customer cancellation (cancel link)
    • Nurture exit · Guard · Orchestration module — Booking removes the contact from the relevant nurture campaigns — a dedicated exit call per flow, so a trial booking clears trial nurture and a re-enrolment booking clears the campaign’s own sequences. → Trial booked (chase sequences stopped)
    • Inbound-reply handover · Staff alert · CRM workflow + conversations — Any inbound reply removes the contact from six nurture drips — deliberately excluding the pre-trial reminders and the confirmation sequence, which are logistics for a class already booked — and applies a tag that is the staff work queue, cleared by hand once a human answers. → Booking diagnostic register (half of replies unanswered)
    • Customer cancellation · API / service · Astro API endpoint — A signed cancel token, verified in constant time and cross-checked against the appointment and contact it claims, sets the CRM appointment to cancelled and dispatches the cancellation handler, deduplicated for a week. → Status transition table
    • Status transition table · Decision · Declarative config — The one table-driven path in the system: appointment status maps to a stage move per pipeline, every move guarded by which stages it is allowed to move from — with the credit pipeline deliberately returning a cancelled booking to its active stage because the trial pass is still valid. → Trial conversion pipeline (showed / no-show), Trial-credit pipeline (cancelled → back to active)
  8. Stage 08

    Self-service rebooking

    The customer-owned path to book again, change program, add a family member, or cancel.

    • Rebooking entry · Source · Astro page + two endpoints — Two ways in — a long-lived signed magic link minted at trial activation and merged into reminder messages, or an email lookup that is rate-limited per IP per hour and returns a generic not-found on a miss. → Trainee card resolver
    • Trainee card resolver · Decision · Pure TypeScript — Merges open credit records with all-status trial records into one card per trainee, resolved by a four-level priority: enrolled, active with credits remaining, trial complete, or booked-but-not-yet-attended. → Short-lived action tokens
    • Short-lived action tokens · Guard · HMAC — Each actionable card carries its own fifteen-minute token, plus one contact-scoped token using a sentinel trainee key that authorises adding a new family member — so the long-lived magic link never appears in any subsequent request body. → Self-service actions (15-minute scoped token)
    • Self-service actions · Workflow · Astro API endpoints — Four customer-owned actions — book another class against a remaining credit, permanently change a trainee’s program, add a new family member’s first trial, and cancel — each verifying the token, cross-checking that the contact and trainee in the body match the token’s own payload, and resolving the parent’s details server-side so no personal data travels in the request. → Create appointment (book another / add a person), Trial-credit pipeline (credit consumed), Trial conversion pipeline (new family member)
  9. Stage 09

    CRM backflow

    Webhooks reconciling CRM-side and agent-side state back into the site’s model.

    • Webhook ingress guards · Guard · Astro API endpoints — Every inbound webhook shares one gate — a shared-secret header compared in constant time and never logged, defensive normalisation that tolerates a misspelled platform field name and rejects any value still containing an unresolved merge tag, schema validation, a one-minute idempotency window, and always an HTTP 200 so the platform does not retry forever. → Three backflow handlers, AI-agent booking backflow (concierge booked natively)
    • Three backflow handlers · Trigger · Astro webhook endpoints — Three handlers listen back to the CRM: trial-stage moves activate a pass, mark a lead lost or close out an enrolment with its value and date; credit-stage moves decrement on attendance, zero a lost trial or close a won one; and appointment-status changes mirror a status set in the calendar UI onto the owning opportunity, normalising the several spellings of "no show". → Trial-credit pipeline (attended → decrement), Trial conversion pipeline (won / lost), Status transition table (status set in the calendar UI)
    • AI-agent booking backflow · Trigger · Astro webhook endpoint — Bridges the gap left when the SMS booking concierge books through the platform’s native action, bypassing the site — it re-derives the program from the trainee’s age rather than trusting the agent’s calendar choice, re-fetches the appointment by id because the merge field carries a locale-formatted date, warns on win-back-tagged and off-segment contacts, then runs the same booking orchestration and campaign exits as a website booking. → Trial conversion pipeline (agent booking reconciled), Ghost-appointment risk (any failure branch)
    • Ghost-appointment risk · Guard · Documented defect — Every failure branch of the agent backflow — invalid age, invalid input, contact not found, appointment not found, or any orchestration throw — returns 200 while the appointment already exists on the calendar, producing an appointment with no pipeline record. → Booking diagnostic register
  10. Stage 10

    Replication & assurance

    The orchestrator, the operator tooling, the replication kit, and the audits.

    • Single CRM orchestrator · Workflow · TypeScript module — Declared as the only thing endpoints may call into for CRM operations — it exposes the lifecycle handlers (opt-in, booking, attendance, credit decrement, cancellation, status change) and nothing bypasses it. → Contact upsert, Trial conversion pipeline, Trial-credit pipeline, Nurture exit
    • Idempotency store · Data store · In-memory TTL map — A bounded, sweeping map with per-surface keys and windows — a day for lead submissions, a week for cancellations, a minute for webhooks — with an unimplemented durable backend declared in the environment schema. → Single lead endpoint, Customer cancellation, Webhook ingress guards
    • Three signing schemes · Guard · HMAC-SHA256 — Rebooking, cancellation and admin access all use the same stateless pattern — an HMAC over a pipe-joined payload, base64url-encoded, compared in constant time — with different payloads, different keys and very different lifetimes, and the admin token deliberately carrying no user data at all. → Rebooking entry, Customer cancellation, Blog + internal CMS (admin scope)
    • Declarative CRM schema · Data store · TypeScript config module — A single source-of-truth file declaring every pipeline, stage, contact and opportunity custom field, custom value, workflow, tag and environment variable the system expects — and a key-gated health endpoint that compares that declaration against the live account and reports missing variables, pipeline drift and a live settings read. → Five operator CLIs, Replication kit, Booking diagnostic register (drift check)
    • Five operator CLIs · Scheduled job · Node scripts — An onboarding CLI with checklist, discover and provision modes; a blog-onboarding CLI that is dry-run by default and always prints a manual fallback; an intake validator that rejects placeholder values; a read-only messaging-health audit; and a rescue tool for stranded records. → Replication kit, Booking diagnostic register
    • Replication kit · Report · 21-document corpus — Nine phases from preflight to handoff, three human sign-off gates, a 13-part client intake questionnaire, and seven hard rules — no silent invention, no production promotion, respect project memory, settings in the CRM not in environment variables, PII discipline, idempotent re-runs, halt on any failed phase.
    • Adversarial security audit · Report · Three parallel browser agents — Three parallel adversarial agents — one capturing every outbound request from live pages, one fetching and grepping the shipped bundle for credentials and source maps, one throwing fourteen adversarial payloads at the booking and personalisation surfaces — plus a static review, producing a ranked finding list with zero test data created. → Single lead endpoint (browser→CRM leak closed by a server door)
    • Booking diagnostic register · Report · Five-day audit — A seventeen-finding register graded critical to low, tracing a booking collapse to duplicate nurture enrolment, duplicate appointment reminders, timers three to four times too long, unanswered inbound replies, and — the meta-cause — nothing watching production at all. → Replication kit (do not clone the reference build yet)

Platforms · Astro · Vercel · GoHighLevel · Tailwind · Playwright

What I built

The acquisition surface

Twenty-six pages and seventeen endpoints, server-rendered. The organic side covers home, program pages, contact, reviews and legal, plus a primary local page and neighbouring-city pages each carrying local-business, breadcrumb and FAQ structured data.

Six channel- and audience-specific ad landers sit over shared components. Each stamps its own lead source, and all are noindex with a canonical pointing at the organic page and excluded from the sitemap, so paid traffic lands on matched copy without creating six duplicates competing with the organic pages.

One server door for every lead

Every opt-in across the whole site flows through a single server endpoint: home, program pages, contact, the in-studio QR page, the ad landers and the CRM chat widget. It upserts the contact, writes lead-source and page fields, applies tags and opens a lead-acquisition opportunity, deduplicated by a daily hash of email, trainee and source.

That endpoint replaced a browser-to-CRM webhook with no protection at all. Four anti-abuse layers run in order: schema validation, a honeypot field, a minimum dwell time and a per-IP rate bucket.

Availability, against an API that hides slots

Slots are generated from one typed weekly schedule minus a blackout list, with a minimum lead time before the next bookable class.

Then the reconciler earns its place. Class calendars accept many bookings per slot, but the platform's free-slot API stops returning a slot after the first booking. So an empty template slot still needs free-slot confirmation, while a partly-booked slot stays open as long as confirmed events are under capacity. Trusting the platform alone would hide every class that already has one person in it, which is most of them.

Trainee identity, not household identity

Two siblings and a parent share a household, an email and a phone number. Without a stable per-trainee key, every pipeline write lands on the wrong person. So identity is a deterministic, diacritic-stripped key derived from first name plus either a self marker or a date of birth, with an explicit rule never to compare on first and last name directly.

The site drives three pipelines on that key: trial conversion, re-enrolment and trial credit. The trial conversion pipeline holds one opportunity per trainee, moved rather than duplicated when they book again.

Settings the staff own

Seventeen settings live as CRM custom values: trial-pass size, seven pipeline-timer durations, the webhook base URL and secret, the campaign deadline, page URL and offer name, the brand name and two revenue values. This was chosen explicitly over environment variables, and the replication kit states it as a hard rule: environment variables are for secrets and resource ids only.

Self-service rebooking

A customer gets in two ways, a long-lived signed magic link merged into reminder messages, or an email lookup rate-limited per IP per hour that returns a generic not-found on a miss. The generic miss is deliberate, because an unthrottled lookup that distinguishes no-such-customer from found is an email-enumeration oracle.

Each actionable card then carries its own fifteen-minute token, so the long-lived link never appears in a request body. Four actions run on those tokens: book another class against a remaining credit, permanently change a trainee's program, add a new family member's first trial, and cancel.

The replication kit

The build was then generalised into a 21-document kit: nine phases from preflight to handoff, three human sign-off gates, a 13-part client intake questionnaire, and seven hard rules. No silent invention, no production promotion, respect project memory, settings in the CRM rather than environment variables, PII discipline, idempotent re-runs, and halt on any failed phase. The gates exist because three of the nine phases genuinely cannot be automated.

Five operator CLIs ship with it: an onboarding CLI with checklist, discover and provision modes; a blog-onboarding CLI that is dry-run by default and always prints a manual fallback; an intake validator that rejects placeholder values; a read-only messaging-health audit; and a rescue tool for stranded records.

Edge cases and failure handling

  • A slot taken between the picker rendering and the submit landing returns a typed error together with the next three available slots over a two-week window, turning a dead end into a second choice.
  • First-trial and re-enrolment bookings dispatch into two entirely separate handlers touching different pipelines, because sharing a code path is how a win-back booking ends up in the new-lead funnel.
  • The status transition table guards every stage move by which stages it is allowed to move from, so an automated move cannot clobber a staff member's manual reclassification. A cancelled booking deliberately returns the credit pipeline to its active stage, because the trial pass is still valid.
  • The conversion handler re-reads trainee fields off the opportunity rather than trusting the payload, because the contact-level last-trainee field collides across siblings.
  • Every failure branch of the AI-agent booking backflow returns 200 while the appointment already exists on the calendar, producing an appointment with no pipeline record. That is named in the architecture document as its own defect class rather than left implicit.

How I verified it

Before handover I ran an adversarial security audit split three ways: one agent capturing every outbound request from live pages, one fetching and grepping the shipped bundle for credentials and source maps, one throwing fourteen adversarial payloads at the booking and personalisation surfaces, plus a static review. Splitting the attack surface three ways is what surfaced the class the code review had missed, the two forms posting straight from the browser to a public CRM webhook. That finding is what produced the single server lead endpoint the whole capture stage now runs through.

When bookings later faltered, I ran a five-day diagnostic that produced a seventeen-finding register graded critical to low. It traced the collapse to duplicate nurture enrolment, duplicate appointment reminders, timers three to four times too long, unanswered inbound replies, and the meta-cause underneath all four: nothing was watching production at all.

The register is followed by a published corrections log that declares itself the higher authority over the audit it follows.

What changed

The site is live on the studio's own domain across twenty-six pages and seventeen endpoints, handed over with a system report and a non-technical owner's handbook. Every CRM call runs through one orchestration module, so rate limiting, idempotency, webhook secrets and signed tokens are dedicated helpers rather than scattered calls.

Staff change deadlines, prices and copy in the CRM and the site follows with no redeploy. Customers rebook, change program, add a family member or cancel themselves, from a link, without phoning the desk.

A new studio site is now a single completed intake doc plus a nine-phase build, with the tooling the operator inherits rather than a developer keeps. The kit is honest about its own reference build too: it carries a standing warning that the reference is not yet safe to clone, naming exactly what a fork would inherit, including wrong timers, unresolvable settings keys, a wrong revenue figure, workflow ids pointing at drafts, duplicate tags, and two forms belonging to other businesses.

Every finding of the booking diagnostic is traced to a root cause on the record, and the corrections that overturned parts of that diagnostic are on the same record.

Before / after

  • Before
  • ✕ One studio needing a site; every next studio needing a rebuild
  • ✕ CRM calls scattered wherever they were needed
  • ✕ Copy and deadline changes queued behind a developer
  • ✕ Handover as a phone call, not a document
  • After
  • ✓ A fork-per-client template: intake doc in, live site out
  • ✓ One orchestrator module owning every CRM call
  • ✓ Staff edit settings in the CRM; the site follows, no redeploy
  • ✓ A system report + owner’s handbook, audited before handover

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