08 — Stack and technologies
The golden rule of altitude applied to tool choice: the simplest thing that meets the requirement; complexity is added when a metric forces it (→ 12).
The doctrine is language-agnostic — but the choice of stack decides how cheap a mistake is and how easily you can test it. This chapter is a sensible default for a project run with Claude: universal, simple, flexible, and scalable. Not dogma — a starting point you depart from deliberately (and record why, as an ADR).
Default stack (a reasonable start)
| Layer | Default | When to change |
|---|---|---|
| Backend / scripts | Python — A popular, readable programming language — The Craft’s default for scripts, data and the back end. A proven default: lots of ready tools and libraries, easy to drive with AI. (+ FastAPI for the API — An agreed way for two programs to talk to each other — one asks, the other answers in a set format. Through an API your app connects to outside services (payments, maps, AI). Treat an API key like a password.) | Front and back share TypeScript → Node |
| Database | SQLite — The simplest database — the whole thing lives in one file, with no separate server. A great default to start: zero configuration, easy backup (you copy the file). As you grow, you migrate up. | Concurrent writes / roles / relations / scale → PostgreSQL — A solid, “grown-up” database for bigger apps — runs as a separate server. The default step after SQLite, when you need many concurrent users and advanced features. |
| Web / front | Server-rendered HTML + a bit of JS | Rich, interactive UI → Next.js |
| API | REST/JSON with an explicit contract (OpenAPI) | gRPC only at real need |
| Cache — A temporarily remembered result, so the same thing isn’t computed again on every request. Speeds the app up, but can be a trap: a stale cache shows old data. / queue | none → add Redis when the traffic profile forces it | not “just in case” |
| Packaging | Docker / container — Packaging an app with everything it needs so it runs the same on every computer. Kills “works on my machine” — the same environment locally and on the server. (reproducibility) | trivial script with no dependencies → no container |
| Hosting | one VPS — A rented piece of a cloud server “just for you”, where you put an app “live”. Predictable cost and full control. One VPS easily carries a few small projects. (Hetzner — A cheap, solid server (hosting) provider you put an app “live” on. The codex’s default server choice — predictable cost and performance without overpaying.) + nginx — A program at the front of the server that takes traffic from the internet and routes it to your app. Handles HTTPS, serves static files, balances traffic — the server’s proven “reception desk”. | uneven/global traffic → Serverless — A model where you don’t manage a server — the code runs itself when someone needs it, and sleeps when not. You pay per use, it scales itself. The trap: a “cold start” — the first call can be slower. (scale-to-zero) |
Overriding rule: boring, mature technology beats trendy. What counts is the ecosystem, the documentation, and testability — not the hype.
Proven toolkit (a concrete baseline)
A toolkit battle-tested on a real project — take what the task requires, skip the rest. Everything here is mature, well-documented, and easy to test and host on a single VPS:
| Layer | Tools |
|---|---|
| Primary tool / process | Anthropic Claude — Claude Desktop / Claude Code (the agent you build with; operate it well → 16) · Git (history, → 05) · GitHub (remote repo, PR) · GitHub Issues (tasks, → 07) |
| Format / content | HTML5 (server-rendered), Markdown (docs), JSON (API, config), XML when an integration forces it |
| Data / queries | SQL — The query language for a database — the way you “ask” the database for data or change it. The universal standard for talking to a database. A badly written query can bog down the whole app. — SQLite (WAL — Write-Ahead Log: a database mode where changes go to a journal first, then to the data. Gives safety (recovery after a crash) and better read/write concurrency.) to start → PostgreSQL at scale; forward-only Migration (database) — A controlled change to the database layout — adding a column, a table or moving data — step by step. Like a renovation to plan: rebuilding data in a set order so nothing “collapses”. |
| Backend / scripts | Python (data, scrapers, migrations on stdlib) · Node.js + Express (web/API) · EJS (server-side templates) |
| Frontend | minimal JS (vanilla), marked for MD rendering; SVG icons: Lucide only (one source of geometry, consistency); self-hosted fonts |
| Sessions / security | persistent session store (a separate file, → 14), helmet, rate-limit, bcrypt, OAuth (passport) |
| SMTP (587 STARTTLS), verified sender domain (→ 14) | |
| Media / image | conversion to WebP (Pillow); image comparison (OpenCV) only when truly needed |
| Ingest / Scraping — Automatically gathering data from websites with a program instead of copying by hand. Powerful for acquiring data, but it needs manners: respect others’ rules (robots.txt), don’t overload the server. | requests + BeautifulSoup (+lxml), fuzzy-match (rapidfuzz), Playwright when a browser is needed (→ 14) |
| LLM | API client (Claude / Anthropic SDK), responses streamed over SSE (→ 13) |
| Tests | pytest (Python) · Jest + supertest (Node) · Playwright (e2e) (→ 03) |
| Build / dev | esbuild, nodemon; Docker for reproducibility |
| Server / ops | one VPS (Hetzner) + nginx (TLS, reverse proxy) + pm2/systemd; GA4 analytics |
Icon rule: for SVG, stick to one set — Lucide (geometry in a <symbol>/sprite,
the rest is style). Mixing icon libraries = inconsistent UI and mismatched weights/strokes.
Python as the default language
- Why: readability (code like prose — easy to review by a human and an agent), batteries included, one language for API + scripts + data/LLM. Fewer contexts to hold in your head.
- FastAPI for the API: types + validation (Pydantic) + auto-OpenAPI = a free contract at the front↔back boundary (→ 12). pytest for tests (→ 03).
- Reproducible env: a virtual environment + pinned deps (lock). “Worked yesterday” goes away.
- Format and types as a contract:
ruff/black, type hints. Consistent style = cheaper review (→ 02).
Database — from simple to scale
- Start: SQLite. Zero-ops, one file, great for an MVP and an always-on VPS. Don’t start with a distributed database “because it’ll grow someday”.
- Scale: PostgreSQL — when concurrent writes, roles, complex relations, or extensions start to hurt. Anticipate the SQLite→PG migration in the model from the start (→ 11).
- Engine-independent rules: forward-only migrations + a backup before each (→ 04), Slug — A readable, short part of a page address that describes its content in words instead of a mysterious number. Better for humans and SEO; a stable slug doesn’t break links when things change. instead of ID in references to user data, Index (database) — A lookup in the database that makes searching instant instead of scanning everything in order. The first move for slow queries — like an index at the back of a book instead of reading 400 pages. after measurement, not on a hunch (→ 13).
- NoSQL / distributed — only when relational genuinely doesn’t suffice, not before.
Web and API — the contract as the boundary
- The less JS, the better. Server-rendered HTML by default; SPA/Next.js only when the interaction truly requires it. A light front = faster and cheaper to maintain (→ 13).
- The API contract (OpenAPI) is the boundary between front and back — it lets you swap one side without touching the other (→ 12). Version it; return structured errors.
- Streaming — Sending a large response in chunks, as it goes, instead of loading everything into memory at once. The app doesn’t choke on huge data — the user sees a result sooner. / SSE for long responses (chat, LLM token-by-token) — the user sees the effect right away, not emptiness (→ 13).
Docker — reproducibility, not a cult
- What for: the same image locally / in CI / in prod. “Works on my machine” ceases to exist.
- Hygiene: a small image (multi-stage, slim base, pinned versions),
.dockerignore, a non-root process.docker-composefor local assembly (app + database). - Don’t containerize by force a single script. Docker where dependencies hurt — not as a ritual.
Server and hosting — one box, good habits
The Monolith — An app built as one whole (one deploy, one database), not a patchwork of separate services. A simple, cheap default to start — easy to grasp. You split it only when scale really demands. is the default architecture — favor it as the easiest to maintain. One application (web + API + jobs) and one database on one VPS is something you can hold in your head: one deploy, one log, one backup, one Rollback — Reverting a change to the previous, working state — “Ctrl+Z” for a deployment. When a new version breaks production, a rollback restores the previous one in seconds instead of fixing in a panic.. Debugging means reading a single process, not correlating traces across a network. Splitting into services/serverless adds the network, contract versioning, and a whole class of new failures (→ 14) — reach for them only when a metric (traffic, team, failure isolation) genuinely requires it, not “because that’s how it’s done.”
- Default: one VPS (Hetzner) + nginx (reverse proxy, TLS) +
systemd/pm2/Docker for processes. Cheap, predictable, full control, zero cold start. Many projects on one box = a separate vhost + a separate directory, the same deploy rules (→ 05). - Scale-to-zero (Cloud Run / serverless) when traffic is uneven/global and the cold start is acceptable; always-on VPS when traffic is predictable. A deliberate cost/latency tradeoff, recorded as an ADR (→ 12).
- Managed services (database, mail, storage) when they take ops off your plate for less than maintaining it yourself would cost.
- Secrets in env / a secret store — never in the repo (→ 09).
TDD and change coverage — the hard core
The most important criterion for choosing a stack: it must be testable from the first line. A technology you can’t easily wrap in a test (test-first, a fast and deterministic suite, gating CI) is a bad choice — even if it’s trendy. The mechanics of TDD — Test-Driven Development: first you write a test (what you expect), then the code that satisfies it. The test stays in the project and guards that a change broke nothing — proof instead of “it probably works”. and change coverage = the canon in → 03; here, just the hard consequence: choose tech that makes it possible, and treat “a commit without a test” as incomplete (→ 00).
The technology-choice rule
- The simplest thing that meets today’s requirement, with an explicit growth path (SQLite→PG, VPS→serverless).
- Testability and ecosystem matter more than novelty.
- Every non-trivial choice = an ADR: one “why” + the rejected alternatives. The next session (human or agent) should know why it’s done this way.
Anti-patterns
- 🚫 Microservices / Kubernetes / a distributed database on an MVP — complexity no one needs yet (→ 12).
- 🚫 A stack for the CV/the trend instead of for the problem and testability.
- 🚫 Changing behavior without a test (“I’ll add it later” — you won’t).
- 🚫 Secrets in the repo; no version pins → “worked yesterday”.
- 🚫 SPA / heavy JS where server-side HTML would do.
- 🚫 Docker/Cloud as a cult instead of a tool fit to the problem.