Overview

A headless "new build available" indicator for Next.js. Detects when a newer deployment is live and surfaces that state to your UI — no opinions about how to render it.

Works on any host. Ships first-class support for Vercel deployments.

Install

pnpm add next-build-notifier

Peer dependencies: react >= 18, react-dom >= 18, next >= 14 (optional).

Architecture

next-build-notifier is split across three entry points:

  • next-build-notifier/config (withBuildNotifier) — a Next.js config wrapper that injects NEXT_PUBLIC_BUILD_ID into the client bundle at build time so the browser always knows which build it started on.
  • next-build-notifier/server (createVersionRoute) — a Route Handler factory for app/api/version/route.ts. Returns the current deployment's build id with Cache-Control: no-store; add export const dynamic = 'force-dynamic' in your route file so it renders dynamically rather than freezing the id at build time.
  • next-build-notifier (client) — BuildNotifierProvider, BuildNotifier, and useBuildNotification. These are Client Components; mount them from a client boundary imported into your server root layout.

Usage

1. Wrap your Next.js config

// next.config.ts
import { withBuildNotifier } from 'next-build-notifier/config';
import type { NextConfig } from 'next';

const config: NextConfig = { /* … */ };
export default withBuildNotifier(config);

2. Add the version route handler

// app/api/version/route.ts
import { createVersionRoute } from 'next-build-notifier/server';

export const dynamic = 'force-dynamic';
export const { GET } = createVersionRoute();

3. Mount the provider and render (or go headless)

BuildNotifierProvider is a Client Component — wrap it in a client boundary and mount that from the (server) root layout.

// app/providers.tsx
'use client';

import { BuildNotifierProvider, BuildNotifier } from 'next-build-notifier';
import type { ReactNode } from 'react';

export function Providers({ children }: { children: ReactNode }) {
    return (
        <BuildNotifierProvider currentBuildId={process.env.NEXT_PUBLIC_BUILD_ID!} intervalMs={60_000}>
            {children}
            <BuildNotifier>
                {(s) =>
                    s.updateAvailable && !s.dismissed ? (
                        <button type="button" onClick={s.reload}>
                            Reload — a new version is available
                        </button>
                    ) : null
                }
            </BuildNotifier>
        </BuildNotifierProvider>
    );
}
// app/layout.tsx  (Server Component)
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="en">
            <body>
                <Providers>{children}</Providers>
            </body>
        </html>
    );
}

currentBuildId is required — pass the build ID baked in by withBuildNotifier (process.env.NEXT_PUBLIC_BUILD_ID).

Headless — consume state directly

'use client';

import { useBuildNotification } from 'next-build-notifier';

export function UpdateBanner() {
    const { updateAvailable, dismissed, reload, dismiss } = useBuildNotification();

    if (!updateAvailable || dismissed) return null;

    return (
        <div role="alert">
            New version available.
            <button type="button" onClick={reload}>Reload</button>
            <button type="button" onClick={dismiss}>Dismiss</button>
        </div>
    );
}

How detection works

  1. At build time withBuildNotifier sets NEXT_PUBLIC_BUILD_ID to the resolved build ID.
  2. resolveBuildId determines that value using this precedence: NEXT_DEPLOYMENT_IDVERCEL_DEPLOYMENT_IDGIT_COMMIT_SHAVERCEL_GIT_COMMIT_SHANEXT_PUBLIC_BUILD_IDBUILD_ID → the string literal 'development' when none are set.
  3. On the client, BuildNotifierProvider checks /api/version (or a custom endpoint) on focus/visibility/reconnect, plus every intervalMs milliseconds when an interval is set.
  4. When the returned build ID differs from the one baked into the bundle, updateAvailable flips to true.

Vercel

Vercel's Skew Protection pins framework-level requests (RSC payloads, route prefetches) to the deployment that served the page. A plain fetch from the browser is not pinned — it resolves against the current live deployment. The /api/version route leverages this: it always returns the build ID of whichever deployment is currently receiving traffic, so a mismatch between that and the baked-in build ID reliably signals a new release.

Self-hosting

On Vercel this works out of the box because VERCEL_DEPLOYMENT_ID is present at both build time and runtime. When you self-host, you must supply a stable per-deploy identifier through an env var that is set during both the build and the running server — GIT_COMMIT_SHA, BUILD_ID, or Next's own NEXT_DEPLOYMENT_ID all work. Without one, both the baked client id and the /api/version response resolve to 'development', so the notifier stays permanently inert: it raises no false positives, but it also never detects a new release.

Configuration

PropTypeDefaultNotes
currentBuildIdstringRequired. The build ID baked into the running client (process.env.NEXT_PUBLIC_BUILD_ID).
intervalMsnumberundefinedPolling interval in ms. A falsy value (0 or undefined) disables the periodic timer; event triggers (focus/visibility/online) still fire.
endpointstring'/api/version'URL of the version route handler.
fetcherfunctionbuilt-inCustom async function that resolves to a VersionResponse.

Dependencies

This package is intentionally dependency-free — it uses only React, the browser's fetch, and standard Web APIs.

In this section

  • Overview — this page
  • Changelog — release history

On this page