Answer
Brew is one Node 22 codebase that runs as two production services (web and worker) on Cloud Run over Cloud SQL Postgres, plus a single-instance staging and a bucket-only demo, and this page settles where each piece of work runs and why only one worker may do paid background work.
Today
The shape
Cloudflare DNS (feed, brew, demo -> 34.54.65.239)
|
Google HTTPS load balancer "squareup-urlmap"
(Cloud Armor brew-prod-armor on the production backend only)
| | |
feed.joinsquareup.com brew.joinsquareup.com demo.joinsquareup.com
PRODUCTION STAGING DEMO (DEMO_MODE=1)
| | |
Cloud Run brew-web Cloud Run shopos-feed Cloud Run brew-demo
ROLE=web, 2..10 inst, ROLE=web,worker, exactly 1 exactly 1 instance,
session affinity, instance, CPU always on bucket storage only
ingress: LB only | |
| jobs (Postgres) | |
v | |
Cloud Run brew-worker | |
ROLE=worker, exactly 1, | |
internal, holds the | |
background lease | |
| | |
Cloud SQL brew-prod Cloud SQL brew-staging gs://brew-prototype-demo
(Postgres 17, HA, (172.30.0.3)
172.30.0.4) |
| |
gs://brew-prod gs://brew-prototype
(pictures, video, PDFs, (binaries, plus a mirror of
zips; versioned) every row while it is the way back)
Outside: Vertex AI (Gemini text, Gemini image, Veo, TTS, embeddings), Anthropic (switch back),
OpenAI (image fallback, transcription), treg (SERP, Ad Library, AI answers, social, marketplaces),
Apify (quick commerce, Indian marketplaces), Higgsfield and Sloosh (Studio and finals), Resend (team email),
Shopify, Meta, Google Ads, GA4, Klaviyo, Judge.me, Slack (read-only connectors), headless Chromium (in the image)
Sources for the diagram: brew/.wt-prod/docs/production.md brew/tools/promote.sh:70-107 brew/deploy.sh:39 brew/tools/deploy-demo.sh memory brew-prod-backend
Services
| Piece | What it is | Source |
|---|---|---|
| brew-web | Production web: ROLE=web, 2 to 10 instances, 2 vCPU, 2 GiB, concurrency 80, session affinity, ingress internal and load balancer only, pool of 5 Postgres connections | brew/tools/promote.sh:79-80 |
| brew-worker | Production worker: ROLE=worker, exactly 1 instance, 2 vCPU, 4 GiB, ingress internal, pool of 10 | brew/tools/promote.sh:94-95 |
| shopos-feed | Staging: both roles in one instance, min 1 max 1, 2 vCPU, 4 GiB, timeout 3600 s, concurrency 200, STORAGE=pg | brew/deploy.sh:39-40 |
| brew-demo | Demo: DEMO_MODE=1, one instance, all Gemini Flash, bucket storage | brew/tools/deploy-demo.sh |
| Runtime identity | Production runs as brew-prod-runtime; staging, the demo and ops jobs run as brew-runtime and cannot read production secrets or data | brew/tools/prod-env.sh:5-6 |
| Image | node:22-bookworm-slim with ffmpeg, chromium and fonts; five npm dependencies: @anthropic-ai/sdk, @imgly/background-removal-node, date-holidays, pg, puppeteer-core | brew/Dockerfile brew/package.json |
Roles, the lease and the queue
ROLEisweb,workeror both (the default, used locally and on staging). brew/roles.js:1-9- On Postgres one worker holds the background lease: a session advisory lock (key 727275) on a connection of its own. If that connection drops, the worker exits and Cloud Run restarts it. brew/roles.js:18-32
- Only the lease holder runs timers, resumes builds and pays for watchers. Every timer is wrapped in
leaderOnly. brew/roles.js:39 brew/server.js:520 - A web-only instance never runs builder work itself. A JS Proxy turns every builder method, other than local reads such as
status,listandevents, into a job the worker runs. Clicks wait up to 90 s (PDFs 180 s); nudges such asseen,healandbookAfterare fire and forget. brew/server.js:464-480 - The job queue is the Postgres
jobstable, claimed withFOR UPDATE SKIP LOCKED, leased for 120 s with a heartbeat, retried with backoff, at most one open job per key. Queues areinteractive,slowandbatch. Delivery is at least once, so every handler must be safe to run twice. brew/jobs.js:1-8 brew/db/migrations/002_jobs_counters_events.sql - Without Postgres (demo, local, tests) a job runs at once in-process. brew/jobs.js:7
How instances talk
| Need | Mechanism | Source |
|---|---|---|
| A stored object changed | objects trigger sends `pg_notify('objects', key |
version)`; other instances drop their cache |
| New cards, a connector read again | bus table plus NOTIFY, kept a day |
brew/bus.js:1-3 |
| Build progress on any instance | build_events table plus NOTIFY, kept 3 days |
brew/build-events.js:1-4 |
| Who is in a jam | jam_presence unlogged table, read every 2 s |
brew/presence.js:1-3 |
| Day caps across instances | counters table, written within 1 s, pulled every 2 s; take() for strict caps |
brew/counters.js:1-5 |
| Sign out everywhere | sessions and session_revocations plus NOTIFY |
brew/sessions.js:1-4 |
| Two people editing one jam | shared-doc.js: each instance writes only the parts it changed; the other side's new list items merge in |
brew/shared-doc.js:1-5 |
| One paid Studio make per board | make_claims rows taken in one transaction |
brew/jam2/make-claim.js:1-4 brew/db/migrations/009_make_claims.sql |
Request path
serve() tries handlers in this order: the health probe, a cross-site refusal, demo routes, the Shopify app, auth, the closed-store gate, the moved-brand redirect, Sloosh, jam mail links, Slack, connectors, the team gate, memory, cadence moves, brand routes, brand files, the public Brand Book, the mic, the feed service (which mounts the jam, Studio, briefs and the Brand Book API), then x-brand routes, /research/ and /gen/ media, static files and pages. brew/server.js:1110-1168
There is no route registry: each module matches its own paths with regexes. brew/docs/handover/engineering/04-api-routes.md
Restarts and deploys
- SIGTERM runs one shutdown path: no new move starts, jams and workspaces flush, and runs are left unfinished so the next instance resumes them. brew/server.js:1066-1081 brew/CLAUDE.md
- At boot the lease holder runs
builder.resumeAll()andfinals.reconcileAll(), so builds, rewrites and paid finals carry on and are never paid twice. brew/server.js:735-738 retire.jsandcontrol/live.jsonstill keep one live revision on bucket deployments; production setsRETIRE_OFF=1because the lease does that job there. brew/retire.js:1-10 brew/tools/prod-env.sh:9
AI providers
ai.jsgives one call shape for Claude or Gemini;gemini-client.jsmaps Claude names to Gemini models. brew/ai.js:1-3 brew/gemini-client.js:1-5- Today staging and production run
AI_PROVIDER=gemini: Plays ongemini-3.1-pro-preview, everything else ongemini-3.5-flash. brew/deploy.sh:39 brew/tools/prod-env.sh:9 brew/moves-engine.js:17 - Draft media: Vertex Gemini image, then OpenAI; video Veo 3.1 fast then Veo 3.1; voice Gemini TTS then OpenAI. brew/media.js:1-7
Tomorrow
| Item | Version | Tag |
|---|---|---|
| Web and worker split, one lease holder, Postgres queue | v1 | Stays |
| One worker for all brands. Per-brand build leases so several workers can build at once | v2 | New |
| A route table that declares each route's auth level, checked by one test | v2 | New |
Staging on the bucket mirror (OBJECTS_MIRROR=1, OBJECTS_READTHROUGH=1) as the way back |
v2 | Goes, once Postgres has run a month without a rollback |
| Demo on the bucket with one instance | v2 | Changes: move to Postgres so one storage path is tested |
retire.js and control/live.json |
v3 | Goes, once every site is on Postgres |
| The feed service as one 348 KB file mounting the jam and Studio | v3 | Changes: split by area (feed, jam, Studio, brief) |
How we do it
- Anything that spends money in the background must run under
leaderOnly(...)orwhenLeader(...). A web instance must never start paid work. brew/roles.js:39 - Anything a click needs from the builder goes through the builder proxy; add a method to
BUILD_LOCALonly if it is a pure read. brew/server.js:468 - A new job handler must be safe to run twice. Paid calls carry their own idempotency keys. brew/jobs.js:4-5
- A new piece of shared state across instances uses Postgres (a table, NOTIFY, or
counters), never process memory. - Check what the worker is doing on production:
gcloud run jobs execute brew-ops --region=asia-south1 --wait # runs db/status.mjs: Arata's connections, table sizes
Numbers
| Metric | Definition | Where measured |
|---|---|---|
| Lease holder | Which worker revision holds the background lease | db/prod-clean.mjs EXPECT_WORKER_REV; alert "worker lost its lease" brew/.wt-prod/docs/production.md |
| Job queue wait | Age of the oldest queued job; alert past 10 minutes | alerts.js brew/.wt-prod/docs/production.md |
| Web instances | Between 2 and 10 | Cloud Run brew-web brew/tools/promote.sh:80 |
| Postgres connections | Instances times pool size; alert above 160 | Cloud Monitoring brew/.wt-prod/docs/production.md |
Open
- When do we run more than one worker? Proposal: stay on one worker until a build queue of more than 3 brands waits over 30 minutes, then build per-brand leases.
- When does staging stop mirroring to the bucket? Proposal: turn
OBJECTS_MIRRORandOBJECTS_READTHROUGHoff on 25 Nov 2026 if no rollback to the bucket happened. - Should the demo move to Postgres? Proposal: yes, in v2, on its own small database, so the demo tests the same storage as production.