Next.js App Router + SQLite. Small footprint, zero vendor lock-in.
Visitor lands → UTM params captured in middleware → stored per-session
│
▼
Next.js App Router (Server Components + API Routes)
│
├── / Home, coaching, TotalBalance, blog pages
├── /admin Custom CMS (auth-gated, cookie session)
└── /api Contact form → Resend → notification + auto-reply
│
▼
SQLite (via Drizzle ORM) — posts, pricing, content, submissions
│
▼
Fly.io — Docker container, persistent volume for DB
Stack
Next.js (App Router)
React 19
TypeScript
Tailwind CSS
SQLite + Drizzle ORM
Resend
Fly.io (Docker)
Key decisions
- SQLite over Postgres. A single-author coaching site with low write volume. No connection pooling overhead, no cloud DB cost, ships inside the same Docker container. Drizzle migrations keep the schema clean.
- Custom CMS instead of a headless SaaS. Edith needed exactly four editable surfaces: blog posts, pricing, page content, and her contact inbox. A bespoke panel with those four screens is faster to build and faster to use than explaining Sanity or Contentful to a non-technical client.
- UTM middleware at the edge. Every page load captures UTM params server-side before any React hydration. They land in the submissions table alongside every contact form so attribution is never lost.
- Resend over SMTP. The hosting provider’s SMTP was unreliable. Resend gives deliverability, API-key rotation without code changes, and a free tier that easily covers Edith’s volume.
- One-day scope discipline. No animations, no image optimization pipeline, no analytics dashboard. Everything that mattered shipped; everything that could wait didn’t make the cut.