cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Adapters and other platforms

See what the Build Adapters API hands a platform after next build, and how Vercel, Netlify, Cloudflare and AWS run Next.js 16, checked against their docs.

By the end, you can
  • 01
    Read an adapterName the adapter's two hooks, when Next.js calls them, and what adapterPath and NEXT_ADAPTER_PATH do.
  • 02
    Read what it receivesTell the build's outputs apart: functions, prerenders with their revalidate and postponed state, static files, proxy.ts and the routing rules.
  • 03
    Choose a platform honestlyCompare how Vercel, Netlify, Cloudflare and AWS run Next.js 16 today, and check a platform against the features your app uses.

One build, handed to a platform

Deploying Next.js named the fourth way to run an app: a platform, a hosting company that runs it on its own computers. A platform usually splits the app: code that runs when a request needs it, files on its CDN, and rules that send each request to the right one. The code that does that split is the platform's adapter, and Next.js has a public API for writing one.

Here is the tea shop with a tiny adapter of its own, my-adapter.js. It doesn't deploy anything: it prints what Next.js hands it. This example is a real Next.js 16.4.3 project, built with next build and started with next start. The adapter's file is open; click the others in the tree.

The tea shop with an adapter that prints what it receivesNext.js
app/api/prices/cart/menu/teas/[slug]/
my-adapter.js
/** @type {import("next").NextAdapter} */const adapter = {  name: "tea-shop-adapter",   modifyConfig(config, { phase }) {    console.log(`tea-shop-adapter: modifyConfig in ${phase}`);    return config;  },   onBuildComplete({ outputs, routing }) {    const paths = (list) => list.map((output) => output.pathname).filter((path) => !path.endsWith(".rsc")).sort();    const proxy = outputs.middleware;    const pages = outputs.prerenders.filter((output) => !/\.rsc$|\.segments\//.test(output.pathname));    const htmlFiles = outputs.staticFiles.filter((file) => file.filePath.endsWith(".html"));    console.log("tea-shop-adapter: onBuildComplete");    console.log("  appPages:", paths(outputs.appPages).join(", ") || "none");    console.log("  appRoutes:", paths(outputs.appRoutes).join(", ") || "none");    console.log("  middleware:", proxy ? `${proxy.runtime}, for ${proxy.config.matchers.map((m) => m.source)}` : "none");    for (const page of pages) {      const file = !page.fallback?.filePath ? "no file" : page.fallback.postponedState ? "shell + postponed state" : "whole file";      console.log(`  prerender ${page.pathname}: ${file}, revalidate ${page.fallback?.initialRevalidate ?? "none"}`);    }    console.log("  staticFiles:", outputs.staticFiles.length, "files, HTML:", paths(htmlFiles).join(", ") || "none");    console.log("  dynamicRoutes:", routing.dynamicRoutes.map((route) => route.source).join(", ") || "none");  },}; module.exports = adapter;
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)  Applying modifyConfig from tea-shop-adaptertea-shop-adapter: modifyConfig in phase-production-build- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (9/9)  Finalizing page optimization ...  Running onBuildComplete from tea-shop-adaptertea-shop-adapter: onBuildComplete  appPages: /, /_global-error, /_not-found, /cart, /menu, /teas/[slug]  appRoutes: /api/prices  middleware: nodejs, for /teas  prerender /: whole file, revalidate false  prerender /_global-error: whole file, revalidate false  prerender /api/prices: whole file, revalidate false  prerender /cart: shell + postponed state, revalidate false  prerender /menu: whole file, revalidate 3600  prerender /teas/green: whole file, revalidate false  prerender /teas/mint: whole file, revalidate false  prerender /teas/[slug]: no file, revalidate none  staticFiles: 13 files, HTML: /404, /500  dynamicRoutes: /teas/[slug] Route (app)           Revalidate  Expire┌ ○ /├ ○ /_not-found├ ○ /api/prices├ ◐ /cart├ ○ /menu                     1h      1d└   /teas/[slug]  ├ ◐ /teas/[slug]  ├ ○ /teas/mint  └ ○ /teas/green ƒ Proxy (Middleware) ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ Ready  Applying modifyConfig from tea-shop-adaptertea-shop-adapter: modifyConfig in phase-production-server

Open the Terminal tab and read it from the top. Next.js prints Applying modifyConfig from tea-shop-adapter before it builds anything, and the adapter answers with modifyConfig in phase-production-build. Near the end, Running onBuildComplete from tea-shop-adapter, and the adapter lists what it received, just before the route table. Under $ next start, the adapter speaks once more: modifyConfig in phase-production-server.

Two rows. Top, next build: it loads next.config, which calls modifyConfig in phase-production-build; then it compiles and prerenders; then it calls onBuildComplete with the outputs and routing, and the adapter writes the platform's files. Bottom, next start: it loads next.config, which calls modifyConfig again, in phase-production-server.next buildload configmodifyConfigcompileprerenderonBuildCompletethe platform's filesoutputs and routingnext startload configmodifyConfigphase-production-server
modifyConfig runs whenever a command loads next.config, and learns which phase it's in. onBuildComplete runs once, at the end of next build, with everything the build made. That's where a platform's adapter writes what its computers run.
Real-life analogyOne packing list, many delivery companies

A tea shop packs its goods once and writes one detailed packing list: what's fresh, what keeps, what must be brewed when it arrives. Every delivery company gets the same list. Each one loads its own vans its own way, and some can't carry everything on the list.

In real life: The shop packs its goods once
In Next.js: next build
In real life: The detailed packing list
In Next.js: outputs and routing, handed to onBuildComplete
In real life: A delivery company's loader
In Next.js: A platform's adapter
In real life: Fridge vans, shelf vans, a van with a kitchen
In Next.js: CDN files, functions, edge functions
In real life: What a company can carry
In Next.js: Which Next.js features a platform supports
The main idea

An adapter is a platform's code inside your build. It may change the config whenever a Next.js command loads it, and it receives everything next build made when the build ends. It turns that into the platform's own functions, files and routing rules. What works on a platform depends on what its adapter, and its computers, do with each part.

The Build Adapters API: adapterPath and two hooks

Next.js's team proposed the API in April 2025, in an RFC (a public "request for comments") called "Deployment Adapters API", discussion 77740 in Next.js's GitHub repository. It shipped as experimental.adapterPath, and Next.js 16.2.0 made it stable as the top-level adapterPath, which 16.4.3 uses. This is the shop's config:

next.config.ts: naming the adapterTypeScript
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  cacheComponents: true,  partialPrefetching: true,  adapterPath: require.resolve("./my-adapter.js"),}; export default nextConfig; 

Lines 4 and 5 are what create-next-app writes. Line 6 names the adapter's file: require.resolve turns ./my-adapter.js into the full path Next.js loads it from. A platform doesn't ask you to edit your config. It sets an environment variable in its build instead:

What a platform's build can run insteadShell
NEXT_ADAPTER_PATH=/opt/platform/adapter.js npm run build

With NEXT_ADAPTER_PATH set, next build loads that adapter as if line 6 named it. Now the adapter itself, open in the example above. Its lines, in order:

  1. Line 1 gives the object the NextAdapter type that next exports, so an editor knows its shape.
  2. Line 3 is its name, which Next.js prints: Running onBuildComplete from tea-shop-adapter.
  3. Lines 5 to 8 are modifyConfig. Next.js calls it with the whole config and the phase, a name for what's running (phase-production-build for next build), and uses the config it returns. This one only prints the phase and changes nothing. A real adapter changes what its platform needs: Vercel's, for example, sets a deploymentId when Vercel's skew protection is on.
  4. Lines 10 to 25 are onBuildComplete, called once, after the build, with outputs and routing, among other things: the project's folder, the final config, the Next.js version and the build's id.
  5. Line 11 lists the outputs' addresses, leaving out those ending in .rsc: each page also has an output for its RSC payload, the address the router asks for. Line 13 keeps the prerenders of whole pages, without their payload pieces.
  6. Lines 15 to 24 print the parts, and line 28 exports the adapter, the way the docs' example does.

Next.js has a compatibility test suite for adapters, which the docs' Testing Adapters page explains, and the Next.js team coordinates testing with the verified adapters' platforms before major releases. The types live in Next.js's build/adapter/build-complete.ts.

What onBuildComplete receives

The interesting part is outputs. Each button below builds a version of the shop with the same adapter. Before you click, guess which lines of the printout will change.

Playground: what does the adapter receive?
The tea shop with its adapter, in this versionNext.js
app/api/prices/cart/menu/teas/[slug]/
my-adapter.js
/** @type {import("next").NextAdapter} */const adapter = {  name: "tea-shop-adapter",   modifyConfig(config, { phase }) {    console.log(`tea-shop-adapter: modifyConfig in ${phase}`);    return config;  },   onBuildComplete({ outputs, routing }) {    const paths = (list) => list.map((output) => output.pathname).filter((path) => !path.endsWith(".rsc")).sort();    const proxy = outputs.middleware;    const pages = outputs.prerenders.filter((output) => !/\.rsc$|\.segments\//.test(output.pathname));    const htmlFiles = outputs.staticFiles.filter((file) => file.filePath.endsWith(".html"));    console.log("tea-shop-adapter: onBuildComplete");    console.log("  appPages:", paths(outputs.appPages).join(", ") || "none");    console.log("  appRoutes:", paths(outputs.appRoutes).join(", ") || "none");    console.log("  middleware:", proxy ? `${proxy.runtime}, for ${proxy.config.matchers.map((m) => m.source)}` : "none");    for (const page of pages) {      const file = !page.fallback?.filePath ? "no file" : page.fallback.postponedState ? "shell + postponed state" : "whole file";      console.log(`  prerender ${page.pathname}: ${file}, revalidate ${page.fallback?.initialRevalidate ?? "none"}`);    }    console.log("  staticFiles:", outputs.staticFiles.length, "files, HTML:", paths(htmlFiles).join(", ") || "none");    console.log("  dynamicRoutes:", routing.dynamicRoutes.map((route) => route.source).join(", ") || "none");  },}; module.exports = adapter;
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)  Applying modifyConfig from tea-shop-adaptertea-shop-adapter: modifyConfig in phase-production-build- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (9/9)  Finalizing page optimization ...  Running onBuildComplete from tea-shop-adaptertea-shop-adapter: onBuildComplete  appPages: /, /_global-error, /_not-found, /cart, /menu, /teas/[slug]  appRoutes: /api/prices  middleware: nodejs, for /teas  prerender /: whole file, revalidate false  prerender /_global-error: whole file, revalidate false  prerender /api/prices: whole file, revalidate false  prerender /cart: shell + postponed state, revalidate false  prerender /menu: whole file, revalidate 3600  prerender /teas/green: whole file, revalidate false  prerender /teas/mint: whole file, revalidate false  prerender /teas/[slug]: no file, revalidate none  staticFiles: 13 files, HTML: /404, /500  dynamicRoutes: /teas/[slug] Route (app)           Revalidate  Expire┌ ○ /├ ○ /_not-found├ ○ /api/prices├ ◐ /cart├ ○ /menu                     1h      1d└   /teas/[slug]  ├ ◐ /teas/[slug]  ├ ○ /teas/mint  └ ○ /teas/green ƒ Proxy (Middleware) ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ Ready  Applying modifyConfig from tea-shop-adaptertea-shop-adapter: modifyConfig in phase-production-server
Try it yourself
The whole shop

Every kind of output: pages and a Route Handler to run, proxy.ts on Node.js, prerenders (the cart as a shell with postponed state), static files and a dynamic route.

Every version was built with next build from Next.js 16.4.3, with the same my-adapter.js. The Terminal shows what the adapter printed, just before the route table. Reset goes back to the first version.

Each row is one part of what the adapter received, and what the whole shop's recording printed for it:

The parts of onBuildComplete's outputs
PartWhat it holdsIn the shop's recording
appPagesThe server code for each page, which renders it when a request needs it./, /cart, /menu, /teas/[slug], and Next.js's own /_not-found and /_global-error.
appRoutesThe code for each Route Handler./api/prices.
middlewareproxy.ts, with its runtime and the paths it runs for.nodejs, for /teas.
prerendersWhat the build made ahead of time, and when it may be made again./menu with revalidate 3600; /cart as a shell with postponed state.
staticFilesFiles that can be sent as they are, by any CDN.13 files: the build's JavaScript and CSS, and /404 and /500 as HTML.
routingThe rules that send each request to the right output: redirects, rewrites, dynamic routes, headers.dynamicRoutes: /teas/[slug].
The tea shop's build output, in the groups onBuildComplete receives. appPages: slash, slash cart, slash menu and slash teas slash slug. appRoutes: slash api slash prices. middleware: proxy.ts, on Node.js. prerenders: slash menu with revalidate 3600, slash cart as a shell, slash teas slash mint. Below, staticFiles: 13 files, sent as they are.appPages//cart/menu/teas/[slug]appRoutes/api/pricesmiddlewareproxy.ts, nodejsprerenders/menu: 3600/cart: shell/teas/mintstaticFiles13 files: _next/static, /404, /500
The same shop, as an adapter sees it: code to run (appPages, appRoutes, middleware), pages made ahead of time (prerenders), and files to send as they are (staticFiles). Each platform decides where each group goes.

Three details in the printout matter for a platform:

  • Revalidate. /menu came with revalidate 3600: its cacheLife("hours") lets it be made again after an hour (Incremental Static Regeneration). The other pages say false: never. A platform needs this number to know when its copy is old.
  • Postponed state. /cart reads a cookie inside Suspense, so its prerender is a shell plus postponed state: what React saved to finish the page later (Partial Prerendering). A platform can send the shell from its CDN at once and ask its function for the rest. Deploying Next.js called that resuming.
  • Runtime. proxy.ts arrived as middleware: nodejs. Since Next.js 16, it always runs on Node.js (Proxy), so a platform must run Node.js code before its routes, not only edge code.

The static export got none of the code: appPages: none, and 32 static files, the HTML pages among them (Static exports). Notice /index in that list: the export's index.html, named by its file. An adapter that serves it has to know that /index means /.

Vercel: an adapter on the public API

Vercel maintains Next.js, and its adapter uses the same public API as every other adapter: the Next.js docs say there are no private hooks. Its adapter is open source, in the nextjs/adapter-vercel repository, and published as @next-community/adapter-vercel. On October 30, 2026, npm had it only under the beta tag, at 0.0.1-beta.35. You don't install it: Vercel sets it for you when it builds the app.

Its onBuildComplete writes the folder Vercel deploys from, which Vercel calls its Build Output API:

  • static/: the static files and the prerendered pages, for Vercel's CDN.
  • functions/: one .func folder for each page or Route Handler that needs code, run as Vercel Functions.
  • config.json: the routing rules, made from routing and the outputs.
One next build, four platforms. Vercel runs the server code in Vercel Functions and keeps files on its CDN. Netlify runs it in a Netlify Function, files on its CDN. Cloudflare with OpenNext runs it in a Worker, files in Workers Static Assets. AWS with OpenNext runs it in Lambda functions, files on a CDN.next buildplatformserver code runs infiles come fromVercelVercel Functionsits CDNNetlifya Netlify Functionits CDNCloudflare + OpenNexta WorkerStatic AssetsAWS + OpenNextLambda functionsa CDN
The same build, four homes. Each platform runs the code that answers requests in its own kind of function, and sends files from its CDN. What differs is which features work there, and how fast: read each platform's own table. Cloudflare's other path, vinext, builds with Vite and never runs next build, so it isn't here.

Vercel's Next.js page describes how the features land there. A ◐ route's static shell is served from the CDN, while a Vercel Function renders the rest and streams it in the same response. proxy.ts becomes Vercel's Routing Middleware, deployed to every region. Images are resized by Vercel's image service. Next.js's Deploying page lists Vercel's adapter as verified, beside Bun's, which builds a folder that runs the app with the Bun runtime. Verified means public code and Next.js's compatibility tests; the page says public test results for each adapter are coming soon.

Netlify and Cloudflare

The Next.js docs list Netlify and Cloudflare under "other platforms": their integrations aren't built on the public API yet, and both are working on verified adapters that are. Everything below is from their own docs and changelogs, as of October 30, 2026.

Netlify adds its adapter to every Next.js build: @netlify/plugin-nextjs, at 5.16.5. It's part of OpenNext, an open-source project that turns next build's output into packages for other platforms. Netlify's Next.js page says the adapter:

  • puts the pages, Route Handlers and Server Actions in one Netlify Function;
  • puts proxy.ts in a Netlify Edge Function;
  • keeps prerendered pages in Netlify's cache, revalidated by path or by tag;
  • sends next/image through Netlify Image CDN.

The feature table on that page marks Cache Components, streaming and after() as fully supported. Its list of limits names a few: a Node.js proxy.ts can't use the file system or C++ addons, and headers and redirects from the config run after proxy.ts, not before as in next start.

Cloudflare runs code in Workers: small programs that start on its servers worldwide. Its Next.js guide gives two ways:

  1. vinext (1.4.0), the one the guide recommends. It's a Vite plugin that reimplements Next.js's API: your app folder stays, but Next.js doesn't build it. The guide calls it beta and points to its compatibility list.
  2. The OpenNext adapter, @opennextjs/cloudflare (1.20.14), for apps that can't move yet (its features page). It builds with Next.js, then reshapes the output into one Worker, with the files in Workers Static Assets. Revalidation needs Cloudflare storage: a cache store (R2, the docs advise), a queue, and a tag cache.

One detail shows why you check dates. OpenNext's changelog added proxy.ts support in 1.20.3 (August 26, 2026), marked experimental and needing the nodejs_compat flag, while its features page still listed Node.js middleware as not supported on October 30, 2026. A Worker also has a size limit: 3 MiB compressed on the free plan, 10 MiB on the paid one.

AWS: OpenNext and Amplify

AWS has no Next.js adapter of its own on the public API. There are two common ways onto it:

  • OpenNext for AWS, @opennextjs/aws (4.1.13), kept up by people from the SST community. It builds the app in standalone mode (Self-hosting) and wraps the server in an AWS Lambda function (its architecture page). It also makes a function for image optimization, one for revalidation with a queue in front, and a folder of assets, .open-next/assets, for a CDN. It doesn't create anything in your AWS account: SST, AWS CDK or Terraform does that. Its npm package accepts Next.js 16.4.1 and later in the 16 line, and its changelog lists each release's Next.js 16 fixes.
  • AWS Amplify Hosting, AWS's managed service. Its "Amplify support for Next.js" page, on October 30, 2026, supports Next.js up through 15, and lists Next.js streaming, on-demand ISR and after() as not supported. For a Next.js 16 app with Cache Components, that page doesn't say yes.

Choosing a platform

No platform is right for every app. Start from what your app does, then read each platform's own page. Here are the four, side by side, as their docs and changelogs described them on October 30, 2026:

Four platforms, as of the date above
PlatformHow Next.js plugs inWhere server code runsWorth knowing
VercelIts adapter, @next-community/adapter-vercel, built on the public API and set for you when you deploy.Vercel Functions; files and prerendered pages on its CDN.A verified adapter. It writes Vercel's deploy folder: static files, one .func folder per function, and a config.json of routes.
NetlifyIts OpenNext adapter, @netlify/plugin-nextjs, added to every Next.js build. Not built on the public API yet.One Netlify Function for pages, Route Handlers and Server Actions; proxy.ts in a Netlify Edge Function.Its table marks Cache Components, streaming and after() as fully supported. A Node.js proxy.ts can't use the file system or C++ addons.
Cloudflarevinext first: a Vite plugin that reimplements Next.js's API, in beta. Or the OpenNext adapter, @opennextjs/cloudflare.A Cloudflare Worker; files from Workers Static Assets.With OpenNext, proxy.ts support is experimental (since 1.20.3) and needs the nodejs_compat flag. Revalidation needs a cache store (R2, the docs advise), a queue and a tag cache.
AWSOpenNext, @opennextjs/aws, with SST, AWS CDK or Terraform to create the resources. Or AWS Amplify Hosting.Lambda functions: the server, image optimization, revalidation. Files from .open-next/assets go to a CDN in the recommended setup.Amplify's own page says Next.js up through 15, without streaming, on-demand ISR or after().

And a short guide. Each row is a situation, and a way that fits it:

A short decision guide
IfConsiderWhy
Nothing in the app needs a request.A static export, on any static host.No servers, no functions: only files. Static exports covers what to check.
You need every Next.js feature, with nothing left out.Self-hosting with next start or Docker, or Vercel where its documented support covers what you use.The docs say a Node.js server and Docker support every feature. A verified adapter has public code and runs the compatibility tests, but support still varies: check what it covers.
You want nobody to run servers.A platform, ideally one with a verified adapter.Check its feature table against your app, and test a preview there: verification isn't a promise of every feature.
Your company already uses Netlify, Cloudflare or AWS.That platform's own Next.js integration.Read its feature table against what your app uses: proxy.ts, Cache Components, after(), image optimization.
You already run servers or Kubernetes.Self-hosting: next start or the standalone server.Every feature works. You run the shared cache, the scaling and the shutdowns.

Then check the platform against your own build, in four steps:

  1. Read its Next.js page: which Next.js versions it supports, and its feature table.
  2. Read your route table: a ◐ route needs streaming, ƒ Proxy (Middleware) needs Node.js code before the routes, and an after() needs time after the answer.
  3. Deploy a preview of your own app and test the pages that use those features.
  4. Prefer a verified adapter when you can: its code is public, and it runs Next.js's compatibility tests. Still check what it supports: verification isn't a promise of every feature.

To practise reading the outputs, sort what the shop's adapter received:

Which part of the outputs?
  • The code that renders /cart for a request.
  • app/api/prices/route.ts.
  • /menu, with revalidate 3600.
  • /cart's shell and its postponed state.
  • The JavaScript files in _next/static.
  • A static export's teas/mint.html.
  • proxy.ts, for /teas.
  • The pattern that matches /teas/oolong to /teas/[slug].
Try it yourself
0 of 8 correct

Sort each piece of the tea shop's build into the part of onBuildComplete's outputs it arrives in.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "Vercel runs Next.js through private hooks." No. The docs say its adapter uses the same public API as every other, and its code is open source.
  • "An adapter runs on every request." No. It runs while Next.js commands run: modifyConfig when the config loads, onBuildComplete after the build. What answers requests is what it built.
  • "Every platform supports every feature the same way." No. Amplify's page stops at Next.js 15, OpenNext's proxy.ts support on Cloudflare is experimental, and Netlify lists its own limits.
  • "vinext is Next.js on Cloudflare." Not quite. It reimplements Next.js's API on Vite; your app isn't built by Next.js there, so check its compatibility list.
  • "A static export needs an adapter." No. It's only files, which any host sends. An adapter on an export only receives static files.
  • "Platform claims stay true." No. They change with every release. Check the date on any comparison, this one included.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upFind the adapter in the config

Which line of this next.config.ts names the adapter?

Starter codeTypeScript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
  adapterPath: require.resolve("./my-adapter.js"),
};

export default nextConfig;

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upWhich hook ran at start?

    After the build, the shop runs next start. Which of this adapter's hooks prints something then?

    Starter codeTypeScript
    /** @type {import("next").NextAdapter} */
    const adapter = {
      name: "tea-shop-adapter",
    
      modifyConfig(config, { phase }) {
        console.log(`tea-shop-adapter: modifyConfig in ${phase}`);
        return config;
      },
    
      onBuildComplete({ outputs, routing }) {
        const paths = (list) => list.map((output) => output.pathname).filter((path) => !path.endsWith(".rsc")).sort();
        const proxy = outputs.middleware;
        const pages = outputs.prerenders.filter((output) => !/\.rsc$|\.segments\//.test(output.pathname));
        const htmlFiles = outputs.staticFiles.filter((file) => file.filePath.endsWith(".html"));
        console.log("tea-shop-adapter: onBuildComplete");
        console.log("  appPages:", paths(outputs.appPages).join(", ") || "none");
        console.log("  appRoutes:", paths(outputs.appRoutes).join(", ") || "none");
        console.log("  middleware:", proxy ? `${proxy.runtime}, for ${proxy.config.matchers.map((m) => m.source)}` : "none");
        for (const page of pages) {
          const file = !page.fallback?.filePath ? "no file" : page.fallback.postponedState ? "shell + postponed state" : "whole file";
          console.log(`  prerender ${page.pathname}: ${file}, revalidate ${page.fallback?.initialRevalidate ?? "none"}`);
        }
        console.log("  staticFiles:", outputs.staticFiles.length, "files, HTML:", paths(htmlFiles).join(", ") || "none");
        console.log("  dynamicRoutes:", routing.dynamicRoutes.map((route) => route.source).join(", ") || "none");
      },
    };
    
    module.exports = adapter;
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAn adapter on an export

      With this config, which part of outputs is the only one with anything in it?

      Starter codeTypeScript
      import type { NextConfig } from "next";
      
      const nextConfig: NextConfig = {
        output: "export",
        adapterPath: require.resolve("./my-adapter.js"),
      };
      
      export default nextConfig;
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the shell

        Which of the shop's routes did the adapter receive with postponed state?

        Starter codeTSX
        import { Suspense } from "react";
        import { cookies } from "next/headers";
        
        async function CartItem() {
          const tea = (await cookies()).get("cart")?.value;
          return <p>{tea ? `In your cart: ${tea}` : "Your cart is empty."}</p>;
        }
        
        export default function CartPage() {
          return (
            <main>
              <h1>Your cart</h1>
              <Suspense fallback={<p>Loading your cart…</p>}>
                <CartItem />
              </Suspense>
            </main>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · Challengeproxy.ts on Cloudflare

          The shop moves to Cloudflare with the OpenNext adapter, and keeps this proxy.ts. Which Workers compatibility flag does it need?

          Starter codeTypeScript
          import { NextResponse, type NextRequest } from "next/server";
          
          // The shop's old address for its teas still works: it shows the home page.
          export function proxy(request: NextRequest) {
            return NextResponse.rewrite(new URL("/", request.url));
          }
          
          export const config = { matcher: "/teas" };
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeA receipt sent after the answer

            The shop emails a receipt with after(), once the order page has been sent. Of the two AWS options, which one's docs say after() isn't supported?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask when the adapter runs, what it receives, and whose docs say so.

              The Adapters and other platforms quiz · 7 questionsScore: first tries count
              1. Question 1 of 7In which Next.js version did adapterPath become stable?

                Choose an answer to see the explanation.

              2. Question 2 of 7When does Next.js call an adapter's modifyConfig?

                Choose an answer to see the explanation.

              3. Question 3 of 7The same adapter runs on the shop as a static export. What does onBuildComplete receive?

                Choose an answer to see the explanation.

              4. Question 4 of 7What tells an adapter that /cart is a static shell with a part made per request?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which two adapters does the docs' Deploying page list as verified?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which way does Cloudflare's own Next.js guide recommend first, as of October 30, 2026?

                Choose an answer to see the explanation.

              7. Question 7 of 7How does Netlify run the shop's proxy.ts?

                Choose an answer to see the explanation.

              Key takeaways

              • An adapter is a module named by adapterPath or NEXT_ADAPTER_PATH, stable since Next.js 16.2.0.
              • modifyConfig runs whenever a command loads the config; onBuildComplete runs once after next build, with the outputs and the routing rules.
              • The outputs are code to run (appPages, appRoutes, middleware), pages made ahead of time with their revalidate and postponed state (prerenders), and files (staticFiles). An export has only files.
              • Vercel's adapter is verified and uses the public API. Netlify and Cloudflare (OpenNext, or vinext) and AWS (OpenNext, or Amplify) have their own integrations, with their own limits.
              • Choose from your app's needs, check the platform's own dated docs, and test a preview of your app there.

              Remember the one-liner.
              Next.js builds once and hands the result to an adapter; what works on a platform is what its adapter and its computers do with each part.

              Read it in Next.js's own code

              These links point at release v16.4.3, the version every example on this page was built with. The NextAdapter interface is in lines 505 to 578 of build-complete.ts, and the export branch that hands an adapter only static files is on lines 683 to 702.

              Coming next: The production checklist. Wherever the shop runs, it checks the same things before launch: performance, security, caching, errors, SEO and monitoring.

              CompleteFrontend Clear concepts. Working examples.