
Last verified: 26 August 2026 against the lockfile of the repo that serves this page.
Most "best stack" posts are a reading list. Someone skims a few docs sites, likes the landing pages, arranges the logos into a diagram, and publishes it as a recommendation. You cannot tell from the outside whether any of it survived contact with a deploy.
This one is a lockfile. Every version below is what this site and the product behind it are running today. Where a choice has cost me something, I say what it cost, and there is a whole section near the end on the parts I would still call wrong.
Disclosure first
I sell a Next.js boilerplate that ships this exact stack, so I have an obvious interest in you thinking it is good. Two things I can do about that: give you the version numbers, so every claim is checkable against public registries instead of my opinion, and spend real words on where the stack loses. Both are below.
The whole stack, one table
| Layer | Pick | Version | The main alternative I passed on |
|---|---|---|---|
| Framework | Next.js (App Router) | 16.2.11 | TanStack Start, Remix |
| UI runtime | React | 19.2.8 | none, this is not a real choice |
| Language | TypeScript | 5.9.3 | none |
| Host | Cloudflare Workers via OpenNext | 1.20.2 | Vercel |
| Database | Postgres on Neon | serverless driver 1.1 | Supabase, PlanetScale |
| ORM | Prisma with driver adapters | 7.9 | Drizzle |
| Auth | Better Auth | 1.6.25 | Clerk, Auth.js |
| Payments, in-app | Stripe | 22.3.2 | Paddle, Lemon Squeezy |
| Payments, selling the kit | Polar | SDK 0.49 | Gumroad, Lemon Squeezy |
| Styling | Tailwind CSS | 4.3.3 | CSS modules, Panda |
| Components | shadcn/ui on Radix | radix-ui 1.4.3 | MUI, Mantine, Chakra |
| Forms | React Hook Form + Zod | 7.83 + 4.4.3 | TanStack Form |
| URL state | nuqs | 2.9 | hand-rolled searchParams |
| Tables | TanStack Table, manual mode | 8.21 | AG Grid |
| Resend + React Email | 6.18 + 6.9 | Postmark, SendGrid | |
| Lint and format | Biome via ultracite | 2.5.5 | ESLint + Prettier |
| Monorepo | pnpm workspaces + Turborepo | 10.19 + 2.10 | Nx, npm workspaces |
| Tests | Vitest + Playwright | 4.1 + 1.62 | Jest + Cypress |
| Docs and blog | Fumadocs | 16.14 | Contentlayer, Astro |
| Analytics | PostHog | posthog-js 1.418 | Plausible, GA only |
If you want the short version: boring database, unfashionable ORM, self-hosted auth, two payment providers doing two jobs, and a build pipeline that runs on someone else's edge. The rest of this post is why.
The seven decisions that were actually hard
Most of that table picked itself. These seven did not.
Host: Cloudflare Workers, and the edge is not the reason
The pitch for Workers is that your code runs in hundreds of locations, close to every user. That pitch is the part of this stack I trust least, so let me argue against it before I defend the choice.
Edge compute is not a speed feature. It is a decision about where your code sits relative to your data, and it only pays off when your code does not need data that lives somewhere else. Put a Worker in Sydney and a Postgres in Frankfurt, and every query is a round trip of roughly 250ms. A dashboard that checks a session, loads the organization, then loads the rows pays three of those before it renders anything. Run the identical request from a server sitting next to the database, and the user pays one round trip for the response while each query costs about a millisecond. For almost any app with a real database behind it, close to the user is the wrong place to be.
Cloudflare agrees, which is the strongest evidence I can give you. Smart Placement exists specifically to move your Worker away from the user and next to your backend, and Cloudflare's own documentation reaches for a Sydney user and a Frankfurt database to explain why, reporting a 4x to 8x improvement in that example. Note what that means: their fix for the edge is a switch that turns the edge off. It is also opt-in, so the default remains the version that is wrong for database-backed apps.
I have paid for this personally, on this domain. Until 24 August 2026, this site's Next.js incremental cache lived in an R2 bucket, and the cross-region read cost roughly a second of TTFB on every request, all to support revalidation that this app never performs. Compute at the edge, data not at the edge. Moving that cache into the static asset binding deleted the problem outright. The 1.8-second TTFB I chased earlier this month was the same family of mistake wearing a different hat.
So if geography is not the reason, what is? Three things that survive scrutiny.
Cold starts effectively do not exist, because Workers are V8 isolates rather than containers. There is no provisioning step to design around and no keep-warm cron pretending to be architecture.
The price curve is better at my scale, and it keeps being better as traffic grows rather than turning on me.
The Worker lives in the same system as the DNS, CDN, and WAF I already run, so the front door is one thing to reason about instead of three.
Not one of those is about being near the user. Choose Workers for cold starts, price, and consolidation. If you choose them because "the edge is faster," you will ship a slower dashboard than a boring single-region server would have given you.
The costs are real too. workerd is not Node, so a dependency that quietly needs a Node built-in fails at runtime instead of at build time, which means preview catches it and CI does not. And a Worker response is not edge-cached the way a static asset is: your HTML runs through the Worker on every request until you write a Cache Rule that says otherwise.
Pick Vercel if you would rather own none of that. It is still the path of least resistance for Next.js, every feature lands there first, and nothing in your build fights you. The honest summary is that this row of the table is the one most likely to be wrong for you, and it is the only row where I would call the alternative equally defensible.
ORM: Prisma, in the year everyone told me to use Drizzle
Drizzle is smaller, closer to SQL, and has better types in a few genuinely annoying corners. The case for it is real and I am not going to strawman it.
I stayed on Prisma 7 for two reasons. The schema file is the single best artifact I have for onboarding another person or an AI agent to a codebase, because it is the whole data model in one readable place. And driver adapters closed the gap that actually mattered: @prisma/adapter-neon over the serverless driver means no query engine binary to ship into a Worker, which was the original reason to avoid Prisma in this exact deployment.
Where Prisma still loses: complex analytical queries turn into raw SQL sooner than they would in Drizzle, and generated client size is something you keep an eye on. That has been an acceptable trade for me. It might not be for you if your product is query-heavy from day one.
Auth: Better Auth, because the tables should be mine
Clerk is a better product than what I run. Better UI, better support, less of my time spent on it. I am not pretending otherwise.
I use Better Auth because in a multi-tenant B2B app, auth is not a widget you drop in. It is half your data model. Users, organizations, memberships, invitations, and roles all have foreign keys into the rest of the schema. When those tables live in someone else's database, every feature that joins a user to their own data costs a network hop and a sync problem. I wrote about why organizations belong in your schema from the first commit, and this is the same argument one layer down.
Better Auth gives me the organization plugin, 2FA, and a Stripe plugin that keeps subscriptions attached to the org rather than the person, all in my Postgres. The bill is that I own the upgrade path and the edge cases. When a session bug appears at 2am, nobody is on call for me.
Payments: the question is not Stripe, it is who is legally selling
There are two payment providers in that table. That is not an abstraction and it is not indecision. It is two different jobs, and choosing between them has almost nothing to do with APIs.
A payment processor moves money and leaves you as the seller, which means you own the tax. For digital goods, that is a worse deal than most people expect. EU VAT on a cross-border sale to a consumer is owed in the buyer's country, and the €10,000 threshold that lets small sellers ignore this only applies if you are established inside the EU. Sell from outside it and you have to register under the non-Union scheme from your first sale. On top of that, US sales tax on digital goods varies state by state. Stripe Tax will calculate every cent of that correctly, and calculating is not remitting. Registering, filing, and remitting stay yours.
A merchant of record becomes the legal seller instead of you. Polar, Paddle, Lemon Squeezy, Gumroad, and FastSpring all do this. They owe the tax, they file it, and you receive one payout. The higher fee is not buying a nicer checkout. It is buying a transfer of liability.
That is why this site sells the kit through Polar. One product, one-time payment, buyers in dozens of countries, one person operating the whole thing. Polar's posted rate starts at 5% plus 50 cents and falls on paid tiers, so call it about two extra points against raw card processing. Two points is cheaper than an accountant and far cheaper than my own evenings spent on VAT returns. I chose Polar over the other four because its pricing is published rather than sales-gated behind a call, and because it runs on Stripe underneath, so the checkout is Stripe quality while the liability is not mine.
The kit itself bills with Stripe, which is a different situation, not an inconsistency. Its buyers are building B2B SaaS where the customer is a company, billing is recurring and usually per seat, and reverse-charge rules hand much of the VAT problem to the buyer. Stripe earns that slot on invoicing, proration, and dunning, and Better Auth's Stripe plugin keeps the subscription attached to the organization rather than to a person, which is the whole game in a multi-tenant app.
So no, it is not always Stripe. Selling a one-time product to a worldwide consumer audience, like a template, a course, or an app? Use a merchant of record and stop thinking about it. Billing companies on recurring contracts with invoices and purchase orders? Stripe, or something heavier as you grow. Genuinely torn? Start with the merchant of record, because migrating off one later is a normal week of work, while backdating three years of unfiled VAT is not.
The thing I would not do is put an abstraction over both on day one. Two providers doing two unrelated jobs is not the same as a provider-agnostic payments layer, and that layer costs you maintenance forever to serve a switch you make exactly once.
UI: shadcn/ui, which is not a dependency
MUI and Mantine give you more, faster. They also give you a theming system you have to learn, and a ceiling you eventually hit at exactly the moment a client asks for something the library did not anticipate.
shadcn/ui is not a package. It is a set of components copied into your repo on top of Radix primitives. That means no version to upgrade, no theme API between you and the markup, and every component editable the same way your own code is. Tailwind v4 on top, with OKLCH tokens in one file.
The honest cost: you own accessibility regressions when you edit a primitive's markup, and "just copy it in" means nobody is shipping you fixes. Radix carries the hard parts: focus management and dismissable layers, which is why this trade works at all.
Client state: none, on purpose
There is no Zustand here, no Redux, and no TanStack Query. That surprises people, so it is worth being explicit about what replaced them.
Server Components fetch data, so most of what a query cache existed to do is now a fetch in a component that renders on the server. Mutations are server actions. Filter and pagination state lives in the URL through nuqs, which means it is shareable, back-button correct, and survives a refresh for free. Form state is React Hook Form, scoped to the form.
What is left over after those four is genuinely small, and small enough that useState handles it. If you find yourself reaching for a global store in a Next.js 16 app, it is worth checking first whether the state wants to be in the URL.
Lint: Biome, and I am not going back
ESLint plus Prettier plus the plugins plus the config file that reconciles them is a stack of its own. Biome is one binary that does both, fast enough that it runs on save without you noticing.
I run it through ultracite, which is a preset on top of Biome with React and Next rules already reconciled. The migration cost was an afternoon. The thing I gave up is the long tail of niche ESLint plugins, and I have not missed one yet.
The boring picks, one line each
Postgres because every hosting question and every hiring question has a known answer. Neon because branching a database per preview deploy is worth more than it sounds. Zod 4 for validation, shared between the form resolver and the server action so one schema guards both. Resend for delivery with React Email for the templates, and MailDev locally so nothing test-related ever leaves the machine. Pino for structured logs, split into server and client entry points. Vitest for units and Playwright for the flows that involve a real browser and a real session. pnpm and Turborepo because a monorepo without a build graph is just a folder. Fumadocs for docs and this blog, because MDX with a typed schema catches a broken post at build time instead of in production.
Where this stack is wrong
Three things I would fix before I would recommend anyone copy this blindly.
The CSS payload. Tailwind v4 emits a large stylesheet and mine is around 169 KiB before compression. I went looking for clever fixes and found mostly dead ends. Next.js inlineCss duplicates the whole sheet into the document several times over, which is worse, not better. Turbopack ignores browserslist, so targeting modern browsers does not shrink the output the way you would expect. The real answer is deleting unused component variants, which is work, not configuration.
Smart Placement is off, and the Cache Rule is not written. Having spent a whole section arguing that edge placement is the wrong default for database-backed apps, I should admit I have not enabled Smart Placement on this Worker. The excuse is decent, since this marketing site has no database and no auth, so there is no backend for the Worker to be placed near. The excuse does not cover the kit's demo app, which does talk to Postgres and should have Smart Placement on. The Cache Rule is the same story: still on the list, still not shipped, and anyone taking the OpenNext path should treat Cache Rules and purge-on-deploy as part of the stack rather than as an optimization for later.
No MCP server. The kit ships agent rules and skills, and agents work well against it. It does not ship an MCP server, and some competitors do. I would rather say that plainly than let a feature table imply otherwise.
How to pick if you are not me
The stack above is tuned for one situation: a small team or a solo developer building multi-tenant B2B SaaS, where organizations and billing are core rather than bolted on later. Change that situation and several rows should change with it.
Building a single-user B2C product? Drop the org modeling, and Clerk starts to look correct because auth stops being half your schema.
Query-heavy analytics product? Look harder at Drizzle, because you will be writing SQL either way and Prisma's ergonomics stop paying rent.
Team of fifteen with a designer? Buy a component library. shadcn/ui's advantage is control, and control is worth less when someone else owns the design system.
Selling one-time products to consumers worldwide rather than billing companies? Swap Stripe for a merchant of record inside the app too, and budget real days for it, because subscription logic and licensing logic are not the same shape.
Shipping to a deadline with no infrastructure opinions? Vercel, Clerk, and whichever payment provider matches the buyer, then do not read another stack post until you have customers.
The only recommendation here I would defend for everybody is the shape rather than the parts: pick the thing you can read the source of, and pay for the thing you cannot afford to debug.
If you want this stack without assembling it
Everything above is what SaaSProKit ships, wired together and tested, with the multi-tenant core already built. That is the product, and it is $200 flat.
If you would rather have this stack built into your product than assemble it yourself, that is what the two week MVP is.