A production-ready SvelteKit + Cloudflare SaaS boilerplate and starter kit. Clone it, rename it, ship it.
svelteflare.com · github.com/pinebasedev/svelteflare · MIT licensed
Svelteflare gives you a working SaaS skeleton — auth, subscriptions, a themed UI kit, and a typed API — on infrastructure that scales to zero and costs nothing until you have real users. You bring the product; the account system, billing, and deployment plumbing are already wired together and tested.
You're comfortable with Svelte/TypeScript and want to skip the two weeks of boilerplate every SaaS needs before you can build the feature you actually care about: user accounts, subscriptions, a component library, and a deploy pipeline. If you'd rather review working code than read a tutorial, start with apps/web/src/routes after the quick start below.
- Auth that just works — email + password with OTP verification, Google OAuth, and password reset via Better Auth
- Stripe subscriptions — plans, checkout, webhooks, and entitlement checks; gate any route or feature on an active subscription
- 56-component UI kit — a themed shadcn-svelte library (
@repo/ui) with semantic tokens and automatic dark mode - Typed end to end — the SvelteKit client consumes the Hono API through a typed RPC client; rename a field on the server and the frontend fails to compile
- Hardened API — CORS, CSRF protection, rate limiting, secure headers, request timeouts, and structured errors as middleware on every request
- Cloudflare-native — Workers, D1 (SQLite), and Cloudflare Email; no servers to manage and a generous free tier
- Static marketing site — a prerendered one-pager (
apps/marketing) built from the same@repo/uikit, ready to swap in your own copy
| Layer | Tech |
|---|---|
| Frontend | Svelte 5 (runes), SvelteKit (static SPA + prerendered marketing site) |
| API | Hono on Cloudflare Workers |
| Auth | Better Auth (email OTP, Google OAuth, Stripe plugin) |
| Database | Drizzle ORM + Cloudflare D1 |
| Billing | Stripe subscriptions |
| Styling | Tailwind v4 + shadcn-svelte with semantic tokens |
| Infra | Alchemy (dev, deploy) + GitHub Actions |
| Monorepo | pnpm workspaces + Turborepo + just |
apps/
web/ SvelteKit SPA — the app template (search-and-replace "Demo App")
api/ Hono Cloudflare Worker — auth, database, billing
marketing/ Static, prerendered marketing site — delete or repurpose for your product
alchemy/ Cloudflare resources, one file each; alchemy.run.ts assembles them per stage
packages/
ui/ Theme + component library (@repo/ui) — single source of truth for how things look
lint/ Theme/color-token lint rule, the oxlint plugin, and the Svelte-only ESLint config
typescript-config/ Shared tsconfig bases for Svelte apps and the Worker
- Node 24 (see
.nvmrc) - pnpm — the exact version is pinned via
packageManagerand Corepack - just — task runner used for all common commands
- A Cloudflare account (free tier is enough).
pnpm devruns everything locally but needs Alchemy connected to it once:pnpm alchemy profile edit --add Cloudflare
git clone https://github.com/pinebasedev/svelteflare.git my-app
cd my-app
pnpm install
pnpm dev # marketing on :9001, web on :9002, api on :9003pnpm dev is alchemy dev: the API runs in workerd with local D1, R2, and email simulators, and the
web app and marketing site run on their own Vite dev servers. It applies the database migrations on
start and needs no configuration. Emails, such as the sign-up verification code, are written to
.alchemy/local/email/*.eml instead of being sent.
To turn on Google login or billing, copy .env.example to .env and fill in GOOGLE_CLIENT_SECRET
and PUBLIC_GOOGLE_CLIENT_ID, or the STRIPE_* keys. Everything else in it is only for deploying.
Run just with no arguments to see every recipe, grouped. The most common ones:
just dev # run all three apps together (web, api, marketing)
just check # typecheck everything, including the Alchemy stack
just lint # oxlint + ESLint (Svelte markup only)
just format # auto-format with oxfmt
just generate # generate a new migration from the Drizzle schema
just deploy pr-test # deploy a throwaway stage by hand; `just destroy pr-test` removes itThe theme is the combination of semantic variables in packages/ui/src/global.css and the visual implementations in packages/ui/src/components. Feature code uses semantic classes (bg-background, text-muted-foreground, bg-primary, …), while a Custom design may also change component shape, spacing, borders, shadows, typography, and interaction treatment without changing component APIs or behavior. Design review happens in the actual locally running product, using its real features and surfaces. See .agents/skills/ui-and-theme for the full contract.
Alchemy provisions everything from alchemy.run.ts: the API Worker with its
D1 database and R2 bucket, and the web app and marketing site as static-asset Workers. There's no
wrangler.jsonc. Each environment is an Alchemy stage, deployed by GitHub Actions:
| Stage | Deployed when | Workflow |
|---|---|---|
pr-{number} |
a PR into staging is opened or updated |
preview.yml |
staging |
a PR merges into staging |
staging.yml |
prod |
a PR merges into main (promote: merge staging into main) |
production.yml |
A PR's stage is destroyed when the PR closes. Nothing in CI can destroy staging or prod, and
prod's database and bucket are kept even by a hand-run pnpm run destroy. On pr-* and staging the web
app, API, and marketing site sit behind one Cloudflare Access application, and the API also verifies
the Access token itself; prod is public. Every deploy is also reported
to project-ops, Pinebase's deployment dashboard, when PROJECT_OPS_URL is set.
- Set
namein the rootpackage.jsonto your project's name, e.g.my-app. Every Cloudflare resource is named after it (my-app-api-staging, themy-app-citoken, ...), and so is the project in project-ops. Deploys refuse to run while it's stillsvelteflare, and the workflows never deploy from the template repository itself. Keep the name once deployed: changing it starts a new stack and leaves the old one running. pnpm bootstrap:github(seealchemy/github.tsfor the admin profile it needs) mints a Cloudflare API token scoped to exactly what the stack deploys and stores it in the repo's secrets, along with the project-ops token. It also pushes the variables below if they're in your.env.- Set the rest in the repo's Settings → Secrets and variables → Actions:
| Name | Kind | Needed for |
|---|---|---|
BETTER_AUTH_SECRET |
secret | every stage (openssl rand -base64 32) |
CLOUDFLARE_WORKERS_SUBDOMAIN |
variable | every stage on workers.dev (dashboard: Workers & Pages) |
CF_ACCESS_ALLOW_EMAIL |
secret | pr-*, staging: who may log in (comma-separated) |
CF_GOOGLE_IDP_ID |
variable | pr-*, staging: the Zero Trust Google identity provider |
CF_ACCESS_TEAM_DOMAIN |
variable | pr-*, staging: the Zero Trust team name |
GOOGLE_CLIENT_SECRET, STRIPE_SECRET_KEY |
secret | optional features |
STRIPE_API_KEY |
secret | registering each stage's Stripe webhook endpoint |
PUBLIC_GOOGLE_CLIENT_ID, EMAIL_FROM_* |
variable | optional features |
PROD_API_DOMAIN, PROD_APP_DOMAIN, PROD_MARKETING_DOMAIN |
variable | optional custom domains on prod |
PROJECT_OPS_URL, CF_ACCESS_CLIENT_ID/_SECRET |
var/secrets | reporting to project-ops |
Until CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID exist, every workflow exits green without
deploying.
Repository-level values serve staging and previews. For prod, create a GitHub Environment named
production and set the values that must differ there: the live-mode STRIPE_SECRET_KEY, a live
restricted STRIPE_API_KEY (webhook endpoints: write only), and its own BETTER_AUTH_SECRET.
Environment secrets override repository ones for the prod deploy only.
Verification codes and password resets go out through
Cloudflare Email Service. Onboard a sending
domain once (dashboard: Email Service → Onboard Domain; with the domain's DNS on Cloudflare, the
DKIM/SPF records are created for you), then set EMAIL_FROM_ADDRESS to an address on it. Every
stage then sends real email, whatever hostname its Worker runs on; without it, emails are skipped
and logged. Use separate senders so preview testing can't hurt production's sending reputation: a
repository variable on a test subdomain (e.g. noreply@test-mail.example.com) for staging and
previews, and a production environment variable (noreply@mail.example.com) for prod. All stages
share the account's daily sending quota, which starts small and grows over time.
Google only redirects to callback URLs registered in advance, and each preview's URL is new. So on
pr-* and staging, better-auth's OAuth proxy routes Google's callback through staging's API, which
exchanges the code and hands the profile back to the preview, encrypted with the
BETTER_AUTH_SECRET the two share. The user and session are created in the preview's own database.
Register two redirect URIs on the Google OAuth client, once (<APP> is your package.json name):
https://<APP>-api-staging.<CLOUDFLARE_WORKERS_SUBDOMAIN>.workers.dev/v1/auth/callback/google- prod's API URL +
/v1/auth/callback/google(prod doesn't use the proxy)
staging and every pr-* stage share one Stripe sandbox; prod uses live mode. Each stage's
webhook endpoint is registered by the stack (alchemy/Stripe.ts) and its signing secret passed
straight to the API: created and deleted with a PR's preview, created once for staging, and kept
even on destroy for prod. Because the sandbox is shared, each endpoint also receives the other
stages' events, which better-auth ignores. Stripe allows 16 endpoints per account, so about 15 open
PRs can have webhooks at once. On the gated stages, Access lets exactly the webhook path through;
Stripe's signature, which better-auth checks, protects it. Locally, run
stripe listen --forward-to localhost:9003/v1/auth/stripe/webhook and put the secret it prints in
.env as STRIPE_WEBHOOK_SECRET.
With the same values in .env, pnpm run deploy --stage pr-test deploys a throwaway stage from your
machine and pnpm run destroy --stage pr-test removes it. Local state lives in .alchemy/; CI keeps its
state in Cloudflare.
- Custom domains need zone permissions the CI token doesn't have (see
alchemy/ciTokenPolicies.ts). - Cross-site cookies. The web app and API are separate hosts. On
workers.devthey're the same site (workers.devis a public suffix, so both share<account>.workers.dev), and so are sibling subdomains of one domain (app.andapi.). Unrelated domains would make the session cookie cross-site, so keep production on one domain. - Patched Alchemy. Alchemy 2.0.0-beta.79 doesn't expose the Access application's CORS and cookie
settings the gated API needs;
patches/alchemy@2.0.0-beta.79.patchadds them. Upgrading Alchemy fails the install until the patch is redone or dropped (drop it once Alchemy ships the settings).
If you're extending this repo with Claude Code or a similar tool, read AGENTS.md first — it covers the non-negotiable theming rules — and the guides in .agents/skills/ for feature work, forms, and auth/billing patterns.
Svelteflare is an open-source project maintained by Pinebase. It exists so developers don't have to rebuild auth, billing, and UI infrastructure from scratch for every new SaaS idea — clone it, make it yours, and spend your time on the product instead.
Questions or ideas? Open an issue on GitHub.