Skip to content

Repository files navigation

Cloudflare SaaS Starter (Flarestack)

A full-stack, edge-native starter kit: TanStack Start on the front, a Hono Worker API on the back, Better Auth on D1 in between. Everything runs on Cloudflare Workers — no containers, no Node server, no separate auth service.

Stack

Layer Choice
Web TanStack Start (SSR) + React 19, TanStack Router / Query, Vite
API Hono on Cloudflare Workers
Auth Better Auth (11 plugins) — email/password, OAuth, passkeys, OTP, 2FA, orgs, SSO, API keys, MCP, admin
Database Cloudflare D1 + Drizzle ORM
Email React Email templates via Cloudflare Email Sending (send_email)
Billing Polar (checkout, customer portal, usage, webhooks) — optional
UI shadcn/ui on Base UI + Tailwind CSS v4, ~60 components
Tooling pnpm workspaces, Turborepo, Biome, TypeScript strict

Layout

apps/
  web/          TanStack Start app → deployed as a Worker
  api/          Hono Worker: /api/auth/* + your routes
packages/
  auth/         Better Auth server + typed React client
  db/           Drizzle schema (D1) + migrations
  email/        React Email templates, renderer, theme
  ui/           shadcn components, app sidebar, app header
  types/        Shared CloudflareEnv binding types

The web Worker talks to the API Worker over a service binding (env.API), not the public internet — see session.ts. Browser-side auth calls go directly to the API origin via VITE_API_URL.

Getting started

pnpm install

1. Create the D1 database and paste the id into apps/api/wrangler.jsonc (it ships as REPLACE_ME):

npx wrangler d1 create example-db

2. Configure secrets:

cp apps/api/.dev.vars.example apps/api/.dev.vars   # then fill in BETTER_AUTH_SECRET
cp apps/web/.env.example apps/web/.env

openssl rand -base64 32   # value for BETTER_AUTH_SECRET

3. Apply migrations to the local D1:

pnpm --filter @example/api migrate:local

4. Run both apps:

pnpm dev          # turbo runs web + api together

Run them individually with pnpm web / pnpm api.

Configuration

Everything optional is off until its env var is set, so the starter boots with nothing but BETTER_AUTH_SECRET. All keys live in apps/api/.dev.vars.example and are typed in packages/types/src/env.ts.

Variable Effect when set
BETTER_AUTH_SECRET Required. Signs sessions.
GITHUB_CLIENT_ID / _SECRET Enables "Continue with GitHub"
GOOGLE_CLIENT_ID / _SECRET Enables Google sign-in
TURNSTILE_SECRET_KEY Captcha-gates sign-in/sign-up/reset (withCaptcha() client helper)
POLAR_ACCESS_TOKEN Mounts checkout, portal, usage, webhooks
ADMIN_USER_IDS Comma-separated ids that always resolve as admin
COOKIE_DOMAIN Cross-subdomain session cookie (e.g. .example.com)
EXTRA_TRUSTED_ORIGINS Required before an SSO IdP's discovery URL is accepted

Social providers are wired in providers.ts; plugins in plugins.ts. Drop a block to remove a feature — nothing else references it.

Database

Drizzle schema lives in packages/db/src/schema/ and mirrors Better Auth's generated schema exactly, including its singular table names (user, session, organization) with plural TS exports. Changing a table name will break the adapter.

pnpm --filter @example/db generate    # schema change → new migration
pnpm --filter @example/api migrate:local

Apply to production with npx wrangler d1 migrations apply example-db --remote from apps/api.

Routes

(marketing)/          public landing
(auth)/               login, signup, forgot-password
(org)/$org/           authenticated shell — sidebar + header + org param

The $org layout route redirects to /login when there's no session, and carries the sidebar/nav/notification config in staticData — see route.tsx. That data is demo content; replace it with your own nav. conversations/ is an empty placeholder route.

Auth forms use react-hook-form + zod via the shared useAuthForm helper, which handles error surfacing and session invalidation after sign-in.

Deploy

pnpm --filter @example/api deploy     # deploy API first — web service-binds to it
pnpm --filter @example/web deploy

Before the first deploy: set production secrets (npx wrangler secret put BETTER_AUTH_SECRET), point BETTER_AUTH_URL / WEB_URL / EMAIL_FROM in wrangler.jsonc at your real domains, and verify your sending domain for Cloudflare Email Routing.

Scripts

Command Does
pnpm dev Web + API together via Turborepo
pnpm build Build every workspace
pnpm typecheck tsc --noEmit across the monorepo
pnpm lint Biome check
pnpm format Biome check + write
pnpm clean Remove all build output and node_modules

Preview email templates with pnpm --filter @example/email preview (port 3001).

Renaming

The starter ships under the @example/* scope with example as the app name. To rebrand, find-and-replace @example/ with your scope across package.json, tsconfig.json paths, and imports, then update name/database_name/service in both wrangler.jsonc files and the appName/issuer/rpName strings in packages/auth/src/.

Requirements

Node ≥ 20, pnpm 10.

About

A full-stack, edge-native starter kit: TanStack Start on the front, a Hono Worker API on the back, Better Auth on D1 in between. Everything runs on Cloudflare Workers — no containers, no Node server, no separate auth service.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages