Headless version/update detection for web apps.
@almeidx/version-check polls a deployment version endpoint and reports when the deployed
identity differs from the version that rendered the current page. Framework packages keep the same
renderless model so applications can show their own “New version available, refresh to update.”
message.
| Package | Purpose |
|---|---|
@almeidx/version-check |
Core checker, comparison helpers, polling lifecycle |
@almeidx/version-check-react |
React hook |
@almeidx/version-check-next |
Next.js build id route helper and client hook |
@almeidx/version-check-vue |
Vue composable |
@almeidx/version-check-vite |
Vite build id plugin and virtual module |
Framework packages re-export the core API and expose the version-check CLI, so apps only need to
install the package they use.
The default endpoint is /version.json. The payload can be a string or an object with one of
version, buildId, id, hash, or sha.
{
"buildId": "2026-06-01T12-00-00Z"
}Generate a simple public/version.json during builds with the package CLI.
version-check generate publicFor package scripts:
{
"scripts": {
"prebuild": "version-check generate public"
}
}The generated buildId uses VERSION_CHECK_BUILD_ID, SOURCE_COMMIT,
VERCEL_GIT_COMMIT_SHA, CI_COMMIT_SHA, GITHUB_SHA, or local-dev.
import { createVersionChecker } from "@almeidx/version-check";
const checker = createVersionChecker({
currentVersion: window.__BUILD_ID__,
intervalMs: 30 * 60 * 1000,
});
checker.subscribe((state) => {
if (state.updateAvailable) {
document.querySelector("#update-banner")?.removeAttribute("hidden");
}
});
checker.start();Use the Vite plugin as the single source of truth for the client build id and /version.json.
// vite.config.ts
import { versionCheck } from "@almeidx/version-check-vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [versionCheck()],
});Read the same id from the virtual module in client code:
import buildId from "virtual:version-check/build-id";
import { createVersionChecker } from "@almeidx/version-check";
const checker = createVersionChecker({
currentVersion: buildId,
});For TypeScript, include the virtual module declarations in your app env file:
import "@almeidx/version-check-vite/virtual";By default, the Vite plugin resolves the build id from deployment environment variables only. Use
buildId or resolveBuildId when you want an explicit value from another source. The virtual module
is the default client API; set define: true only if you explicitly want a global Vite constant.
import { useVersionCheck } from "@almeidx/version-check-react";
function refreshPage() {
window.location.reload();
}
export function VersionBanner({ currentVersion }: { currentVersion: string }) {
const versionCheck = useVersionCheck({
currentVersion,
intervalMs: 30 * 60 * 1000,
});
if (!versionCheck.updateAvailable) return null;
return (
<div role="status">
New version available.
<button type="button" onClick={refreshPage}>
Refresh
</button>
</div>
);
}Create an App Router route:
// app/api/version/route.ts
import { createNextVersionHandler } from "@almeidx/version-check-next";
export const GET = createNextVersionHandler();Pass the initial build id from the root layout:
// app/layout.tsx
import { getNextBuildId } from "@almeidx/version-check-next";
import type { PropsWithChildren } from "react";
import { VersionBanner } from "./version-banner";
export default async function RootLayout({ children }: PropsWithChildren) {
const buildId = await getNextBuildId();
return (
<html lang="en">
<body>
<VersionBanner initialVersion={buildId} />
{children}
</body>
</html>
);
}Use the client hook:
"use client";
import { useNextVersionCheck } from "@almeidx/version-check-next/client";
function refreshPage() {
window.location.reload();
}
export function VersionBanner({ initialVersion }: { initialVersion: string }) {
const versionCheck = useNextVersionCheck({
currentVersion: initialVersion,
});
if (!versionCheck.updateAvailable) return null;
return (
<div role="status">
New version available.
<button type="button" onClick={refreshPage}>
Refresh
</button>
</div>
);
}The composable returns the reactive state plus check (force a check now) and stop (tear down
early). It only polls in the browser, so it is SSR-safe, and it disposes automatically with the
surrounding effect scope.
import { useVersionCheck } from "@almeidx/version-check-vue";
const { state } = useVersionCheck({
currentVersion: import.meta.env.VITE_BUILD_ID,
});
function refreshPage() {
window.location.reload();
}<button v-if="state.updateAvailable" type="button" @click="refreshPage">Refresh to update</button>By default, any deployment identity change is considered an update. Use compare when your app
needs a stricter policy, such as semver-only upgrades or channel-aware deploys.
createVersionChecker({
currentVersion: "2.0.0",
compare: ({ currentVersion, latestVersion }) => String(latestVersion) > String(currentVersion),
});The checker polls every intervalMs (default 30 minutes) and also re-checks on window focus, network
reconnect, and the tab becoming visible. A few options tune this:
pauseWhenHidden(defaulttrue) pauses the interval while the tab is hidden and checks when it becomes visible again if the lifecycle cooldown has elapsed, instead of polling a backgrounded tab.minIntervalMs(default 1 minute) sets a minimum gap between lifecycle-triggered checks. Set it to0to disable the cooldown. Bursts of focus/online/visibility events are always collapsed to a single in-flight check regardless.refetchOnWindowFocus,refetchOnReconnect, andrefetchOnVisibilityChange(each defaulttrue) turn off individual triggers.
The React and Vue hooks also expose check() to force an immediate check (e.g. from a "check for
updates" button); the Vue composable additionally returns stop().
pnpm install
pnpm checkUseful scripts:
pnpm buildbuilds packages and examples.pnpm build:typecheckruns TypeScript strict type checks.pnpm lintruns oxlint.pnpm fmtruns oxfmt.pnpm testruns vitest.