Template April 2026
I

built a SaaS starter because every other one is wrong

An opinionated Cloudflare-first template. Next.js 16, D1, Better Auth, Stripe. No monorepo. No edge-runtime religion. No status page.

TL;DR

I shipped saas_template — a Cloudflare-first SaaS starter that takes a stance on every decision so you don't have to.

Most starters are menus pretending to be products. A starter should remove decisions, not multiply them. This post is the decisions I made and why the alternatives are wrong.

The menu problem

Every SaaS starter I've evaluated in the last year is a pile of options pretending to be a product. Authentication provider? Pick from six. Database? Pick from eight. Monorepo or not? Here are both. Deploy target? Whatever you want, king.

That isn't a starter. That's a menu.

A starter exists to remove decisions, not multiply them. Here are the decisions I made, why I made them, and why I think the alternatives are wrong.

Eight decisions

01

Cloudflare for everything

Workers, D1, R2, KV, Queues, Email Routing, Turnstile. One platform, one bill, one dashboard.

02

No monorepo

Flat Next.js app. Promote to workspaces only when you have two real consumers — not before.

03

D1, with a door

Repository layer isolates the DB. 10 GB gets 95% of SaaS there. Swap to Postgres/Turso is one file.

04

Better Auth

Passkeys, MFA, magic links, OAuth. Handles Origin + Fetch Metadata + SameSite. No custom CSRF layer.

05

Stripe hosted

Checkout + Portal, never embedded. Zero PCI scope. Webhook idempotency via D1 unique PK.

06

SSG for SEO, always

Marketing + blog: SSG/ISR at the edge. Auth + dashboard: SSR. API: Workers. Pick per route, not per app.

07

Route-scoped CSP

Static CSP for marketing, nonce CSP for authed routes. SSG stays cacheable. No global nonce middleware.

08

Disciplined by default

AGENTS.md codifies TDD + OWASP Top 10 + 12-Factor + Clean Code. Not aspirational — enforced per PR.

1. Cloudflare for everything

Compute on Workers. DB on D1. Files on R2. Cache on KV. Jobs on Queues. Email routing. Turnstile. Rate limiting. Analytics.

One platform. One bill. One dashboard. One set of credentials.

The usual objection: "but vendor lock-in." Two things. First, every stack is lock-in — using Vercel + Neon + Clerk + Resend + Upstash just spreads it across five vendors while pretending it didn't happen. Second, the actual lock-in surface is smaller than people think. Workers run Node. Drizzle schemas port to Postgres in a day. Stripe and Resend are already portable.

What you're paying for when you go multi-vendor is the illusion of optionality. In exchange you get five signup flows, five credit cards on file, five attack surfaces, and a distributed systems problem you didn't have to have. Pick one platform and commit.

2. No monorepo

Flat Next.js app. No packages/ui. No packages/config. No Turborepo. No pnpm workspaces.

Monorepos are a premature optimization 99% of the time. A starter has one app. Adding apps/web + packages/ui to a starter with zero shared consumers is scaffolding theater — you pay the complexity tax today for a benefit you won't see for eighteen months, if ever.

The right time to extract packages/ui is when two apps actually consume it. Not before. Flatten first, promote later. The refactor is a weekend when the need is real.

3. D1, with a door

D1 is a 10 GB SQLite database. People hear "SQLite" and assume toy. It isn't. It's fast, it's cheap, it has Time Travel built in, it runs at the edge, and 10 GB is more than 95% of SaaS products ever reach.

The legitimate concerns — single-threaded writes, 30-second query cap, 2 MB row limit — are real, but they're concerns at scale. A pre-revenue SaaS does not have them. You can spend six months building on D1 and the day you need more, you haven't locked yourself in because…

Every domain call in the template goes through a repository layer. db/repo/index.ts defines the interfaces; db/repo/d1.ts implements them. Swapping to Postgres or Turso is one file, not a rewrite. That's the entire point. Starters that hardcode db.query.users.findFirst() throughout the app are putting a ticking clock on your runway.

4. Better Auth, not Auth.js, not Clerk

Auth.js is a half-finished library masquerading as a standard. Every time I use it I fight its assumptions. Clerk is great until you're paying them $500/mo for "users over the free tier" and realizing you're paying to rent your own login screen.

Better Auth made the right tradeoffs. Passkeys, MFA, magic links, OAuth — all in. Works on Workers. Handles Origin + Fetch Metadata + SameSite protection itself, which means you don't add a custom CSRF layer that breaks OAuth, passkeys, and webhooks the moment a user's cookies get weird.

I watched a starter last week stack next-auth + a custom CSRF middleware + a third signature check on top. Three layers of security theater, none coherent. When it broke in prod, nobody knew which layer to look at. Better Auth + no extras is one layer of auth you can reason about.

5. Stripe hosted, never embedded

Stripe Checkout + Customer Portal. Hosted URLs. No embedded payment UI.

Three free wins: PCI scope drops to zero, your CSP doesn't whitelist Stripe's entire JS surface, and when Stripe ships new features (tax compliance, local payment methods, subscription upgrades) you get them automatically.

The people embedding Stripe Elements do it because their designer didn't want a 200ms redirect. That is not a reason. It is vanity dressed as UX.

The webhook is where the engineering lives:

Signature verify → insert into stripe_events with a unique PK before the side-effect runs. Stripe retries, guaranteed. Second INSERT fails, side-effect doesn't run, duplicate logged. One DB row or you have a bug.

Most starters use an in-memory Set or Redis SETNX. The Set dies with the Worker. SETNX has eventual-consistency windows you will absolutely hit. Just use the database's unique constraint.

6. SSG for SEO pages, always

Marketing: SSG. Blog: SSG with ISR. Auth and dashboard: SSR. API: Workers handlers.

People debate SSR vs SSG for SEO like it's a real debate. It isn't. Google ranks them equivalently — but SSG wins every performance axis, which feeds ranking. Static HTML at the edge: 20ms TTFB globally. SSR: 150–400ms per request. Cache misses on SSR hit your origin; SSG never misses. Your crawl budget goes further. Your Core Web Vitals stay green without you trying.

The reason people default to SSR is that it's the lazy choice — one rendering mode, one mental model. A starter should choose the right mode per route, not the easy one.

7. Route-scoped CSP, never global nonce

A global nonce CSP via middleware forces dynamic rendering everywhere, which kills SSG for your marketing pages. This is the #1 security mistake in Next.js templates and almost nobody calls it out.

lib/csp.ts builds a static CSP for marketing/blog (strict allowlist, no nonce, fully cacheable) and a nonce-based CSP for auth/app routes (dynamic, per-request). You get strict CSP and edge caching. Not a tradeoff — just doing it correctly.

8. Disciplined by default

The repo ships with an AGENTS.md. It codifies three disciplines:

  1. TDD is priority #1. Write the failing test first. No code without a test. Contract tests at boundaries. One behavior per test. Red, green, refactor, commit.
  2. OWASP Top 10 checklist per PR. Every category mapped to concrete checks in this codebase. Paste the checklist, tick each box, or the PR doesn't merge.
  3. 12-Factor + Clean Code. Config in env, never code. Stateless processes. One responsibility per function. No flag arguments. No narrating comments. TODOs with owner and date or they get deleted.

Not aspirational. It's the file AI assistants read before touching code. If your template doesn't tell Claude/Cursor/Codex how to behave, they'll regress your standards within three PRs. Mine tells them. In detail. Non-negotiable.

What I deliberately left out

A status page. A newsletter system. Multi-tenancy. i18n. Storybook. Admin impersonation. Usage metering in D1 (Analytics Engine when needed). Embedded Stripe. Edge runtime.

Not because they're bad — because they're not earned yet. A starter's job is to get you to paying customers. Everything above is a future problem. The template documents the extension path for each one so you know where to add it when it becomes real.

Leaving things out is harder than putting things in. It's also the actual design work.

Use it

git clone https://github.com/omar16100/saas_template.git my-saas
cd my-saas
pnpm install
cp .env.example .env.local
# generate a secret: openssl rand -base64 32
pnpm dev

Full setup, runbooks, ADR, landing page, and the AGENTS.md:

MIT. Fork it, rip it apart, disagree with every opinion here. That's fine — just disagree on purpose instead of by accident, which is what every other starter forces you to do. The template takes a stance. So should you.