The tech stack, with receipts

Series: The Build Track · Post 2 of 7

The exact stack behind a marketing AI that runs 24/7: Python, SQLite, Ollama, PM2, Cloudflare. What each piece costs, and the alternative we rejected.

Every "how I built my AI" post eventually shows you a stack diagram with twelve logos on it. Mine has about six, and half of them are free.

That's not humility, it's the design. Chapter 1 made the argument: rent the brain, own the body. This chapter is the body's parts list. What we actually run, what each piece costs, and the alternative we rejected at every layer. The pattern repeats all the way down: boring infrastructure, interesting behavior. Every place I could choose between a clever tool and a dull one, I took dull, and I'll tell you what dull bought me.

The stack on one page

  • Compute: one Windows box, running 24/7. Python 3.13.
  • Process manager: PM2 running 6 daemons. Health monitor, context watchdog, the Telegram bot, the master scheduler, a read-only dashboard, and a Cloudflare tunnel.
  • Data: SQLite. 21 separate databases, one per domain, plus a ChromaDB vector store.
  • Local model: Ollama, serving qwen models for bulk work. Drafts, classification, research summaries.
  • Brain: Claude, through a consumer subscription. Never the API.
  • Web face: a FastAPI service on port 8801 behind a Cloudflare tunnel, and a Next.js 14 portal on Cloudflare Pages with Supabase handling auth.

That's the whole thing. No Kubernetes, no message queue, no microservices. Now the receipts, layer by layer.

A Windows box and PM2, which nobody recommends

The respectable answer is a Linux server with everything in Docker. I run Windows because the machine I own runs Windows, and the machine I own costs zero dollars a month.

The fair framing: Docker buys you reproducibility, and reproducibility matters when more than one person deploys the system, or when you deploy it more than once. I am one person deploying it once. What I actually need is "these six processes stay up and restart when they crash." PM2 is a process manager from the Node world that happily runs Python, and it does exactly that. The scheduler daemon, the health monitor, the Telegram bridge, the dashboard, the tunnel. PM2 keeps them alive and gives me one command to see all of them.

This choice cost me, and I'll itemize. Windows has sharp edges that Linux tutorials never mention. Subprocesses need special flags so they don't spawn visible windows. File paths fight you. The worst one took a while to even diagnose: when I started reading these databases from a Linux sandbox over a Windows file mount, the mount served stale, truncated views of large files that were actively growing. My 120 MB CRM database showed up corrupted on the other side. It wasn't corrupted. The mount was lying.

The fix was a tool that runs natively on the Windows box and uses SQLite's online backup API to write consistent point-in-time snapshots, plus a manifest with row counts so anything downstream can check freshness without opening the big file. That job now runs every morning at 08:15. Nobody's stack diagram includes "the job that exists because file mounts lie." Mine does.

If you're starting fresh and you're comfortable in Linux, use Linux. The architecture doesn't care. The principle is just: use the machine you already own before you rent one.

SQLite, which everybody outgrows except they don't

The CRM database holds 156,925 contacts in a single 120 MB SQLite file. Conventional wisdom says that should be Postgres by now. Conventional wisdom is off by about two orders of magnitude.

SQLite's real limit isn't size. It's concurrent writers. A web app with a thousand users hammering the same table needs Postgres. A company AI is the opposite shape: a handful of scheduled jobs, each writing to its own domain, mostly at different times of day. So we run 21 separate databases. One for the CRM, one for outreach history, one for social content, one for engagement telemetry, one for the scheduler's own run ledger, and so on. One database per domain means one writer per domain, which means SQLite's weakness never comes up. Each connection runs in WAL mode with a busy timeout, and that is the entire concurrency story.

Every database is a file. Backup is copying a file. Inspection is opening a file. The snapshot job backs up the important ones daily, and a monthly restore drill actually extracts a snapshot and checks integrity, because a backup you've never restored is a hypothesis.

When SQLite is actually wrong: multiple processes writing the same tables at the same moment, or you need someone else's machine to query your data over a network. The day I have employees doing concurrent writes, I move that one database to Postgres and nothing else changes. Files buy you that too. Migrations stay per-domain instead of big-bang.

Ollama and the local models, with the scar

Ollama serves local models on port 11434. The workhorses are qwen. qwen2.5-coder for bulk generation, and qwen3:8b for the outreach engine's research and drafting, which we switched to in May because it follows JSON instructions better. The marginal cost of a local model call is electricity. That's why the daily social cycle can afford roughly 96 sequential model calls, and the overnight content scan can push 18 accounts through a content filter without anyone watching a meter.

Here's the scar. Chapter 1 mentioned it and chapter 6 has the full story, but the stack lesson belongs here: a local model at this size will fabricate with total confidence. Ours invented client case studies, and two of them auto-published before the system caught it. The fix was not a better prompt. Prompts are requests. The fix was a mechanical gate: every number and named claim in generated content must exist in the source material the model was given, checked by code, not by asking nicely.

So the stack rule I'd give you is this. Local models are a labor layer, never a judgment layer, and between the labor layer and the public internet there has to be a gate made of code. If your stack has Ollama and no gate, you have a fabrication engine with a scheduler attached.

The alternative, doing bulk work through a paid API, is genuinely simpler and the models are better. But bulk work is where the volume is, and volume on a taxi meter is how hobby projects grow enterprise bills.

ChromaDB, locally, because vector stores rot quietly

Semantic memory lives in a local ChromaDB store with two collections, general knowledge and episodes, using embeddings generated by Ollama. Nothing leaves the box, even for indexing.

The hosted alternatives (Pinecone and friends) are good products solving a problem I don't have, which is serving vector search at scale to many users. My vector store has exactly one customer: the machine itself. What I do have, and the hosted pitch never mentions, is rot. A vector store that only grows becomes a memory where everything is dimly relevant to everything. Ours runs a nightly aging pass. Entries get scored by importance, archived entries get deleted after a 180-day TTL, and both collections have hard size caps. Chapter 4 goes deep on this. The stack-level point: wherever your vectors live, the compaction job is not optional, and you'll write it yourself either way.

The public face: tunnel, portal, Supabase

The website parts are the most conventional layer. A FastAPI service sits on port 8801 and a Cloudflare tunnel exposes it. Engine status is public; dashboard and CRM routes sit behind bearer-token auth. The client portal is Next.js 14 on Cloudflare Pages, with Supabase doing auth (Google, LinkedIn, email) and its database. A portal signup fires a webhook into the CRM, which is the moment a website visitor becomes a row the rest of the machine can act on.

The tunnel deserves a sentence of advocacy. It means the Windows box exposes no ports and I never touched a router config. The box makes an outbound connection to Cloudflare and traffic flows back through it. For a machine sitting in a house, I think this is flatly the right answer.

The bill, itemized

  • Claude subscription: the one fixed cost that matters. Same consumer subscription anyone buys; current pricing is on Anthropic's site. This is the brain.
  • Anthropic API: $0, by design. There's an API key in the environment file and a standing rule that nothing uses it.
  • X API: about 35 cents a month. The other platform APIs we use are on free tiers.
  • Hosting: roughly $0. Cloudflare Pages and the tunnel are free tier. Supabase is free tier at this size. The server is a PC I already owned, so the real cost is electricity.
  • Ollama and every model on it: free software, electricity.

There are paid services wired in for specific jobs, an email-sending platform and a contact-enrichment API, that scale with usage and sit near zero when held. But the load-bearing monthly number is the Claude subscription plus pocket change. That's not an accident. It's what "rent the brain, own the body" looks like on a bank statement.

What this list is actually telling you

Notice what's absent. No agent framework, no orchestration platform, no managed anything in the hot path. Every component is either a file, a process, or a model server, and every one of them can be inspected with tools that existed fifteen years ago. Except the models.

That's the trade I keep making, and the one I'd push you toward: spend your novelty budget on behavior, not infrastructure. The interesting parts of this system live in the memory, the training loop, and the guardrails. All of it is interesting Python sitting on aggressively boring foundations. When something breaks at 2am the foundation is never the suspect, which keeps the search space small, which is why I sleep.

If you're an engineer, you could assemble this layer list in a weekend. The chapters ahead are why assembly was the easy part: the scheduler that keeps it honest, the memory that makes it useful, the guardrails we earned. If you're an owner and the words "WAL mode" made your eyes slide, that's useful data too.

Previous
Previous

The scheduler is the heartbeat

Next
Next

The architecture decision nobody tells you about: the brain and the body