Hydration Proof

Search documentation

Find a page or section

Every option, its type and its default.

The hydration-proof config is optional: the framework, the build and start commands and the routes are detected. When you need to change something, write a TypeScript file that exports defineConfig({ ... }). Every option has a default, and command-line flags override the file.

The hydration-proof config file

hydration-proof reads the first of these files it finds in the directory it runs in: hydration-proof.config.{ts,mts,js,mjs,cjs,json}.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  routes: {
    dynamic: { "/products/[id]": ["1", "42"] },
  },
});
  • TypeScript is loaded by Node.js itself, with no extra tooling (it needs Node.js 22.18 or newer). Use plain types only: no enum, no namespace and no parameter properties. Relative imports need their file extension (./routes.ts).
  • defineConfig returns the object unchanged. It is there for type checking and completion.
  • Unknown options are errors, with a suggestion when the name looks like a typo ("is not a known option. Did you mean ...?").
  • configVersion is optional and currently always 1. Setting it lets a future version that changes the format migrate the file instead of guessing.
  • An older config is updated with hydration-proof migrate (see the CLI).

For a JSON config, point $schema at the schema in the package to get completion in your editor:

hydration-proof.config.json
{
  "$schema": "./node_modules/hydration-proof/schema/config.json",
  "routes": { "exclude": ["/api/**", "/admin/**"] }
}

The same schema is published at https://hydration.jscrate.dev/schema/config.json.

server

How the app is built and started, or the URL of one that is already running. The adapter supplies the commands, so most apps leave this out. {port} in a command is replaced with the chosen port, which is also passed as PORT. The app, and everything it spawned, is stopped when the run ends.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  server: {
    build: "pnpm build",
    command: "pnpm start --port {port}",
    // Build only when there is no build output yet; "always" in CI
    buildWhen: "if-missing",
    env: { NEXT_PUBLIC_API_URL: "http://localhost:4000" },
    timeout: 180_000,
  },
});

To test an app you started yourself, set url (or pass --url): nothing is built or started. mode: "both" tests the production build and the development server (devCommand) in one run.

OptionTypeDefaultDescription
commandstringthe adapter's start commandCommand that starts the app. {port} is replaced with the chosen port.
buildstring | falsethe adapter's build commandCommand that builds the app for production. false never builds.
buildWhen"always" | "if-missing" | "never""if-missing"Build before testing: always, only when no build output exists, or never.
urlstringURL of an app that is already running. When set, nothing is started.
portnumbera free portPort for the started app.
cwdstringthe config file's directoryWorking directory for the commands.
envRecord<string, string>Extra environment variables for the app.
timeoutnumber120000Milliseconds to wait for the app to answer.
reuseExistingbooleantrue outside CIUse an app already listening on the URL instead of starting one.
modeBuildMode | "both""production"Test the production build, the dev server, or both (and compare).
devCommandstringthe adapter's dev commandDevelopment server command when both modes are tested.

routes

Which pages are tested. Without paths, routes are discovered from the framework and the build output. Dynamic routes need example values, or they are skipped. Routes explains every source, the glob syntax and the route cache.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  routes: {
    discover: true,
    // Routes discovery cannot see: redirects, rewrites, anything behind a flag
    paths: ["/pricing?plan=team"],
    // One real value per dynamic segment, or the route is skipped
    dynamic: {
      "/blog/[slug]": ["hello-world"],
      "/products/[id]": ["1", "42"],
    },
    exclude: ["/api/**", "/admin/**"],
    sitemap: true,
  },
});
OptionTypeDefaultDescription
paths(string | RouteEntry)[]["/"] when discovery is offRoutes to test. Strings are paths.
dynamicRecord<string, string[]>Example values for dynamic segments, e.g. { "/products/[id]": ["1", "42"] }.
includestring[]Glob patterns (*, **) a route must match.
excludestring[]Glob patterns of routes to skip.
discoverbooleantrue when paths is emptyFind routes from the framework (Next.js app/ and pages/, plus build manifests).
queryRecord<string, string[]>Query-string variants per route pattern, e.g. { "/search": ["?q=shoes", "?q=&page=2"] }.
sitemapboolean | stringRead routes from the sitemap: true for /sitemap.xml (and robots.txt), or a sitemap URL/path.
crawlboolean | CrawlConfigFollow same-origin links found on tested pages.
notFoundbooleantrue for Next.jsAlso test a URL that does not exist, to check the not-found page hydrates.
manifestExamplesnumber3Most example values taken per dynamic route from build manifests.

scenarios

The browser environments every route is tested in. By default there is one scenario, default, that uses this machine's locale and timezone and a light color scheme, so a server started on the same machine renders with the same values. Scenarios also carry cookies, storage and a login function for signed-in pages; see scenarios.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  scenarios: [
    { name: "default" },
    { name: "dark-mobile", colorScheme: "dark", viewport: "mobile" },
    { name: "karachi", locale: "ur-PK", timezoneId: "Asia/Karachi" },
  ],
});
OptionTypeDefaultDescription
name*stringUnique name, shown in reports and used by --scenario.
localestringthis machine's localeBCP 47 locale for the browser (also sets Accept-Language).
timezoneIdstringthis machine's timezoneIANA timezone for the browser, e.g. Asia/Karachi.
colorSchemeColorScheme"light"Emulated prefers-color-scheme: "light", "dark" or "no-preference".
reducedMotion"reduce" | "no-preference"Emulated prefers-reduced-motion.
viewportViewportOptionViewport size as { width, height }, or a preset: "mobile" (390×844, touch), "tablet" (820×1180) or "desktop" (1280×800).
userAgentstringUser agent string for the browser context.
storageStatestringPlaywright storage state file (cookies and localStorage), e.g. a logged-in user.
cookiesCookieConfig[]Cookies set before every page load, e.g. a session cookie for a signed-in user.
headersRecord<string, string>Extra request headers.
localStorageRecord<string, string>localStorage entries set before page scripts run.
sessionStorageRecord<string, string>sessionStorage entries set before page scripts run.
initScriptsstring[]Scripts run before page scripts (code strings).
login(context: LoginContext) => Promise<void>Sign in once before this scenario's pages are tested. The cookies and storage it leaves behind are used for every page.
mocksMockConfig[]Answers for browser requests (API fixtures). Server-side requests are not affected.
includestring[]Only test routes matching these globs in this scenario.
excludestring[]Skip routes matching these globs in this scenario.
queryRecord<string, string>Query parameters added to every URL (e.g. { currency: 'EUR' }).
browserBrowserNamebrowser.nameBrowser for this scenario.
networkNetworkProfileThrottle the network: "fast-3g", "slow-3g" or { downloadKbps, uploadKbps, latencyMs }. Chromium throttles natively; Firefox and WebKit delay subresources instead.
cpunumberSlow down JavaScript by this factor (Chromium only).
cacheCacheStatecoldwarm loads the page once before testing it, like a returning visitor.
clockstring | numberFixed browser time (ISO string or epoch ms) for Date.now() / new Date(). A diagnostic option: the server keeps its real clock, so time-dependent output is still found.
randomSeednumberSeed for Math.random() and crypto.getRandomValues() in the browser (diagnostic, like clock).

matrix

Tests every scenario in combinations of environments, so a problem that only shows up in one locale, timezone, theme, screen size or browser is found. Pairwise selection (the default) tests every pair of values at least once with few combinations. The environment matrix guide has the details and the limits of each browser.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  matrix: {
    locale: ["en-US", "de-DE", "ar-EG"],
    timezoneId: ["UTC", "Asia/Karachi", "America/Los_Angeles"],
    colorScheme: ["light", "dark"],
    viewport: ["desktop", "mobile"],
    // Custom axes: feature flags, tenants, currencies
    axes: {
      checkout: {
        new: { cookies: [{ name: "flag-checkout", value: "new" }] },
        old: {},
      },
    },
  },
});

The first value of every axis is the baseline and is always tested. --no-matrix tests the scenarios as configured.

OptionTypeDefaultDescription
localestring[]Locales to test, e.g. ['en-US', 'de-DE', 'ar-EG'].
timezoneIdstring[]Timezones to test, e.g. ['UTC', 'Asia/Karachi', 'America/Los_Angeles'].
colorSchemeColorScheme[]Color schemes to test, e.g. ['light', 'dark'].
reducedMotion("reduce" | "no-preference")[]Reduced-motion settings to test.
viewportViewportOption[]Viewports to test: sizes or the presets mobile, tablet, desktop.
browserBrowserName[]Browsers to test: chromium, firefox, webkit.
networkNetworkProfile[]Network profiles to test: fast (no throttling), fast-3g, slow-3g or custom.
cpunumber[]CPU slowdown factors (Chromium only); 1 is no slowdown.
cacheCacheState[]Cache states to test: cold and warm.
axesRecord<string, Record<string, ScenarioVariant>>Custom axes: axis name → value name → scenario settings. { flags: { 'new-checkout': { cookies: [{ name: 'flag', value: 'on' }] }, 'old-checkout': {} } }
strategy"pairwise" | "full" | "sample"pairwise (default) covers every pair of values, full every combination, sample a random subset.
maxnumber16Most environments per scenario.
seednumber1Seed for sample.
scenariosstring[]allScenarios the matrix applies to.

probes

probes: true (or --probe) proves the cause of value mismatches. Pages with text or attribute differences are loaded again with one thing changed at a time: the clock, the random seed, the locale, the timezone, the theme, the viewport or storage. Each probed page costs up to 9 extra page loads, so probes are off by default. See probes.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  probes: { factors: ["time", "random"], maxPages: 5 },
  // Load every page 3 times and mark findings seen in only some runs as flaky
  repeat: 3,
});
OptionTypeDefaultDescription
factorsProbeFactor[]all that applyWhat to vary.
maxPagesnumber5Most pages probed per run.

ready

When a page counts as settled before the final snapshot. hydration-proof never waits for "network idle": it waits for a quiet period without DOM changes or React commits. For pages with animations, polling or live data that never go quiet, set a selector or a function, or lower quietMs.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  ready: {
    quietMs: 400,
    selector: "[data-page-ready]",
    // A page function, as a string, that must return a truthy value
    function: "() => window.__APP_READY__ === true",
  },
});

A route can override these with its own ready (see routes).

OptionTypeDefaultDescription
quietMsnumber400Quiet time (ms) without DOM changes or React commits before the page counts as settled.
timeoutnumber30000Maximum time (ms) per page.
hydrationTimeoutnumber15000Maximum time (ms) for hydration to finish once React is loaded.
selectorstringA selector that must exist before the final snapshot.
functionstringPage function source that must return truthy before the final snapshot.

checks

Turns individual checks on or off. Everything that compares the server HTML with the hydrated page is on by default. The interaction and navigation checks load each page several extra times, so they are off until you enable them; see interactions and navigation, which also covers the top-level interactions option for custom Playwright steps.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  checks: {
    // Also flag suppressHydrationWarning that hides nothing
    suppressedWarnings: "strict",
    interactions: true,
    navigation: { from: "/", maxRoutes: 10 },
  },
});
CheckFinds
reactErrorsErrors and warnings React reports (HP2xxx)
domDiffDOM differences React produced while hydrating, in the root and in every Suspense boundary (HP1xxx), and <head> values hydration changed (HP1014)
propsAuditAttributes and text that differ from what React renders on the client, including the ones React never reports, and events handled twice (HP5006)
invalidHtmlMarkup the browser repairs, duplicate ids and useId collisions between React roots (HP3xxx)
externalChangesChanges made by other scripts before hydration (HP4xxx)
suppressedWarningsDifferences hidden by suppressHydrationWarning ("info"), plus unused suppression ("strict")
OptionTypeDefaultDescription
reactErrorsbooleantrueCollect React hydration errors and warnings.
domDiffbooleantrueCompare the DOM before and after hydration.
propsAuditbooleantrueCompare attributes and text with what React renders on the client.
invalidHtmlbooleantrueCheck the server HTML for markup the browser has to repair.
externalChangesbooleantrueDetect changes made by other scripts before hydration.
suppressedWarnings"off" | "info" | "strict""info"How to treat suppressHydrationWarning: report suppressed differences as info, or also flag unused suppression.
interactionsbooleanfalseType, click, focus and scroll while the page loads and check nothing is lost.
navigationboolean | NavigationConfigfalseCompare client-side navigation to each route with loading it directly (Next.js).

ignore

Findings to leave out. Ignored findings stay in the report, marked as ignored, and do not fail the run. An issues rule needs a reason, and a rule past its expires date fails the run, so a temporary exception cannot become permanent. Ignoring findings covers when to use each kind.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  ignore: {
    // Third-party markup that is expected to differ
    selectors: ["#ad-slot", "[data-chat-widget]"],
    attributes: [/^data-gtm-/],
    issues: [
      {
        code: "HP4001",
        route: "/checkout",
        reason: "The payment iframe rewrites its container. Ticket ACME-431.",
        expires: "2026-12-31",
      },
    ],
  },
});

[data-hydration-proof-ignore] is always in selectors, so you can also mark an element in your markup.

OptionTypeDefaultDescription
selectorsstring[]Elements whose subtree is not compared. [data-hydration-proof-ignore] is always included.
attributes(string | RegExp)[]Attribute names (or patterns) that are never compared.
textPatternsRegExp[]Text differences are ignored when both values are equal after removing these patterns.
issuesIgnoreRule[]Ignore specific findings.

ci

When the run fails: the lowest failing severity, a warning limit, a baseline of accepted findings, budgets and a history file for trends. Baselines and budgets explains how to adopt hydration-proof on an app that already has findings.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  ci: {
    failOn: "warning",
    maxWarnings: 10,
    newIssuesOnly: true,
    budget: {
      warning: 5,
      codes: { HP1004: 3 },
      routes: { "/legacy/**": { error: 2 } },
    },
    history: true,
  },
});
OptionTypeDefaultDescription
failOnSeverity | "never""error"Lowest severity that makes the run fail.
maxWarningsnumberFail when more warnings than this are found.
baselinestring.hydration-proof/baseline.jsonBaseline file of accepted issues.
newIssuesOnlybooleanOnly fail on issues that are not in the baseline.
budgetBudgetConfigHydration error budget: how many findings of a severity (in total, per route glob or per code) are allowed before the run fails.
historyboolean | stringAppend a line per run to a history file for trends: true for .hydration-proof/history.ndjson, or a path.

hooks

Code that runs around the test run, after the app is up. setup runs once per build mode, before any page is tested and before the scenario logins, and may return a teardown function. Teardown functions always run, even when testing fails. An error in setup stops the run. There are only these two hooks; sign-in belongs in a scenario's login.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  hooks: {
    // Seed data or create users. May return a teardown function.
    setup: async ({ baseUrl }) => {
      await fetch(`${baseUrl}/api/test/seed`, { method: "POST" });
      return async () => {
        await fetch(`${baseUrl}/api/test/reset`, { method: "POST" });
      };
    },
  },
});
OptionTypeDefaultDescription
setup(context: HookContext) => Promise<void | (() => Promise<void> | void)> | void | (() => Promise<void> | void)Runs once after the app is up and before any page is tested (seed a database, create users). May return a teardown function.
teardown(context: HookContext) => Promise<void> | voidRuns once after all pages are tested.

owners

Who owns a finding. Owners come from route globs in the config and from the CODEOWNERS entries of the source files, and they are shown in every report and can be filtered in the HTML report.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  owners: {
    routes: { "/checkout/**": ["@acme/payments"] },
    codeowners: true,
  },
});
OptionTypeDefaultDescription
routesRecord<string, string | string[]>Route glob → owners, e.g. { '/checkout/**': ['@acme/payments'] }.
codeownersboolean | stringOwners of source files from CODEOWNERS: true (default) looks in .github/, docs/ and the repository root; or a path.

Other options

The table lists every top-level option, including the ones above:

OptionTypeDefaultDescription
$schemastringJSON Schema for editor completion in a JSON config, e.g. ./node_modules/hydration-proof/schema/config.json.
configVersion1Config format version.
adapter"auto" | "next" | "react-router" | "remix" | "astro" | "vite" | "node" | "none" | (string & {}) | Adapter"auto"Framework adapter. "auto" detects it from package.json and config files; or name one: "next", "react-router", "remix", "astro", "vite", "node", "none", a plugin adapter, or an adapter from defineAdapter().
pluginsHydrationProofPlugin[]Plugins: adapters, normalizers, cause detectors, reporters and route providers.
serverServerConfigHow the app is built and started, or the URL of one already running. See server.
routesRoutesConfigWhich pages are tested: discovered, listed, from the sitemap or crawled. See routes.
scenariosScenarioConfig[]one scenario named default, using this machine's locale and timezone and a light color schemeEnvironments every route is tested in: locale, timezone, theme, viewport, cookies, signed-in state. See scenarios.
readyReadyConfigWhen a page counts as settled before the final snapshot. See ReadyConfig.
browserBrowserConfig{ name: "chromium", headless: true }Which browser runs the pages.
workersnumberhalf the CPU cores, at most 4Pages tested in parallel.
retriesnumber0 (1 on CI)Retries for pages that failed to load.
checksChecksConfigTurn individual checks on or off. See checks.
ignoreIgnoreConfigSelectors, attributes, text patterns and findings to leave out. See ignoring findings.
reportersReporterName[]["list", "json", "html"], plus github inside GitHub ActionsOutput formats: list, json, html, junit, sarif, github, gitlab. See reports.
outputDirstring.hydration-proof/reportWhere reports are written.
screenshots"failures" | "all" | "off""failures" when the html reporter is on, otherwise "off"Screenshots for the HTML report: of failing pages ("failures"), of every page ("all"), or none ("off").
ciCiConfigWhen the run fails: severity, baselines, budgets and history. See baselines and budgets.
hooksHooksConfigCode that runs once before the first page (setup) and after the last (teardown).
cachebooleantrueCache discovered routes between runs (keyed by build).
matrixMatrixConfigTest every scenario in combinations of environments.
probesboolean | ProbesConfigfalseProve causes: pages with value mismatches are loaded again with one thing changed (clock, random seed, locale, timezone, theme, viewport, storage).
repeatnumber1Load every page this many times and report flaky issues.
interactionsInteractionConfig[]Custom interactions to run on routes (before or after hydration).
ownersOwnersConfigWho owns findings: route owners and CODEOWNERS.
redactboolean | RedactConfigtrueRemove secrets and personal data from reports.
projects(string | ProjectConfig)[]Monorepo: test these projects (each has its own config) in one run.
sourceOriginsstring[]Extra origins scripts and source maps may be fetched from, for apps that serve assets from a CDN. The app's own origin is always allowed.

adapter and plugins

adapter is "auto" by default: it is chosen from package.json and the framework's config files. Name one to override it, or pass your own from defineAdapter(). Plugins add normalizers, cause detectors, route providers, reporters and adapters. See adapters and plugins.

browser

OptionTypeDefaultDescription
nameBrowserName"chromium"chromium, firefox or webkit.
channelstringInstalled browser channel, e.g. chrome or msedge.
headlessbooleantrue (--headed turns it off)Run the browser without a window.

redact

Redaction is on by default: emails, tokens, card numbers and secret URL parameters are removed from every report. Add patterns, black out elements in screenshots, or set redact: false to keep everything. See security.

OptionTypeDefaultDescription
builtInbooleantrueRemove emails, tokens, card numbers and secret URL parameters.
patternsRegExp[]More text to remove from reports.
selectorsstring[]Elements to black out in screenshots.

projects

A config at the root of a monorepo can list the apps to test. Each app keeps its own config, and the run writes one combined report. See monorepos.

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  projects: ["apps/web", { path: "apps/admin", name: "admin" }],
});
OptionTypeDefaultDescription
path*stringFolder of the project (with its own hydration-proof config).
namestringthe folder nameName shown in reports.
configstringfound automaticallyConfig file inside the folder.

sourceOrigins

To report a file:line, hydration-proof downloads the page's scripts and their source maps, but only from the app's own origin. If your app serves its bundles from a CDN, allow that origin too:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  sourceOrigins: ["https://cdn.example.com"],
});

Findings are still reported when a source map is skipped, only without a source location.