# Adapters

> hydration-proof adapters build, start and find the routes of Next.js, React Router, Remix, Astro and Vite SSR apps, or write your own with defineAdapter.

Source: https://hydration.jscrate.dev/docs/adapters
Last updated: 2026-09-18

hydration-proof adapters tell the CLI how to build and start your app, where
its routes are, and which markup the framework adds that is not page content.
With the default `adapter: "auto"`, the adapter is chosen from `package.json`
and your config files. Set it yourself when detection is wrong, or write one
for a framework it does not know.

## Which hydration-proof adapters are built in?

| Adapter        | Detected by                                                      | Build / start / dev                                                                                                   | Routes                                                 | Client navigation                           |
| -------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------- |
| `next`         | `next` dependency or `next.config.*`                             | `next build` (or your `build` script when it runs `next build`) / `next start --port {port}` / `next dev`             | `app/` and `pages/`, plus pages the build pre-rendered | `router.push` (App Router and Pages Router) |
| `react-router` | `@react-router/dev` or `react-router.config.*`                   | `react-router build` / `react-router-serve ./build/server/index.js` / `react-router dev`                              | `react-router routes --json`                           | `window.__reactRouterDataRouter.navigate`   |
| `remix`        | `@remix-run/dev`                                                 | `remix vite:build` / `remix-serve ./build/server/index.js` / `remix vite:dev` (the classic compiler is supported too) | `remix routes --json`                                  | `window.__remixRouter.navigate`             |
| `astro`        | `astro` or `astro.config.*`                                      | `astro build` / `node ./dist/server/entry.mjs` with `@astrojs/node`, otherwise `astro preview` / `astro dev`          | `src/pages`                                            | —                                           |
| `vite`         | `vite` and `react-dom`, plus `server.js` or `src/entry-server.*` | the `build`, `start` (or `serve`, `preview`) and `dev` scripts                                                        | from the config                                        | —                                           |
| `node`         | `react-dom` and a `start` script                                 | the `build`, `start` and `dev` scripts                                                                                | from the config                                        | —                                           |
| `none`         | the fallback                                                     | `server.command` / `server.build`                                                                                     | from the config                                        | —                                           |

With `adapter: "auto"`, adapters from [plugins](https://hydration.jscrate.dev/docs/plugins) are tried first,
then the built-in ones in the order of the table. The first match wins, and
`none` is used when nothing matches.

Each adapter also knows the file that means a build exists, such as
`.next/BUILD_ID` for Next.js. By default the app is built only when that file
is missing (`server.buildWhen: "if-missing"`); `--build` forces a fresh build.

### What else an adapter handles

- **Framework markup.** Markup the framework adds is left out of every
  comparison: the Next.js route announcer, dev overlay and RSC payload scripts,
  the React Router and Remix context scripts, Astro's island loader and dev
  toolbar, and Vite's client and React Refresh scripts.
- **The development server host.** The `next` adapter opens `next dev` on
  `localhost`, because Next.js blocks development resources for other hosts.
- **The not-found page.** The `next`, `react-router`, `remix` and `astro`
  adapters also load `/hydration-proof-not-found` and check that the not-found
  page hydrates. It must answer 404. Turn this off with `routes.notFound: false`.
- **Client navigation.** Adapters with a router in the last column support
  [navigation checks](https://hydration.jscrate.dev/docs/interactions), which compare a client-side
  navigation to each route with loading it directly.

Dynamic routes use one syntax for every framework: `[id]`, `[...slug]`
(catch-all) and `[[lang]]` (optional). React Router's `:id`, `*` and `:lang?`
are converted, so `routes.dynamic` looks the same everywhere:

```ts title="hydration-proof.config.ts"
import { defineConfig } from "hydration-proof";

export default defineConfig({
  routes: { dynamic: { "/products/[id]": ["1", "42"] } },
});
```

## Choose an adapter

Set `adapter` when detection picks the wrong framework, or to skip detection
altogether:

```ts title="hydration-proof.config.ts"
import { defineConfig } from "hydration-proof";

export default defineConfig({ adapter: "react-router" });
```

| Value                                                                | Meaning                                                           |
| -------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `"auto"`                                                             | Detect from `package.json` and config files (the default)         |
| `"next"`, `"react-router"`, `"remix"`, `"astro"`, `"vite"`, `"node"` | That built-in adapter                                             |
| `"none"`                                                             | No framework: `server.command` and `routes` come from your config |
| a plugin adapter's name                                              | An adapter a plugin provides                                      |
| `defineAdapter({ … })`                                               | An adapter object from your config                                |

An unknown name stops the run with exit code `2` and lists the adapters that
exist. To see what was detected, and how many routes it found, run:

```bash
npx hydration-proof doctor
```

Every adapter can be overridden: `server.command`, `server.build`,
`server.devCommand` and `routes` in your config always win.

## Custom React servers

For Express, Fastify, `node:http` or any other server that renders React, use
the `node` adapter (or `none`) with your own commands:

```ts title="hydration-proof.config.ts"
import { defineConfig } from "hydration-proof";

export default defineConfig({
  adapter: "node",
  server: {
    build: "npm run build",
    command: "node server.js", // listens on process.env.PORT
    devCommand: "node --watch server.js",
  },
  routes: { paths: ["/", "/pricing", "/products/1"] },
});
```

`{port}` in a command is replaced with the port hydration-proof chose, and the
same port is in the `PORT` environment variable. A custom start command only
gets a build step when you configure `server.build`.

Streaming servers are supported. With `renderToPipeableStream` or
`renderToReadableStream`, Suspense boundaries that hydrate later are compared
on their own. [Custom React servers](https://hydration.jscrate.dev/docs/frameworks/custom-server) covers the
setup in detail.

## Astro islands

Each `<astro-island>` hydrates as its own React root, so findings are reported
per island. Astro changes island attributes (`ssr`, `props`, `client` and
others) while it loads them; these are never compared. Pages without any React
are normal in an Astro site and are not reported.

Islands that call `useId` need an `identifierPrefix`, or two roots generate the
same ids. Astro's React integration sets one per island; without it
hydration-proof reports [HP3004](https://hydration.jscrate.dev/docs/issues/hp3004). See
[Astro hydration errors](https://hydration.jscrate.dev/docs/frameworks/astro).

## Write an adapter with defineAdapter

For a framework hydration-proof does not know, create an adapter with
`defineAdapter`, directly in the config or in a plugin:

```ts
function defineAdapter(definition: AdapterDefinition): Adapter
```

Create an adapter for a framework hydration-proof does not know.

```ts title="hydration-proof.config.ts"
import { existsSync } from "node:fs";
import { join } from "node:path";
import { defineAdapter, defineConfig } from "hydration-proof";

const waku = defineAdapter({
  name: "waku",
  detect: (rootDir) => existsSync(join(rootDir, "waku.config.ts")),
  commands: () => ({
    build: "waku build",
    start: "waku start --port {port}",
    dev: "waku dev --port {port}",
    buildOutput: "dist/index.js",
  }),
  discoverRoutes: () => [
    {
      pattern: "/",
      dynamic: false,
      router: "app",
      file: "src/pages/index.tsx",
    },
  ],
  normalizers: [
    {
      name: "waku-data",
      match: (node) =>
        node.k === 1 &&
        node.tag === "script" &&
        node.attrs.some(([name]) => name === "data-waku")
          ? "drop"
          : undefined,
    },
  ],
  navigation: {
    navigate: `(url) => { window.__WAKU_ROUTER__?.push(url); return Boolean(window.__WAKU_ROUTER__); }`,
  },
  notFound: true,
});

export default defineConfig({ adapter: waku });
```

| Field                                         | Description                                                                                                                                |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`                                        | Shown in reports. `adapter: "<name>"` selects an adapter a plugin provides                                                                 |
| `detect(rootDir)`                             | Used by `adapter: "auto"`; plugin adapters are tried before the built-in ones. Without it, the adapter is never detected                   |
| `commands({ rootDir, packageManager })`       | `build`, `start`, `dev`, `buildOutput` (a file that exists after a build) and `env`. `{port}` is replaced, and `PORT` is set               |
| `discoverRoutes({ rootDir, packageManager })` | Routes with `pattern`, `dynamic`, `router`, `file` (relative to the project) and optionally `wrappers` (layout files, used by `--changed`) |
| `normalizers`                                 | Markup to ignore: return `"drop"` to remove a node, `"opaque"` to keep an element but not compare its content                              |
| `ignoreAttributes`, `elementAttributes`       | Attributes never compared, everywhere or only on some elements (`{ "astro-island": ["ssr"] }`)                                             |
| `devHost`                                     | Host for the development server (Next.js needs `localhost`)                                                                                |
| `navigation`                                  | Page function sources that start a client navigation (`navigate`) and a prefetch (`prefetch`). Return `false` when the router is missing   |
| `notFound`                                    | Also load a URL that does not exist and check the not-found page                                                                           |
| `pagesWithoutReact`                           | Pages without React are normal (islands architectures) and are not reported                                                                |

The options that are not functions, with their types:

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `name` (required) | `string` | — | Adapter name, used by `adapter: '<name>'` and shown in reports. |
| `normalizers` | `NormalizerRule[]` | — | Rules that drop or blank framework markup before DOM stages are compared. |
| `ignoreAttributes` | `(string \| RegExp)[]` | — | Attributes the framework adds or changes on its own; never compared. |
| `elementAttributes` | `Record<string, (string \| RegExp)[]>` | — | Attributes the framework changes on its wrapper elements, by tag name. |
| `devHost` | `string` | — | Host to use for the dev server, for frameworks that block other origins (Next.js uses `localhost`). |
| `navigation` | `AdapterNavigation` | — | Page-function sources that start (`navigate`) and prefetch (`prefetch`) a client-side navigation, for navigation checks. |
| `notFound` | `boolean` | — | Also test a URL that does not exist. |
| `pagesWithoutReact` | `boolean` | — | Pages without React are normal (islands architectures). |

The adapter API is part of the stable public API: see
[compatibility](https://hydration.jscrate.dev/docs/compatibility). To share an adapter between apps, put it
in a [plugin's](https://hydration.jscrate.dev/docs/plugins) `adapters` list.

## Related

- [Next.js hydration errors](https://hydration.jscrate.dev/docs/frameworks/nextjs) and the `next` adapter
- [React Router](https://hydration.jscrate.dev/docs/frameworks/react-router) and
  [Remix](https://hydration.jscrate.dev/docs/frameworks/remix) apps
- [Vite SSR apps](https://hydration.jscrate.dev/docs/frameworks/vite-ssr)
- [Routes](https://hydration.jscrate.dev/docs/routes): discovery, dynamic values, sitemaps and crawling
- [Plugins](https://hydration.jscrate.dev/docs/plugins): normalizers, detectors, routes and reporters
