TRAINYOURAGENT

UD Growth Engine: a 239-table revenue operating system for a roofing company

We built the entire revenue system for United Developers, our own roofing operating company: 456 SQL migrations over 239 production Postgres tables, 54 Supabase edge functions, 40 scheduled database jobs and a 1,378-page marketing site. It proves we can carry a whole business on one schema and keep it honest.

The problem

United Developers is a roofing contractor. Roofing is a business where the money leaks in specific, boring, well-understood places: a lead comes in and nobody calls it inside five minutes; an inspection happens and the photos never make it into the supplement; an insurance adjuster asks for a document nobody can find; a marketing dollar gets spent against a channel nobody can attribute. Every one of those is a data problem wearing a sales problem's clothes. The industry's answer is to buy six SaaS products and let them disagree with each other. The CRM has one version of a lead, the dialler has another, the ad platform has a third, and the roof-measurement tool has a fourth. Reconciling them is somebody's full-time job, and that somebody is usually the owner at eleven at night. We took the opposite position: one Postgres database is the system of record, and everything else — the phone, the email, the ads, the field app, the marketing site — is a client of it. If a fact about a lead is not in that database it does not exist. That decision is what makes the rest of the numbers on this page possible, and it is also what makes them enforceable.

One schema, 456 migrations, nothing hand-applied

The database is defined entirely by files. There are 456 timestamped SQL migrations in supabase/migrations, totalling 52,497 lines, and they compose into 239 production tables. Nothing is applied by hand in a dashboard. That sounds like ordinary discipline until you look at what it costs to actually hold: with 456 migrations there is no such thing as remembering what the schema is, so the schema has to be able to prove things about itself. So it does. A guard called schema-drift-verify parses the declared schema and diffs it against production structurally. A second guard, column-drift-build-verify, goes further: it builds the full migration set on a real PostgreSQL instance and diffs one identical catalogue projection — type, NOT NULL, generation class — against production, hashed per table and drilled per column on mismatch. It is wired into the deploy workflow as a blocking step. Its first production run was green across 239 tables; on a manual run before that it caught two numeric-precision drifts, one of which a migration from that same week had introduced (a `numeric` column where production held `numeric(2,1)`). The practical end state is stated plainly in the repository's own ledger: a green deploy now structurally requires the schema in the files and the schema in production to agree on tables, columns, types, NOT NULL, generation, and every enumerating CHECK, with nothing grandfathered and no non-blocking steps.

54 edge functions as the only write path

Application logic lives in 54 Supabase edge functions, all prefixed `ud-`, each a Deno TypeScript module with a single job. They divide roughly into intake (`ud-lead-inbound`, `ud-email-inbound`, `ud-sms-inbound`, `ud-bulk-leads`), the voice and booking loop (`ud-riley-lookup`, `ud-riley-dial`, `ud-riley-book`, `ud-riley-outcome`), field and claims work (`ud-damage-ai`, `ud-adjuster-packet`, `ud-supplement`, `ud-inspection-deliver`, `ud-roof-quote`, `ud-proposal`), growth (`ud-leadgen`, `ud-canvass`, `ud-segment`, `ud-review-blast`, `ud-meta-capi`, `ud-ads-sync`), and operations (`ud-orchestrator`, `ud-metrics`, `ud-measure`, `ud-integration-probe`, `ud-staff-ops`). The rule that keeps this from becoming spaghetti is that exactly one component writes downstream. A durable orchestrator owns the lead → confirmation call → booking workflow, and nothing else is permitted to book, dial or email. That is not an aesthetic preference; it is the fix for double-dialling and double-booking, which are the two failure modes that make a homeowner hang up on you forever. The orchestrator is worth being precise about because it is a piece of deliberate scope reduction. The original design called for Temporal as the durable engine. Temporal needs its own multi-container cluster and a worker host, which this operation does not run. So migration 20260620000101 reverse-engineers the durability contract Temporal provides — workflow rows, idempotent run IDs with a reject-duplicate reuse policy, retries, and a tick-driven worker — directly in Postgres, and `ud-orchestrator` is the worker side of it. It is named after the thing it replaces, and the file says so in its first three lines.

40 scheduled jobs and 25 workflows doing the unattended work

Forty distinct pg_cron jobs run inside the database itself (45 `cron.schedule` calls across the migrations, some of which reschedule an existing job). They do the work that has to happen whether or not a human is awake: re-probing integration health every two hours, refreshing canvassing territory data, rolling up metrics, ageing lead states, sweeping outboxes, and firing the speed-to-lead escalation clock. Above the database, 25 GitHub Actions workflows handle everything that touches the outside world — Supabase deploys, page deploys, Brevo list synchronisation, storm-data ingestion, asset fetching and optimisation, live smoke tests, health checks, security scanning, and a daily article generator. The scheduled work is where a system like this either becomes an asset or becomes a liability, because unattended jobs fail quietly. The engine's answer is the integration probe: a job that re-tests every external credential on a two-hour cycle and writes the result to an `integration_status` table, so that 'is the ad platform connected' is a query rather than an opinion. As of the last recorded read, that table was reporting four integrations as NOT CONNECTED — see 'What we are not claiming' below. The probe being able to say so is the feature.

1,378 static pages, and a ledger that refuses unproven claims

The public marketing surface is 1,378 static HTML pages under `website/`, generated and deployed by workflow rather than authored by hand — service pages crossed with service areas, storm reports, resource articles, and a control centre. Static HTML was chosen over a framework for a specific reason: a roofing site's job is to be fast on a phone in a driveway with two bars of signal, and there is no rendering strategy that beats a file. The part of this repository we would point at first, though, is not code. It is `brain/STATE.md`, a verified-state ledger with a rule in its frontmatter: nothing enters this file as VERIFIED without a proof command and its output. It defines a five-word status vocabulary — VERIFIED, SHIPPED-UNVERIFIED, BUILT-NOT-LIVE, CLAIMED, BROKEN — and it defines CLAIMED as 'someone said it works, nobody proved it; treat as broken.' That file exists because on 5 August 2026 three systems previously reported as done were found completely broken: a password reset that never sent, an opt-out system that silently dropped every rep email, and six files a prior AI agent claimed to have deployed that existed in no branch. The ledger is the institutional response to being lied to by a tool. It is also, directly, the reason this portfolio page is written the way it is.

The numbers, and the command behind each one

What is genuinely hard about this

Not the feature count. Any team with enough time can produce 54 functions. The hard part is that a system with 239 tables and 40 unattended jobs has an enormous surface for silent divergence — between the schema you declared and the schema that exists, between the job you scheduled and the job that is running, between the integration you configured and the integration that is actually authenticated. Every one of those gaps produces the same symptom: a dashboard that looks fine while the business quietly loses money. Closing that surface is what most of the 157 guards are for, and it required accepting a genuinely unpleasant trade: the deploy pipeline is allowed to fail on things that are not obviously broken. A no-op `ALTER COLUMN TYPE` — one where the new type equals the old — halted a deploy because PostgreSQL checks view dependencies before it notices the change is a no-op. The right response was not to make the check non-blocking. It was to guard the ALTER on `atttypmod` and confirm against production that both branches skip. Non-blocking checks are how you get a green pipeline and a broken database. The second hard part is cultural and it is the reason the state ledger exists. When a large fraction of the code is written by AI agents, the dominant failure mode stops being bugs and becomes confident false reports of completion. The mitigation is not better prompting. It is a rule that a claim without a proof command and its literal output is treated as broken, and a deploy gate that does not care what anybody claimed.

What this entry does not claim

Stack and status

Built with Supabase (Postgres + Edge Functions), Deno / TypeScript, pg_cron, Row Level Security, GitHub Actions, Stripe, Brevo, Meta Conversions API, GA4, AccuLynx, Twilio, Anthropic Claude and Static HTML generation. Relationship: Our own company. Stage: In production. Period: 13 June — 23 August 2026. Our role: Architecture, schema, edge functions, CI enforcement, site generation Figures re-derived on 2026-08-23.

Where a supplied figure was wrong

Corrected against the working tree on 2026-08-23: commits 2,220 → 2,245; migrations 455 → 456; edge functions 56 → 54 (55 directories, one of which is the shared library); pg_cron jobs 35 → 40 distinct; CI guards 153 → 157; lines ~478,000 → 487,318. Tables (239), workflows (25), static pages (1,378) and scripts (89) all held.