System architecture
The whole stack, one box
Monorepo in, one spot instance out. The constraint that shaped everything: a single operator has to be able to hold the whole system in their head.
How it works
Two apps, one instance, one request path
Two apps live in one repo — apps/web (Next.js) and backend (FastAPI) — and ship as two containers onto a single t4g.small. Caddy terminates TLS and reverse-proxies both. The only thing off the box is the database (Neon Postgres).
A crew chat’s lifecycle: the browser POSTs and holds the connection open; Caddy routes to the api; FastAPI hands off to the CrewOrchestrator, which runs the pipeline; the agents call the Anthropic API and read org memory from Postgres; the answer streams back as SSE events over the same connection, token by token.
Background work runs in-process — a BackgroundTasks queue for edit-learning and embeddings, an APScheduler job that sweeps stale approvals. No broker, no workers. (The documented prod upgrade is SQS → Lambda — same interface, different backend.) Two health checks gate everything: /health is liveness; /health/ready is the deep readiness probe the deploy smoke-tests against.
One operator’s whole stack on a single box. Caddy terminates TLS and proxies to the web and api containers; the api drives the crew, which calls the model and reads memory from Postgres; tokens stream back over the same connection. Only the database lives off-box.
Dev and prod stay the same shape. The same async code runs on SQLite (a local file) and Postgres (Neon); the only divergence — pgvector for semantic search — degrades to keyword matching so the same code path runs either way. One codebase, two environments, no “works on my machine” surprises.
Decisions & trade-offs
What each choice cost
One box, not microservices
one head can hold it · no orchestration tax
Two containers and a proxy on one instance, not a fleet of services. At one operator’s scale, microservices buy coordination overhead I can’t staff. The day this needs to scale out, the seams (web / api / db are already split) are where I’d cut.
In-process background work
$0 · upgrade path documented
Edit-learning and embeddings run as FastAPI BackgroundTasks; a scheduler ticks in-process. No queue, no broker bill. The code calls a bg_queue interface, so swapping to SQS → Lambda later is a backend change, not a rewrite.
SQLite in dev, Postgres in prod
fast local loop · one codebase
Async SQLAlchemy speaks both. I develop against a file and ship against Neon; the one real gap (pgvector) degrades to keyword search so the same path runs either way. Parity beats fidelity for a solo loop.
Deep readiness, not just liveness
bad config can't go live
/health says the process is up; /health/ready proves it can reach the DB and has real secrets. The deploy gates on the second — a container that booted misconfigured fails readiness and never gets traffic.
The learning
What’s running and what CDK last drew are two different things — a tag in SSM, not the template, decides the version.
What I'd tell you
Three things I'd say out loud
Build the system you can hold in your head. The point of one box is that one person can reason about the entire request path — no service mesh to trace, no distributed transaction to debug. That’s not a scaling strategy, it’s a comprehension strategy, and for a solo operator comprehension is the bottleneck, not throughput.
Parity over fidelity in dev. SQLite isn’t Postgres, but the async interface is identical and the one real gap (pgvector) degrades gracefully. I optimized for a fast local loop that behaves like prod, and stayed honest about the single seam instead of pretending there are none.
Decouple the artifact from the infra definition. What’s deployed is a pointer the box reads at boot, separate from what CDK last synthesized — so I change the running version by moving a tag, not by re-reasoning about the infrastructure. It’s the old artifact-vs-config split, applied so one person can ship without touching the scary part.
None of this is novel — it’s a boring monolith on a cheap box. The discipline was refusing the parts that would have made it impressive and unmaintainable-by-one-person. On this budget, the architecture’s job is to stay small enough to fit in a single head.
A personal experiment — cost-capped and opinionated. The views here are my own, not my employer’s, and this is a learning writeup, not an enterprise blueprint.