Skip to content
Operations

Deployment

The public, read-only copy: Vercel in front, Catalyst AppSail behind, a masked snapshot.

The real pipeline runs on your laptop. The deployed site is a read-only window onto a copy of what it found: the dashboard on Vercel, the API on Zoho Catalyst AppSail, and one snapshot of the database with every email address masked inside the file. Nobody there can send, spend or write anything.

The pieces#

PieceWhereNotes
Dashboard (and these docs)VercelEvery docs page is pre-rendered: it is served as finished HTML and never waits for the API.
API proxy/zapi/*Same-origin rewrite on the Vercel domain to the backend (next.config.ts) — no CORS to configure. Reads the backend marks cacheable are kept at the edge for 10 minutes, then served stale while it refreshes.
APICatalyst AppSail · Python 3.12 · 1024 MBThe Development environment is the live backend; ZONULY_PUBLIC=1 refuses every write and every private read.
Datademo.db.gzA stripped, masked snapshot, committed to the repository and unpacked into /tmp when the API starts.
Memory: 1024 MB
The AppSail instance was raised from 512 MB to 1024 MB on 23 Oct 2026: the app itself peaks around 250 MB, but the unpacked snapshot sits beside it in the same memory, and a deploy at 512 MB never came up.

The snapshot#

scripts/make_demo_db.py builds demo.db.gz from your working database. Because the file sits in a public repository, hiding addresses in the API was not enough — so the file itself is cleaned:

WhatHow
Every email addressReplaced, in every text column of every table (the knowledge graph’s search index included), by a stand-in on the same domain: first letter + seven characters of a hash. The site looks the same; no address in the file reaches a real inbox.
EmptiedCached pages, model spend, inbound replies, your Upskill notes and proof links, real interview outcomes, the mixer’s spend ledger, and the scrape audit trail (extraction_write / extraction_reject).
On screenThe public API also shows addresses masked, as p•••@company.com.
refresh the public numbers
python scripts/make_demo_db.py          # -> demo.db.gz
git add demo.db.gz && git commit -m "Refresh the snapshot" && git push

How a push becomes the live site#

  1. Smoke test the snapshot
    On every push to main, the snapshot workflow unpacks demo.db.gz and runs scripts/smoke.pyagainst it — a snapshot older than the code's schema fails here, not in production.
  2. Build the backend
    scripts/build_catalyst.py assembles deploy/catalyst/appsail/build/: the code, config, lists and snapshot, with every dependency fetched as a Linux wheel. Never shipped: .env, secrets/, jobhunter.db, profile/, backups — and not Playwright or the Google API client, which a read-only instance cannot reach.
  3. Deploy and check
    The workflow deploys to Catalyst Development with the CATALYST_TOKEN repository secret, waits for /api/health, and runs the smoke test against the live URL.
  4. The dashboard
    The workflow ships only the backend. The dashboard is deployed to Vercel separately; on Vercel its build sets the API base to the /zapi proxy.

A sleeping backend#

The Development instance sleeps about a minute after its last request. The first visitor after that waits while it wakes and unpacks the snapshot — measured at 7.5 s for /api/overviewon 23 Oct 2026, and up to half a minute right after a deploy. The dashboard retries a read for about 35 seconds before saying it can't reach the API; the docs never wait at all, and the front page's live figures fill in when they arrive.

Where public mode switches things off, see the Public mode note in the Dashboard tour and the conventions in the HTTP API.