← Learning

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.

System architectureA browser talks over HTTPS and SSE to one EC2 instance running Caddy, a web container and an api container; the api runs the crew, which calls the Anthropic API and reads memory from Neon Postgres.BrowserNext.js UIHTTPS · SSE ↓ONE t4g.small · SPOTCaddy — auto-TLS · reverse proxywebNext.js · SSRapiFastAPI · asynccreworchestrator + 4 agentsin-process workBackgroundTasks · APSchedulerAnthropic APIthe modelNeon Postgresorg memorymodel calls

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.