Click a link and the page is already there.
Not a spinner that resolves quickly. Not a skeleton that flashes for 200ms. The header, the sidebar, the page title, the table chrome: all of it arrives in the first response, and only the parts that genuinely need fresh data fill in behind it.
That is Makerkit v4. Every route in all three Next.js kits is rebuilt around Next.js 16.3 Cache Components, so your app paints on click instead of after a database query.
TL;DR
- See it before you read it: Modern Next.js runs this in your browser. Race the two routes, move a Suspense boundary, dial a cache profile, and watch what paints.
- What your users get: pages appear on click. Nothing jumps around as data arrives, because the layout was already on screen holding its shape.
- What you get: marketing pages that are safe on any CDN, blog and docs prerendered at build, and 69 of 72 routes prerendering something.
- What it costs: ten tagged steps, a few hours, no rewrite. Every tag is a working state you can merge and deploy on its own.
- Where it ships: v4 on the Next.js Supabase kit, v2 on the Next.js Drizzle and Prisma kits. Same work, different version numbers.
- Stack: Next.js 16.3, React 19, TypeScript 7, oxlint, oxfmt, Tailwind 4, Base UI.
Your app was slow for a reason that was never your fault
The pattern that makes a Next.js SaaS feel slow is the pattern the framework teaches you. Open the members page, the server runs a query, and until that query returns the browser has nothing to render. The header does not depend on that query. It waits for it anyway.
Here is the entire fix:
- async function Page({ params }) {- const members = await loadMembers(); // blocks the entire page- return <PageBody><PageHeader /><MembersTable members={members} /></PageBody>;- }+ function Page({ params }) {+ return (+ <PageBody>+ <PageHeader />+ <Suspense fallback={<TableSkeleton />}>+ <MembersTable params={params} /> {/* only this waits */}+ </Suspense>+ </PageBody>+ );+ }Look at the header. In the first version it cannot render until loadMembers() comes back, even though it never touches the result. In the second it renders immediately and the table arrives when it arrives.
This is easier to watch than to read about. We built an interactive walkthrough of the Next.js 16 rendering model that renders both of those routes side by side, so you can race them and then move the boundary yourself and see exactly what lands in the first response.
That is not a new idea. What is new is that Next.js 16 can now prerender the static part of that page at build time and stream the rest per request, in the same route, which was impossible when prerendering was all or nothing.
Applying it to one page is an afternoon. Applying it to every page in a SaaS kit, including auth, billing, admin, blog, docs, and a multi-tenant dashboard, is what v4 is.
The number that matters is zero
Makerkit v4 ships zero routes that opt out of instant navigation, which is both the claim worth checking and the one most starter kits cannot make.
Next.js gives you export const instant = false to mark a route as allowed to block. In your own app that is a reasonable escape hatch for the one page with a stubborn dependency. In a starter kit it is something else: every opt-out hands every customer a route that is quietly slow, forever, in a file they did not write and will not think to audit. The order of the ten migration steps exists specifically so the flag turns on with none.
The build tells the same story. Before the migration, one route in the Drizzle kit prerendered anything and 37 were fully dynamic. After it, 15 routes are fully static, 54 ship a prerendered shell with data streaming in, and the 3 that remain dynamic are API route handlers that only ever run per request. The Supabase kit lands in the same place from a different starting point. The full route breakdown is on the Modern Next.js page, split by what each of the 72 routes actually prerenders.
Your marketing pages are now safe on any CDN
Caching a Makerkit marketing page no longer risks leaking a signed-in user's data, because the server HTML no longer contains any.
In v3 those pages were force-dynamic. The layout awaited the session to decide whether to render "Sign in" or "Dashboard", so user data ended up in the response. A shared cache could then hand one visitor another visitor's header. The force-dynamic flag kept that out of caches and cost you the cache.
v4 moves that decision to the client. The server HTML is identical for everyone, so the page is safe on a CDN regardless of how the CDN is configured. That is a stronger guarantee than a flag, because a flag is a configuration you can get wrong and an empty response body cannot leak.
We verified it with a real session rather than a grep, because a grep that finds nothing proves nothing. The check needs two controls first: prove the session cookie actually works, meaning the authenticated route redirects without it and returns 200 with it, and prove the scan can detect a leak, meaning it finds your email on an authenticated page. Only then does a clean result mean anything. Ours came back byte-identical between anonymous and authenticated requests for the same URL.
Re-run that check whenever you touch your marketing layout. Putting a server-side session read back in is a one-line change with a very bad failure mode.
What you are actually buying: the sharp edges we already hit
We hit every one of these porting three kits, and most of them failed in ways that pointed us at the wrong file. Here they are as a checklist, and the shorter symptom-first version sits alongside the demos if you want to hand someone the list without the release notes around it.
next build stays green when the migration silently fails. A route that blocks on runtime data is valid, it just is not instant. The build passes. The only reliable signal is the dev overlay, per route, in a browser. We audited every route that way after the port and found ones a clean next build --debug-prerender never flagged.
Two insight classes surface differently, and one of them hides from you. Uncached data outside a boundary shows up on a direct page load. params or searchParams outside a boundary shows up only on a client-side navigation. A page that awaits searchParams at the top loads perfectly on a direct hit and reports nothing at all. Click a <Link> to it and the overlay flags it. Auditing by visiting URLs will not find these, so grep:
grep -rn "await searchParams\|await props.searchParams" \ apps/web/app packages/*/src --include="page.tsx"<Suspense> does not fix an unstable value. Date.now(), new Date(), and Math.random() fail differently from uncached data. Uncached data stops the prerender gracefully. An unstable value errors, because the same render would produce a different answer next time. The fix depends on what the value is for:
| The value is | Fix |
|---|---|
| Per-request | await connection() before you read it |
| Stable for a while | use cache with cacheLife |
| Browser-only | 'use client' |
| Telemetry | performance.now() |
Await connection() properly. It stalls a prerender by returning a promise that never resolves, so void connection() does nothing.
Never wrap a use cache function in React's cache(). They do not compose. The build fails with Invalid value used as weak map key, reported against generateMetadata rather than the loader that caused it, which sends you hunting in the wrong file. The wrapper is redundant anyway, since use cache dedupes across requests and not just within one.
Your Playwright suite will break, and the failure will lie to you. Cache Components enables React's <Activity>, so the route you navigate away from stays mounted and hidden instead of unmounting. Every data-testid matches twice after a navigation. page.locator() and getByTestId() are strict and tell you so. page.click() and page.fill() are not: they take the first match, usually the invisible stale one, then wait for it to become visible until the test times out. No error, no clue, two minutes gone. Scope selectors and containers with :visible.
<Activity> also keeps your dialogs open. A dialog whose submit action ends in redirect() used to be destroyed by the navigation, so nothing ever needed to close it. Now it reappears, backdrop and all, when the user returns to that route. Close it explicitly on navigation. Do not reach for a generic effect cleanup: cleanup also runs on ordinary unmount and on StrictMode's double invoke, so conditionally mounted dialogs unmount the instant they open. We shipped that, then reverted it.
A stale .next looks exactly like a code regression. Switching between tags without clearing it gave us /api/auth/* returning 404 across an entire suite. Nothing was wrong with the code. We lost an hour to that before anyone said the obvious thing out loud, so: rm -rf .next after any checkout.
Why Makerkit is the most modern Next.js boilerplate you can buy
Most starter kits ship the framework's default rendering model and stop. Makerkit v4 ships the model Next.js 16 was designed around, applied to every route, validated with the opt-out count at zero.
Cache Components, Partial Prerendering, next/root-params, and Partial Prefetching are all on, and every route passes validation with them on. "Built with Next.js 16" tells you what is in someone's package.json. This tells you what the routes actually do when a user clicks something.
The toolchain underneath is native. Linting runs on oxlint and formatting on oxfmt, both from the Oxc toolchain, and type checking runs on TypeScript 7, the Go-ported native compiler. pnpm run healthcheck chains all three into one command. A fast deterministic feedback loop matters most when an AI agent is verifying its own work before handing it back to you.
The other thing we build for now is coding agents. Fourteen AGENTS.md files ship in the repo, one per package, with CLAUDE.md and GEMINI.md pointing at the same source so Claude Code, Cursor, Windsurf, and Gemini CLI all read one set of rules. A bundled MCP server exposes Makerkit-specific tools to your agent. The v4 rules include the Cache Components non-negotiables, so an agent working in your codebase knows not to await searchParams at the top of a page before it writes the line.
The upgrade itself is ten tags, each a working state. Your app builds, runs, and deploys after every one. Merge step 4, ship it, come back to step 5 next week. The migration guide names what breaks along the way: the workspace hooks that now suspend, the auth gating that moved into the proxy, the tests that will fail and why the failure is misleading. That guide ships inside the repo as well as on this site.
Upgrading is ten steps, and you can stop after any of them
Each step below is one tag you can merge, verify, and deploy on its own.
- Next.js 16.3. The dependency floor for everything below.
app/[locale]/layout.tsxbecomes the real root layout. v3 shipped a pass-throughapp/layout.tsxwhose only job was to giveapp/not-found.tsxa layout. That pass-through counted as the root layout, so[locale]sat one level below it and never qualified as a root param.- The locale resolves via
next/root-params. next-intl'srequestLocaleis a lazy getter that callsheaders()the moment it is read. That single read is what made every route request-bound in v3. - Loading states scale to the route. Five
loading.tsxfiles all rendered the same full-page spinner. Each now gets treatment proportional to how predictable its shape is, with shared blocks that step 6 reuses as Suspense fallbacks. - Marketing auth moves to the client. This is the CDN change above.
<Suspense>boundaries across the app. Layouts pass promises down unawaited and render their shell andchildrenimmediately. This is where most of the work lands.- Cache Components on. Zero opt-outs.
- Blog, changelog, and documentation prerender at build.
generateStaticParamsplususe cache, in one pass. - Partial Prefetching on. Every
<Link>loads the shared App Shell for its destination. - A visual refresh. Purely cosmetic and entirely optional.
Next.js 16.3 is a hard floor, and skipping it fails quietly. next typegen on an older version does not error, it emits no root params file at all, so you get a missing type instead of a message telling you why. If step 3 looks broken, check your resolved Next version before you touch anything else.
Quick Recommendation
Makerkit v4 is best for:
- Teams shipping a Next.js SaaS where users spend all day clicking between routes
- Anyone starting a new Next.js 16 project who wants Cache Components applied correctly from day one
- Existing customers on v3 who can spend a few hours merging ten tagged steps
Skip the upgrade for now if:
- You are pinned below Next.js 16.3 by another dependency and cannot move yet
- You are mid-launch and have no time to run your e2e suite against
<Activity>behavior - You have heavily customized the app directory and cannot spend the time on steps 6 and 7
Our pick: upgrade incrementally. Merge steps 1 through 6, which improve your app under the old rendering model on their own, then take 7 through 9 when you have time to walk every route in the dev overlay.
Which kit, which version
The same work ships across three Next.js kits under two version numbers, because the kits were on different majors when it landed.
| Kit | Version | Branch | Tags |
|---|---|---|---|
| Next.js Supabase | v4 | v4 | v4-step/* |
| Next.js Drizzle | v2 | v2 | v2-step/* |
| Next.js Prisma | v2 | v2 | v2-step/* |
Drizzle and Prisma are architecturally identical here and differ only in packages/database. The Supabase kit is a deeper variant, with Supabase Auth and RLS-enforced authorization, so a few steps differ in detail. Each kit ships its own migration guide with its own tag names and version numbers.
Frequently Asked Questions
What are Next.js Cache Components?
Do I have to upgrade to v4 all at once?
Does v4 require Next.js 16.3?
Will the upgrade break my Playwright tests?
Is `export const instant = false` acceptable?
Which Makerkit kits include this work?
Next steps
If you want the rendering model itself rather than the release, start with Modern Next.js: Suspense, Partial Prerendering, and Cache Components explained one at a time, each with a demo you can drive.
Read the v4 migration guide for the step by step upgrade, including what to check after each tag. New to the kit? Start with the Next.js Supabase installation docs, or the Drizzle and Prisma variants. For how we pick the stacks we ship, see the best Next.js SaaS boilerplate.