System architecture, end to end
Every part of GAVEL from the browser to PayPal and the disk, and how a bid, a hold, a voice bid and a webhook travel through them.
If you have never seen the code, read this page first. It answers one question: when someone bids, which programs run, where do they run, and where does the money part sit?
System architecture, end to end
Scroll the figure sideways, or open it full size.
Pick a path to light up the boxes and arrows it uses.
Hover, focus or tap a box for what it does and which file implements it.
packages/server/src/boot/compose.ts, host/node-ws.ts, host/audio-ws.ts, voice/stt.ts, render.yaml, docs/DEPLOY.md and the workflows named in the boxes. The whole chain has been run against the fake PayPal; the PayPal sandbox gates in docs/GATES.md cover single calls, not a complete auction. The web hosting box follows docs/DEPLOY.md; the repository has no workflow that deploys the web app.Text version of this diagram
Reading order, top to bottom.
- Browser: the web app talks to the host over HTTPS and the room socket, the microphone sends frames to the voice socket, and the PayPal popup talks to PayPal directly.
- Host: the HTTP API and the room socket both feed the room runtime. The voice socket feeds the speech chain, which hands a decision to the room. The room asks the engine to decide each bid and starts the settlement saga after the hammer.
- The hold service (called by the HTTP API) and the settlement saga make the money calls. The webhook route also asks PayPal to check each signature, and PayPal sends events back to it, which feed the saga.
- Disk: room log, engine ledgers, saga journals, hold records, webhook ids, the payment audit log and the kill switch file. With STORE=postgres the room log and journals move to Postgres.
- Outside: Deepgram, Groq and the PayPal sandbox.
- Build and operate: GitHub Actions, the Render deploy hook, the uptime probe and the docs site build.
The short version
- One browser app for bidders, the auctioneer and the big screen. It is static files.
- One server process (a single Node program) that owns everything stateful: the live rooms, the auction engine, the payment logic and the files on its disk.
- PayPal is called only by the server, and only by two parts of it: the hold service (to create and authorize a hold) and the settlement saga (to capture the winner and release the rest). The only direct contact between a browser and PayPal is the approval popup.
- Speech to text also runs on the server. The microphone sends audio frames and the server turns them into a bid decision.
Why one process and one instance
render.yaml pins numInstances: 1 and puts the data directory on a persistent disk. The room log, the ledger, the journals and the holds are files owned by one process, and a data directory lock stops a second host from opening them. The price is that GAVEL does not scale out; the benefit is that the order of writes, which the money logic depends on, is easy to reason about.
Four journeys through the boxes
Use the buttons under the diagram to light each one up.
A tap or typed bid
The web app sends a bid message on the room socket (/ws). The room runtime checks it with the engine (ascending price, the bidder's ceiling, the lot state). The event goes into the room log first; the engine ledger is written before the event is acknowledged. Everyone in the room then receives the update.
Registration and the hold
The bidder registers with a ceiling. The HTTP API creates a PayPal order with intent AUTHORIZE through the hold service. The bidder approves in the PayPal popup (which talks to PayPal directly). The host then authorizes the order, stores the hold and registers the bidder. See Registration and holds.
A voice bid
Audio frames go to the /audio socket. The speech chain turns them into text (Deepgram, then Groq, then cached replay), a parser reads an amount and a decision step decides whether to ask for confirmation. A decision reaches the room through Room.submitVoiceBid. See Voice and the confirm gate.
Settlement and the webhook
After the hammer the room starts the settlement saga. The hammer is already in the engine ledger; the saga saves its journal, then captures the winning price and voids every other hold. PayPal later posts events to /webhooks/paypal; the host verifies the signature with PayPal, stores the event id once and hands the event to the settlement service. See Settlement saga and Webhooks.
Where each thing runs
The service gavel-host (render.yaml): one instance, Node 22, a health check on /healthz, a 1 GB disk at /var/data, STORE=file, PAYMENTS=sandbox, PAYPAL_ENV=sandbox. Auto deploy is off; deploys come from the deploy workflows through the Render deploy hook.
What is on the disk
With STORE=postgres the room log, snapshots, token denylist and journals live in Postgres; the ledgers, holds, webhook ids and the kill switch stay in the data directory. The data and ledger model explains each record.
Entry points the server exposes
Prop
Type
Where this lives in the code
- packages/server/src/boot/compose.tswires stores, PayPal, rooms, HTTP routes and both sockets
- packages/server/src/host/node-ws.tsthe /ws path and the upgrade handling
- packages/server/src/host/audio-ws.tsthe /audio socket
- packages/server/src/voice/stt.tschooseStt: Deepgram, Groq, cached replay
- packages/server/src/http/holds.tshold routes and the gateway they are allowed to use
- packages/server/src/http/webhook-guard.tswebhook size cap and logging
- render.yamlthe Render service and its disk
- docs/DEPLOY.mddeploy steps and the settings the checks need
- docs/submission/ARCHITECTURE.mdthe package diagram and the settlement sequence
Last updated on