Skip to content

Containers + data flow

The picture

Architecture overview — operator interacts with the bot
container; three containers (bot, webhooks, booker) share three
persistent stores (Turso, R2, FalkorDB); each container reaches
its own subset of external services (Enable Banking, LlamaParse,
OpenAI/OpenRouter, e-Financials).

The diagram source lives at src/assets/diagrams/architecture-overview.d2; rebuild via d2 --layout=elk --theme=0 src/assets/diagrams/architecture-overview.d2 src/content/docs/diagrams/architecture-overview.svg.

The chain in one paragraph

The operator forwards a receipt to Discord. The bot OCRs it via LlamaParse, stores the bytes in R2 and the structured fields in Turso, embeds the text via OpenAI, then runs the matcher to look for an existing unmatched bank transaction. Meanwhile the webhooks container polls Enable Banking every 6 hours and writes new bank transactions into Turso, running the matcher in the other direction. When a match crosses HITL_THRESHOLD, the bot opens a checkpoint and posts a Discord prompt. On approve, the row goes to booked status. The booker daemon ticks every 15 min, finds booked rows without an efin_entry_id, calls vat_decision (which queries Turso's tax_rules + the FalkorDB knowledge graph and reasons via an LLM), and writes the entry to e-Financials. Done.

What's where

Container What it does What it owns
prod-accounting-bot Discord bot, HITL prompts, chat agent (/ask), all slash commands, ingest pipeline Discord token, the chat agent's lifecycle, the doc_match + booker_approve + client_create_approval prompt loops
prod-accounting-webhooks FastAPI for Stripe webhooks, OAuth callbacks; runs the Enable Banking poll loop in-process EB session lifecycle, Stripe webhook signature verification
prod-accounting-booker Booker daemon — picks up approved recons, writes to e-Fin The e-Fin client connection, idempotent re-book logic

All three containers run the same image with different CMDs. That keeps deploys symmetric: one image push → all three update.

Why three containers

  • Bot needs the Discord WebSocket open 24/7. It can't be a cron because Discord's gateway is persistent.
  • Webhooks needs an HTTP port open for Stripe + OAuth callbacks. It also runs the EB poll loop (cheap enough to share the process).
  • Booker runs writes that take ~1-2 seconds each and are rate-limited by e-Fin. Putting it in the bot would block the Discord event loop; putting it in webhooks would couple cron to HTTP traffic. A dedicated daemon is the right surface.

What runs where else

  • Production secrets: Infisical (path: /companybot/agent, env: prod). All three containers wrap their CMD with infisical run to inject secrets at startup.
  • CD: GitHub Actions builds + pushes to Google Artifact Registry; dockhand (also running on this VPS) watches the registry and redeploys when a new tag lands.
  • Observability: Logfire (OTLP → LGTM stack on the VPS).
  • Backups: Turso is hosted; R2 has versioning enabled; FalkorDB is rebuildable from the source PDFs in R2.