Next.jsReactSaaSStarter

saas_template — Next.js SaaS Template

By omar16100

A Cloudflare-oriented SaaS starter template built on the Next.js App Router, integrating D1, Better Auth, and Stripe, with marketing pages, authentication, dashboard, and SEO infrastructure.

Explore the design

Compare the page layouts and mobile experience. Open any screenshot for a closer look.

2 previews
Desktop homepage1 / 2
Desktop captureTop of page

Actual screenshot captured by the server browser.

All pages & views 2

Screenshots show visual appearance; interactive behavior may differ.

Recorded 2026-10-01View source

What stands out

A quick read of the recorded project description.

01

Cloudflare as the runtime platform: Workers compute, D1 database, KV cache, R2 storage, Queues, and Email Routing

02

Better Auth provides email/password, Google OAuth, magic links, Passkeys, and 2FA

03

Stripe-hosted Checkout and Customer Portal, with webhooks using D1 for idempotent records

04

SEO infrastructure: dynamic sitemap, robots, RSS, JSON-LD, IndexNow, llms.txt, and noindex for preview environments

05

Secure defaults: per-route CSP, HSTS, X-Frame-Options DENY, Referrer-Policy, and Permissions-Policy

06

Repository layer db/repo/* isolates D1, making it easier to swap databases later without changing domain code

Template Details

This is a SaaS starter template that treats Cloudflare as its primary runtime platform; the repository is MIT-licensed. The stack centers on the Next.js App Router, deployed to Cloudflare Workers via @opennextjs/cloudflare, with D1 and Drizzle for data, Better Auth for authentication, and Stripe-hosted Checkout and Customer Portal for payments. Rendering is split by route: marketing pages and blog use SSG/ISR for SEO, authentication and dashboard use SSR, and API routes run on Workers. The repository is a flat Next.js application, not a monorepo.

The template includes page structures for marketing, pricing, blog, legal pages, sign-in/sign-up and password reset, and an authenticated dashboard, along with a dynamic sitemap, robots, RSS, JSON-LD, an IndexNow endpoint, llms.txt, and consent-gated GA4 and PostHog analytics. On security, it provides per-route CSP, strict security headers, idempotent D1 records for Stripe webhooks, and account deletion plus data export to R2.

Note that the README explicitly lists several modules that are not yet wired end to end: the Turnstile component renders but no route calls the verification function, rate-limiting utilities exist but are not connected to routes, account deletion only enqueues with no consumer processing it, the root layout canonical affects pages that do not set their own canonical, and billing has inconsistent price variable naming, portal redirect, and webhook retry logic issues. The template intentionally excludes multi-tenancy, i18n, Storybook, status pages, and newsletters.

Next.jsSaaSStarter

Is it right for your project?

Use Cases

  • Starting a SaaS project that needs Cloudflare Workers and D1 as its runtime foundation
  • A Next.js application where marketing pages and blog use SSG/ISR while authentication and dashboard use SSR
  • A starter template with built-in authentication, subscription billing, email templates, and SEO metadata
  • A deployment approach that uses compute, database, object storage, queues, and email routing within the same platform

Suitable For

  • Developers familiar with the Next.js App Router and the Cloudflare ecosystem
  • SaaS product prototypes or early-stage projects with a single-user model
  • Teams that accept the template's opinionated structure and are willing to replace components as needed
  • Scenarios that need an SEO-friendly marketing site alongside a protected dashboard

Getting started & customization

Use these recorded setup notes alongside the repository instructions for your chosen version.

  1. 1

    After cloning, run pnpm install, copy .env.example to .env.local, and at minimum generate BETTER_AUTH_SECRET

  2. 2

    Use wrangler to create D1, KV, R2, and Queues resources, and fill the returned IDs into the REPLACE_ME placeholders in wrangler.toml

  3. 3

    Configure BETTER_AUTH_SECRET, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, RESEND_API_KEY, and TURNSTILE_SECRET_KEY per environment via wrangler secret put

  4. 4

    NEXT_PUBLIC_* variables are not secrets and must be set in the build environment; vars in wrangler.toml only override runtime reads

  5. 5

    Before deployment, complete the checklist: domain routing, Resend sending domain verification, Stripe webhook registration, Turnstile site creation, and search platform verification

Read repository instructions

License & source material

Usage conditions & limitations

Free to use · keep license notice

This entry covers the free version in the public repository under the MIT license; the free scope is that version's code and does not imply that external services or third-party assets are free.

Must comply with when using

  • You may use, modify, commercialize, and distribute this version's code for free.
  • When distributing copies or substantial portions of the code, retain the original author's copyright notice and the full MIT license notice.
  • The code is provided as is; warranties and limitations of liability are governed by the original license and applicable law.

Additional costs & items to verify

  • The template requires you to configure credentials to run fully: authentication needs BETTER_AUTH_SECRET, and related features degrade when Stripe, Resend, or Turnstile are not configured.
  • Deployment and resource creation require a Cloudflare account and wrangler login, which are external service dependencies.
  • Deployment requires connecting a domain to Cloudflare; local development does not require it.
  • Email sending depends on the Resend service, requiring a verified sending domain and DKIM, SPF, and DMARC configuration.
  • Billing depends on Stripe, requiring webhook registration per environment and a signing secret.
  • Human verification depends on Cloudflare Turnstile, requiring site creation and key configuration.
  • Log export depends on the Axiom service, requiring a dataset and token plus Logpush configuration.
  • Analytics depends on PostHog and GA4, requiring project and property creation and ID configuration.
  • Search platform verification requires registering the site with Google Search Console and Bing Webmaster Tools.
  • IndexNow requires generating a key file yourself and placing it in the public directory.
  • Turnstile only renders the component; no route calls the verification function, so protection is not wired end to end.
  • Rate-limiting utilities exist but are not connected to any route, so they are not actually in effect.
  • Account deletion only enqueues, with no queue consumer processing it, so the deletion flow is incomplete.
  • The root layout canonical points to the homepage, so pages that do not set their own canonical are pointed to the homepage by search engines.
  • Billing has defects: inconsistent price variable naming, portal redirect not implemented, and webhook failures are not retried.
  • Demo images, fonts, icons, and other third-party assets must be checked against their respective licenses; the code's MIT license alone cannot be used to judge them.
  • External services such as domains, hosting, databases, payments, or AI may incur separate charges.
View Original Text Basis

Permission is hereby granted, free of charge

Original License Text

The app boots with degraded features until you fill in credentials (auth needs the secret; Stripe/Resend/Turnstile each fail gracefully if unconfigured).

Original README

a Cloudflare account · `wrangler login`

Original README

Domain added to Cloudflare (can be a subdomain); only required for deploy, not local dev

Original README

Verify your sending domain in **Resend** (DKIM + SPF + DMARC)

Original README

Register a **Stripe** webhook endpoint per environment → paste signing secret

Original README

Create **Turnstile** site → paste keys

Original README

Create **Axiom** dataset + token → set up Cloudflare Logpush job → Axiom

Original README

Create **PostHog** project + **GA4** property → paste IDs

Original README

Add site to **Google Search Console** and **Bing Webmaster Tools** → paste verification tokens

Original README

Generate `INDEXNOW_KEY` → serve at `/{key}.txt` under `public/`

Original README

Turnstile: the widget renders on sign-in/sign-up, but no route calls `verifyTurnstile` in `lib/turnstile.ts`.

Original README

Rate limiting: `lib/rate-limit.ts` exists, but no route calls it.

Original README

Account deletion enqueues a purge job, but no queue consumer processes it.

Original README

The root layout sets canonical `/`, so pages without their own `alternates.canonical` (pricing, blog index, auth pages) point search engines at the homepage.

Original README

Billing: the pricing button reads `NEXT_PUBLIC_STRIPE_PRICE_*` while `.env.example` defines `STRIPE_PRICE_*`; the dashboard "Manage billing" form receives the portal URL as JSON instead of being redirected; the webhook records an event before applying it, so a failed apply is not retried.

Original README
https://github.com/omar16100/saas_template/blob/main/LICENSEhttps://github.com/omar16100/saas_template/blob/main/README.md

Compiled by DeepSeek based on repository license and README ·2026-10-02. Results only cover the materials read; dependencies, images, fonts, and subsequent term changes are subject to their respective original texts.

Project checks & description sources

Template Information

Source platforms
GitHub
Documentation and maintenance reference score
More documented · 88 / 100
Technologies
Next.js · React
Website type
SaaS · Starter
How this score is calculated

Our heuristic uses license records (up to 30), a demo link (12), a preview (10), stars (15), recorded maintenance (15), description completeness (8), feature records (5) and classification data (5). It is not a code, security or performance test, or a Google score. A link does not establish tested functionality.

What to verify for this project

Repository update recorded
2026-09-27
Screenshot last checked
2026-10-02

A demo link is listed. Check it with your own content and viewport sizes; its presence does not confirm that forms, authentication or payments work.

The catalog identifies Next.js as the primary stack. Check the actual dependency manifest and lockfile for versions and hosting requirements. These notes use recorded metadata, not a code audit or a deployment test.

Use a selection guide →

How this description was prepared

AI-assisted description based on catalog records and repository material. Repository authors are not automatically the reviewers of this description.

Automatically checked against source and license rules; no manual editorial review.

Inspect the recorded source material

SHA-256: fe7ba73becb456ad7620e8b5acc049bc8d0d7ba0f44c92c5f6ad81c923aca0a8

2026-10-02T01:00:59.807Z

Source file / repository
# SaaS Template

[![Website](https://img.shields.io/badge/website-omar16100.github.io-f38020)](https://omar16100.github.io/saas_template/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

**Website:** https://omar16100.github.io/saas_template/

Opinionated, Cloudflare-first SaaS starter. Clone, rename, deploy.

**Stack:** Next.js 16 (App Router) on Cloudflare Workers via `@opennextjs/cloudflare` · D1 + Drizzle · Better Auth (passkeys, MFA, OAuth, magic links) · Stripe hosted Checkout + Portal · Resend + CF Email Routing · shadcn/ui + Tailwind · GA4 + PostHog (consent-gated) · Turnstile · Axiom via Logpush · Vitest + Playwright · GitHub Actions CI + opt-in Cloudflare deploy.

**Rendering:** Marketing + blog → SSG/ISR (edge-cached, best SEO). Auth + dashboard → SSR. API routes → Workers.

---

## Why this template

- **One platform.** Every runtime dependency (compute, DB, files, cache, queues, email routing) lives on Cloudflare. One bill, one dashboard, one set of credentials.
- **SEO-first.** Static marketing + blog, dynamic sitemap, JSON-LD, IndexNow, `llms.txt`, preview auto-`noindex`, Lighthouse report on every PR (non-blocking).
- **Secure by default.** Route-scoped CSP, strict security headers, D1-backed Stripe webhook idempotency, Better Auth with passkeys, Turnstile widget and D1 rate-limit helpers, account delete + data export (see [Known gaps](#known-gaps)).
- **Boring, swappable stack.** Repository layer (`db/repo/*`) isolates D1 so you can swap to Postgres/Turso later without touching domain code.
- **No monorepo tax.** Flat Next.js app. Promote to a workspace later only if you genuinely need to.

---

## Quick start

```bash
git clone https://github.com/omar16100/saas_template.git my-saas
cd my-saas
pnpm install
cp .env.example .env.local
# fill in BETTER_AUTH_SECRET at minimum: openssl rand -base64 32
pnpm dev
```

Visit `http://localhost:3000`.

The app boots with degraded features until you fill in credentials (auth needs the secret; Stripe/Resend/Turnstile each fail gracefully if unconfigured).

---

## Full setup

### 1. Prereqs
- Node 22 (see `.nvmrc`) · pnpm 9 · a Cloudflare account · `wrangler login`
- Domain added to Cloudflare (can be a subdomain); only required for deploy, not local dev

### 2. Install

```bash
pnpm install
cp .env.example .env.local
```

### 3. Local secret

```bash
# Generate a 32+ char secret for Better Auth
openssl rand -base64 32
# paste into .env.local → BETTER_AUTH_SECRET
```

### 4. Create Cloudflare bindings

```bash
# Databases
wrangler d1 create saas_db
wrangler d1 create saas_db_preview

# KV (cache)
wrangler kv namespace create CACHE
wrangler kv namespace create CACHE --preview

# R2 buckets (uploads + Next ISR cache, both envs)
wrangler r2 bucket create saas-uploads
wrangler r2 bucket create saas-uploads-preview
wrangler r2 bucket create saas-next-cache
wrangler r2 bucket create saas-next-cache-preview

# Queues (main + DLQ, both envs)
wrangler queues create saas-jobs
wrangler queues create saas-jobs-dlq
wrangler queues create saas-jobs-preview
wrangler queues create saas-jobs-preview-dlq
```

Paste the returned IDs into `wrangler.toml` wherever you see `REPLACE_ME`.

### 5. Secrets per environment

```bash
# Production
wrangler secret put BETTER_AUTH_SECRET --env production
wrangler secret put STRIPE_SECRET_KEY --env production
wrangler secret put STRIPE_WEBHOOK_SECRET --env production
wrangler secret put RESEND_API_KEY --env production
wrangler secret put TURNSTILE_SECRET_KEY --env production

# Preview (repeat with TEST keys)
wrangler secret put BETTER_AUTH_SECRET --env preview
wrangler secret put STRIPE_SECRET_KEY --env preview
# ...etc
```

Public (`NEXT_PUBLIC_*`) vars are not secrets, so never `wrangler secret` them. `next build` inlines them, so they must be set where you build (`.env.local`, your shell, or the GitHub environment used by `deploy.yml`); the `[env.*.vars]` blocks in `wrangler.toml` only cover runtime reads.

### 6. Database

```bash
pnpm db:migrate:local    # apply db/migrations locally
pnpm db:migrate:preview  # apply to preview D1
pnpm db:migrate:prod     # apply to production D1
pnpm db:generate         # after editing db/schema: write the next migration, then commit it
```

If your D1 already has tables from migrations you generated before `0000_initial.sql` was committed, keep your own `db/migrations` history and run `pnpm db:generate` instead of applying it (see `docs/27092026_auth_schema_plan.md`).

`db/schema/auth.ts` must hold every table and field the enabled Better Auth plugins write: Better Auth checks it on every auth request and fails the request on a mismatch. `tests/unit/auth-schema.test.ts` runs the same check, so adding a plugin without its tables fails `pnpm test`.

### 7. Run

```bash
pnpm dev         # http://localhost:3000
pnpm build       # next build
pnpm build:worker # next build + OpenNext worker bundle (.open-next/)
pnpm preview     # run the built worker locally (after build:worker)
pnpm typecheck
pnpm lint
pnpm test        # vitest
pnpm test:e2e    # playwright
```

### 8. Deploy

```bash
pnpm deploy:preview   # wrangler deploy --env preview
pnpm run deploy       # wrangler deploy --env production (`pnpm deploy` is a pnpm builtin, so use `run`)
```

GitHub Actions can do this for you: `.github/workflows/deploy.yml` deploys a preview on every PR and production on merge to `main` once you add repo secrets `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`. Create GitHub environments `preview` and `production`, each with at least the variable `NEXT_PUBLIC_APP_URL` plus any other `NEXT_PUBLIC_*` values your build needs; the workflow exports them before building and sets `NEXT_PUBLIC_IS_PREVIEW` itself. Without the secrets the deploy job is skipped and the workflow passes. Secrets are not passed to PRs from forks or Dependabot, so those PRs skip it too.

### 9. Post-deploy checklist

- [ ] Add your domain to CF and route `example.com/*` in `wrangler.toml`
- [ ] Enable **CF Email Routing** (inbound only: support@, hello@ → your inbox)
- [ ] Verify your sending domain in **Resend** (DKIM + SPF + DMARC)
- [ ] Register a **Stripe** webhook endpoint per environment → paste signing secret
- [ ] Create **Turnstile** site → paste keys
- [ ] Create **Axiom** dataset + token → set up Cloudflare Logpush job → Axiom
- [ ] Add site to **Google Search Console** and **Bing Webmaster Tools** → paste verification tokens
- [ ] Generate `INDEXNOW_KEY` → serve at `/{key}.txt` under `public/`
- [ ] Create **PostHog** project + **GA4** property → paste IDs

See `docs/setup.md` for verbose version and `todo.md` for the full one-time checklist.

---

## Environment variables

Full list in `.env.example`. Categorized in `docs/setup.md`. The minimum to boot locally: `NEXT_PUBLIC_APP_URL`, `NEXT_PUBLIC_APP_NAME`, `BETTER_AUTH_SECRET`.

---

## Project structure

```
app/
  (marketing)/     # SSG/ISR: landing, pricing, blog, legal
  (auth)/          # sign-in, sign-up, reset
  (app)/           # SSR, authed dashboard
  api/             # auth, stripe, indexnow, account, health
  sitemap.ts  robots.ts  feed.xml/
components/        # UI + consent + analytics + JSON-LD
content/blog/      # MDX posts
db/
  schema/          # Drizzle schema (auth, billing, app)
  repo/            # repository layer: D1 impl today, swappable
  migrations/      # SQL from `pnpm db:generate`, committed; CI fails if it is out of date
emails/            # react-email templates
lib/               # auth, db, stripe, resend, logger, csp, rate-limit, env
tests/             # unit (vitest) + e2e (playwright)
docs/              # index, c4model, setup, runbooks/, adr/
```

---

## What's included

- **Auth:** email+password, Google OAuth, magic links, passkeys, 2FA (Better Auth)
- **Billing:** Stripe hosted Checkout, Customer Portal, webhook with D1 idempotency, Stripe Tax
- **Email:** Resend outbound + react-email templates (welcome, verify, reset, magic link, deletion)
- **Bot / abuse:** Turnstile widget on sign-in/sign-up, D1 fixed-window rate limiter helper (both still to be wired into routes, see [Known gaps](#known-gaps))
- **Security:** route-scoped CSP (static for marketing = SSG-safe, nonce for app), HSTS, X-Frame-Options DENY, no-sniff, strict Referrer-Policy, Permissions-Policy
- **SEO:** dynamic sitemap, robots (preview `noindex`), RSS, JSON-LD (Organization / WebSite+SearchAction / BreadcrumbList / Article / FAQPage / SoftwareApplication), IndexNow endpoint, `llms.txt`, canonical URLs on blog posts and legal pages, consent-gated GA4 + PostHog, web-vitals → PostHog, Lighthouse report on PRs
- **Privacy:** consent banner gates analytics, account delete w/ 30-day grace (enqueued; consumer not included yet), account data export to R2
- **Ops:** structured JSON logging (ship to Axiom with Cloudflare Logpush, see `docs/setup.md`), CI (lint, typecheck, unit tests, migration generation, production build, non-blocking Lighthouse on PRs), opt-in deploy (preview per PR, prod on main), Dependabot
- **Docs:** C4 diagram, setup, runbooks (D1 escape hatch, backup/restore, Stripe isolation, passkey domain binding), ADR

## Known gaps

`.github/workflows/ci.yml` installs, lints, typechecks, tests and builds the template on every PR and push to `main`. As of 27 Sep 2026 these parts are scaffolded rather than wired end to end (tracked in `todo.md`):

- Turnstile: the widget renders on sign-in/sign-up, but no route calls `verifyTurnstile` in `lib/turnstile.ts`.
- Rate limiting: `lib/rate-limit.ts` exists, but no route calls it.
- Account deletion enqueues a purge job, but no queue consumer processes it.
- The root layout sets canonical `/`, so pages without their own `alternates.canonical` (pricing, blog index, auth pages) point search engines at the homepage.
- Billing: the pricing button reads `NEXT_PUBLIC_STRIPE_PRICE_*` while `.env.example` defines `STRIPE_PRICE_*`; the dashboard "Manage billing" form receives the portal URL as JSON instead of being redirected; the webhook records an event before applying it, so a failed apply is not retried.

## Intentionally not included

- Monorepo / Turborepo (flat app; promote only when needed)
- Multi-tenancy / orgs (single-user model; extension path in ADR)
- i18n / hreflang
- Storybook
- Status page
- Newsletter
- Custom ML/AI features

---

## Philosophy

Every dependency was picked to **reduce later regret, not to maximize current convenience**:

- Stripe *hosted* Checkout over embedded payment UI → card details are entered on Stripe's page, not yours, and CSP stays simple
- D1 *with a repo boundary* → cheap today, swappable when you outgrow D1's size limits
- Flat app *without* a monorepo → no scaffolding tax until you actually have multiple apps
- Better Auth *without* custom CSRF → one layer, not two that fight each other
- SSG for SEO pages *always* → crawlers see instant HTML, Google rewards you, cache misses never hit origin

---

## License

MIT, see `LICENSE`.

## Contributing

PRs welcome. Before submitting:

```bash
pnpm lint && pnpm typecheck && pnpm test
```

Keep the template opinionated: if a feature is "nice to have for some projects", put it in `docs/` as an extension, not in the main template.
Report a correction

Explore similar templates

Compare previews and descriptions within the same stack and category.