cloudflare-saas-stack: the starter kit that argues with itself about how many Cloudflare products you need
Quickly make and deploy full-stack apps with database, auth, styling, storage etc. figured out for you. Add all primitives you want.
At a glance
- What is it?
- A Next.js template wired to D1, Drizzle and NextAuth, with a README that promises zero environment variables and a repository that ships three required ones. Both versions of the claim are in here.
- Who is it for?
- cloudflare-saas-stack has already made the decisions most SaaS templates leave open, which is worth more than the polish in the README, but the documentation drifted behind the code in three specific places: the clone URL still points at Dhravya, the migration script name is misspelled as db:migrate:prd, and the zero-environment-variables claim sits in the same file as a required .env.example.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the kit actually bundles
The description field says it lets you add all primitives you want, and the README's stack list names seven specific choices: Next.js for the frontend, TailwindCSS for styling, Drizzle ORM for database access, NextAuth for authentication, Cloudflare D1 for serverless databases, Cloudflare Pages for hosting and ShadcnUI as the component library. That is the entire value proposition, and it is a narrow one. You are not learning anything about SaaS architecture here, you are skipping the wiring decisions so you can start writing product code against a database and an auth provider on the first day.
The repository metadata backs up the size claim. It has 3747 stars and 343 forks with 4 open issues, the default branch is main, the language is TypeScript, and the last push was on 2026-10-02. It is not archived, and there are no releases at all, no tags to speak of. So popularity here means people starred a template, not that they are consuming a versioned dependency.
Two topics are attached, `cloudflare` and `cloudflare-saas-stack`, which is a thin label set for a project whose entire premise is a technology stack. Compare that to the topic lists on comparable starter repositories, which tend to enumerate every framework they include. This one is tagged as a Cloudflare thing and nothing more.
The file tree gives the clearest picture. Twenty-one top-level entries including `drizzle/`, `src/`, `public/`, `scripts/`, `wrangler.toml`, `drizzle.config.ts`, `components.json`, `next.config.mjs`, `tailwind.config.ts` and `cf-env.d.ts`. The presence of `wrangler.toml` alongside a Next.js app is the shape of the whole project: a Node framework wrapped in Cloudflare's build and deploy tooling.
The clone URL and the repository do not match
Step 2 of Getting Started tells you to clone this:
git clone https://github.com/Dhravya/cloudflare-saas-stack
cd cloudflare-saas-stack
npm i -g bun
bun install
bun run setupThe repository this article is written about lives at `supermemoryai/cloudflare-saas-stack`. The README points at `Dhravya`, a personal account rather than the organization account. Both paths almost certainly resolve, since this is the same project that was transferred or forked between accounts, but they are not guaranteed to be the same commit from this moment on.
Write both facts down rather than picking one. The README's clone command is stale by the standards of the rest of the file, which is otherwise current: it references `c3` bootstrapping, the `@cloudflare/next-on-pages` CLI, Wrangler links to current documentation URLs and a D1 REST API path. It is not an old README. It is a current README with one line that did not get updated when ownership moved.
How to judge it in thirty seconds: clone the README's URL, run `git remote -v`, and check whether the origin points at `supermemoryai` or at the personal account. If it points at the personal account, you are on a fork that will stop receiving pushes when the author stops pushing. Switching the remote takes one command, and it is worth doing before you write any code, because the next change you want will arrive as a commit on the organization copy rather than your local one.
Zero environment variables, except for three
The final section of the README is called The Beauty of This Stack, and the second bullet claims no environment variables are needed, with the parenthetical that you should use `env.DB`, `env.KV`, `env.Queue`, `env.AI` and similar. The same file ships `.env.example`, which contains this:
CLOUDFLARE_D1_ACCOUNT_ID=
DATABASE=
CLOUDFLARE_D1_API_TOKEN=Both statements are true at once, and figuring out which one applies to you is the single most useful thing this article can tell you. The `env.*` claim describes runtime access inside a deployed Pages Function, where bindings arrive as properties of the environment object and the code reads `env.DB` without importing anything. The `.env.example` variables are consumed by drizzle-kit, which runs on your machine, outside the Worker runtime, and therefore has no bindings to read. That is why they exist, and the comment at the top of the file says so: these are the variables for running drizzle-kit on your production database.
The README explains the same split one section earlier, in more detail than the bullet does. In dev mode, drizzle-kit modifies SQLite files directly, and Wrangler generates them under `.wrangler/state/v3/d1` after `bun run dev`. The `db:migrate:prod` and `db:studio:prod` scripts instead use the d1-http driver, which reaches the remote database over REST and needs the account ID, the database name and an API token. Three environment variables, used only when you migrate or browse the production database, and never needed by the deployed app.
There is a third environment file the README does not mention in the bullet at all. The manual setup section asks for `.dev.vars` in the project root containing `AUTH_SECRET`, which it says you can generate with `openssl rand -base64 32` or `bunx auth secret`, plus `AUTH_GOOGLE_ID` and `AUTH_GOOGLE_SECRET` for Google OAuth. Those are the variables the bundled auth actually needs, and the OAuth consent screen and credential creation steps are the most laborious part of the whole setup. The zero-variables claim is not just partially wrong, it is wrong in a way that hides the slowest step in getting started.
A practical way to separate the three groups: bindings in `wrangler.toml` for runtime, `.env.example` for production migrations, `.dev.vars` for local auth secrets. The README documents all three and then summarises them as none, so read the manual setup section rather than the closing section when you are actually setting the project up.
The migration script name in the README does not exist in package.json
The Database Migrations section says to apply migrations with `bun run db:migrate:dev` for development and `bun run db:migrate:prd` for production. There is no `prd` script. The scripts block in package.json defines `db:migrate:dev`, `db:migrate:prod`, `db:studio:dev` and `db:studio:prod`, with `prd` nowhere in the file. Running the README's production command verbatim returns a bun error rather than a migration.
The same section also glosses the studio commands as `bun run db:<migrate or studio>:dev` and `:prod`, which do exist. So one of the four documented commands is right, one is wrong, and the failure is silent in the sense that nothing warns you in advance. This is the class of small drift that accumulates in a README nobody has rewritten end to end in a while.
The manual setup section gets it right, twice. For a local migration it offers `bunx wrangler d1 execute ${dbName} --local --file=migrations/0000_setup.sql` or `bun run db:migrate:dev`, and for the remote one the same command with `--remote` or `bun run db:migrate:prod`. Both spellings that actually exist appear there, so a reader who follows the numbered manual steps instead of the summary block gets working commands.
Since you have to create the migration file before either of these work, the sequence matters. `bun run db:generate` produces the files under `drizzle/`, and the file name the manual section references, `migrations/0000_setup.sql`, is the convention it assumes. If your generated file is named differently, substitute the actual filename, because D1's execute command takes an exact path rather than picking the newest file for you.
One more note on the runtime target. `pages:build` calls `@cloudflare/next-on-pages`, and `deploy` runs that build followed by `wrangler pages deploy`. That is the Pages path rather than the Workers path, and it is why the README advises previewing periodically instead of trusting `bun run dev`, since the adapter has to translate server-side code that the dev server runs natively. The README's warning is the honest part: local dev is optimal, and periodically verifying the Pages build catches the cases where it is not.
R2 storage is documented, and the dependency list does not mention R2
There is a section titled Cloudflare R2 Bucket CORS / File Upload that opens with a reminder to add a CORS policy to the bucket, then gives the policy itself:
[
{
"AllowedOrigins": [
"http://localhost:3000",
"https://your-domain.com"
],
"AllowedMethods": [
"GET",
"PUT"
],
"AllowedHeaders": [
"Content-Type"
],
"ExposeHeaders": [
"ETag"
]
}
]The CORS shape is the familiar one for browser uploads: allow GET and PUT, allow the Content-Type request header, and expose ETag so the client can read it back after a PUT. Without ExposeHeaders on ETag you cannot see the object version the upload produced, which is the detail most hand-written policies leave out.
Now the mismatch. The stack list at the top of the README names seven components and R2 is not one of them, and the dependencies in package.json contain no R2 client library at all. There is no `@aws-sdk/client-s3`, no Cloudflare storage binding helper, nothing. What is in the dependency list is `@libsql/client` at ^0.10.0, which is a client for libSQL and Turso, plus `drizzle-orm` at ^0.33.0 and `drizzle-kit` at ^0.24.2 as the ORM and its migration tool. The repository description mentions storage as one of the primitives, and the README documents the bucket CORS setup in detail, so file upload is clearly intended to work.
Write both facts side by side. The bucket configuration instructions are complete enough to follow, and the absence of an R2 library in package.json means either the upload code uses a bare `fetch` against the bucket endpoint, or the storage piece is the part you are expected to add. Given that the description advertises storage as included, check `wrangler.toml` for an R2 binding after you clone. If a binding is declared there and no code uses it yet, you have your answer in one file.
This is also the pattern to watch across the whole template. Three primitives in the description, and the honest accounting is: database and auth are fully wired with libraries in package.json, styling and components are wired with Tailwind and ShadcnUI, and storage is wired as far as the bucket configuration and nothing further.
The Supermemory claim is a fact about another repository
The second paragraph of the README says this is the same stack used to build Supermemory.ai, which is open source at git.new/memory, and that it now has 20k+ users running on $5/month. Both numbers are unverifiable from this repository and are about a different codebase. That is not a criticism so much as a reading instruction: the cost figure describes one application's infrastructure bill, not a promise about what your clone will cost.
A $5/month bill for a SaaS serving 20k users is plausible given D1 and Pages pricing, and the README repeats the figure in its closing section as cost-effective scaling. But infrastructure cost for an application depends on request volume, database size, whether R2 is serving large files and whether you stay inside the free tiers. The same kit with a video upload feature and a heavy background job queue looks nothing like the same kit with a text interface over one table. Treat the $5 as an existence proof that the architecture can be cheap, not as a line item for your own project.
The linked Supermemory repository is worth reading for a different reason. If you intend to run this stack rather than only read it, comparing what the production app added on top of the template tells you which parts of the template are load bearing. A starter that became a real product with 20k users necessarily has error handling, rate limiting, background jobs and billing, and none of those are in the tree here.
One structural observation about maintenance signals. There are no releases and no tags, so the package version is 0.1.0 and marked private, which is correct for a template that is cloned rather than installed. There is a `.cursorrules` file at the repository root, which means the project is configured for AI-assisted editing and the rules it relies on are visible in the repository rather than hidden in someone's editor settings. For a template you will fork, having that file present is a small convenience.
Editorial conclusion
cloudflare-saas-stack has already made the decisions most SaaS templates leave open, which is worth more than the polish in the README, but the documentation drifted behind the code in three specific places: the clone URL still points at Dhravya, the migration script name is misspelled as db:migrate:prd, and the zero-environment-variables claim sits in the same file as a required .env.example. The fastest way to size up the project is to run bun install and bun run setup, then read wrangler.toml to see which bindings you actually ended up with, since that file is where the four open issues on this repository are most likely to surface.
Frequently asked questions
What is cloudflare-saas-stack used for?
It is a starter template for building a SaaS product on Cloudflare. It gives you a Next.js frontend with TailwindCSS and ShadcnUI, a D1 database accessed through Drizzle ORM, and Google-based authentication through NextAuth, with deployment configured for Cloudflare Pages. You clone it, fill in your own Google OAuth credentials and D1 database, and start on product code instead of infrastructure.
Does this starter template need environment variables?
Runtime code does not, because it reads Cloudflare bindings through the environment object. Two setup tasks do. drizzle-kit needs three values from .env.example, namely CLOUDFLARE_D1_ACCOUNT_ID, DATABASE and CLOUDFLARE_D1_API_TOKEN, when you run migrations or the studio against the remote database. Google OAuth needs AUTH_SECRET, AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET in a .dev.vars file. The README's claim of no environment variables refers to the first category only.
How do I deploy it to Cloudflare?
Run bun run deploy, which builds the app with @cloudflare/next-on-pages and then calls wrangler pages deploy. You need Wrangler installed and authenticated with wrangler login beforehand. The README advises running bun run preview periodically as well, because that builds through the Pages adapter and checks the code in the same environment the deployed app will use.
Why does bun run db:migrate:prd fail?
Because that script does not exist. The README's Database Migrations section names db:migrate:prd, while package.json defines db:migrate:prod, db:migrate:dev, db:studio:dev and db:studio:prod. Use bun run db:migrate:prod for the remote database, or the manual section's direct form: bunx wrangler d1 execute ${dbName} --remote --file=migrations/0000_setup.sql.
Can I add file uploads with Cloudflare R2?
The README documents the bucket CORS policy you need, allowing GET and PUT, the Content-Type header, and exposing ETag so the client can read the uploaded object's version. What it does not include is an R2 client library in package.json, so check wrangler.toml for an R2 binding and expect to write the upload call yourself. Storage is the least wired of the primitives the project description advertises.