Version skew is a critical challenge in modern web applications, particularly in Next.js deployments.
When users keep your application open for extended periods, they might run outdated code against your latest backend changes.
This article explores how to implement robust version control and automatic updates, with a special focus on Makerkit's built-in solution - that you can apply to your own Next.js SaaS project.
NB: Vercel has a feature called "Skew Protection" that takes care of this specific issue, but outside of it you're largely on your own. This guide is intended for those hosting their own deployments outside of Vercel, such as Cloudflare Pages, Digital Ocean, AWS, etc.
Understanding Version Skew
Version skew occurs when a user's browser runs an outdated version of your frontend code while attempting to interact with an updated backend.
This mismatch can lead to:
- Unexpected API errors
- Broken functionality
- Poor user experience
- Data inconsistencies
- Security vulnerabilities
The Solution: Automatic Version Checking
A robust solution involves three key components:
- A version endpoint that returns the current deployment's version
- A client-side version checker that periodically polls this endpoint
- An update mechanism that prompts users when a new version is available
1. The Version Endpoint
// app/version/route.ts/** * We force it to static because we want to cache for as long as the build is live. */export const dynamic = 'force-static';// please provide your own implementation// if you're not using Vercel or Cloudflare Pagesconst KNOWN_GIT_ENV_VARS = [ 'CF_PAGES_COMMIT_SHA', 'VERCEL_GIT_COMMIT_SHA', 'GIT_HASH',];export const GET = async () => { const currentGitHash = await getGitHash(); return new Response(currentGitHash, { headers: { 'content-type': 'text/plain', }, });};async function getGitHash() { for (const envVar of KNOWN_GIT_ENV_VARS) { if (process.env[envVar]) { return process.env[envVar]; } } try { return await getHashFromProcess(); } catch (error) { console.warn( `[WARN] Could not find git hash: ${JSON.stringify(error)}. You may want to provide a fallback.`, ); return ''; }}async function getHashFromProcess() { // avoid calling a Node.js command in the edge runtime if (process.env.NEXT_RUNTIME === 'nodejs') { if (process.env.NODE_ENV !== 'development') { console.warn( `[WARN] Could not find git hash in environment variables. Falling back to git command. Supply a known git hash environment variable to avoid this warning.`, ); } const { execSync } = await import('child_process'); return execSync('git log --pretty=format:"%h" -n1').toString().trim(); } console.log( `[INFO] Could not find git hash in environment variables. Falling back to git command. Supply a known git hash environment variable to avoid this warning.`, );}
This endpoint serves the current deployment's Git hash, which acts as our version identifier. It's marked as force-static
to ensure optimal caching.
If the getHashFromProcess
functions fails in your environment, make sure to provide a better solution for getting the hash.
2. The Version Checker
The version checker is a React component that:
- Polls the version endpoint at regular intervals
- Compares the current version with the running version
- Triggers an update prompt when differences are detected
3. The Update Mechanism
When a version mismatch is detected, users are prompted to reload their application to get the latest version. This ensures they're always running the most recent code.
Makerkit: Built-in Protection
Makerkit, a premium Next.js SaaS starter kit, comes with this protection built-in. Its implementation includes:
- Automatic version checking with configurable intervals
- User-friendly update prompts
- Seamless integration with major deployment platforms
- Support for both development and production environments
- Customizable UI components
The version checker in Makerkit uses React Query for efficient polling and state management, along with ShadCN UI for a polished user interface.
Best Practices
When implementing version checking:
- Choose appropriate polling intervals (e.g., 120 seconds)
- Use environment variables for configuration
- Implement graceful fallbacks
- Consider user experience in update prompts
- Cache version endpoints effectively
Example Implementation
Here's a simplified version of Makerkit's implementation:
function useVersionUpdater(props: { intervalTimeInSecond?: number } = {}) { const interval = VERSION_UPDATER_REFETCH_INTERVAL_SECONDS ? Number(VERSION_UPDATER_REFETCH_INTERVAL_SECONDS) : DEFAULT_REFETCH_INTERVAL; return useQuery({ queryKey: ['version-updater'], refetchInterval: interval * 1000, queryFn: async () => { const response = await fetch('/version'); const currentVersion = await response.text(); // Compare versions and trigger updates }, });}
Below is the full implementation of Makerkit's version updater:
'use client';import { useEffect, useState } from 'react';import { useQuery } from '@tanstack/react-query';import { RocketIcon } from 'lucide-react';import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,} from '../shadcn/alert-dialog';import { Button } from '../shadcn/button';import { Trans } from './trans';/** * Current version of the app that is running */let version: string | null = null;/** * Default interval time in seconds to check for new version * By default, it is set to 120 seconds */const DEFAULT_REFETCH_INTERVAL = 120;/** * Default interval time in seconds to check for new version */const VERSION_UPDATER_REFETCH_INTERVAL_SECONDS = process.env .NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS;export function VersionUpdater(props: { intervalTimeInSecond?: number }) { const { data } = useVersionUpdater(props); const [dismissed, setDismissed] = useState(false); const [showDialog, setShowDialog] = useState<boolean>(false); useEffect(() => { setShowDialog(data?.didChange ?? false); }, [data?.didChange]); if (!data?.didChange || dismissed) { return null; } return ( <AlertDialog open={showDialog} onOpenChange={setShowDialog}> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle className={'flex items-center space-x-4'}> <RocketIcon className={'h-4'} /> <Trans i18nKey="common:newVersionAvailable" /> </AlertDialogTitle> <AlertDialogDescription> <Trans i18nKey="common:newVersionAvailableDescription" /> </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <Button variant={'outline'} onClick={() => { setShowDialog(false); setDismissed(true); }} > <Trans i18nKey="common:back" /> </Button> <Button onClick={() => window.location.reload()}> <Trans i18nKey="common:newVersionSubmitButton" /> </Button> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> );}function useVersionUpdater(props: { intervalTimeInSecond?: number } = {}) { const interval = VERSION_UPDATER_REFETCH_INTERVAL_SECONDS ? Number(VERSION_UPDATER_REFETCH_INTERVAL_SECONDS) : DEFAULT_REFETCH_INTERVAL; const refetchInterval = (props.intervalTimeInSecond ?? interval) * 1000; // start fetching new version after half of the interval time const staleTime = refetchInterval / 2; return useQuery({ queryKey: ['version-updater'], staleTime, gcTime: refetchInterval, refetchIntervalInBackground: true, refetchInterval, initialData: null, queryFn: async () => { const response = await fetch('/version'); const currentVersion = await response.text(); const oldVersion = version; version = currentVersion; const didChange = oldVersion !== null && currentVersion !== oldVersion; return { currentVersion, oldVersion, didChange, }; }, });}
The implementation above uses:
- Shadcn UI
- React Query
- The
Trans
component fromi18next
Customization
You can customize the interval time by specifying the NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS
environment variable.
NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS=10
The above will query the version endpoint every 10 seconds. Depending on your deployment environment, you may need to adjust this value to ensure optimal performance.
Platform Support
The solution works across various deployment platforms:
- Vercel (native support via VERCEL_GIT_COMMIT_SHA)
- Cloudflare Pages (via CF_PAGES_COMMIT_SHA)
- Custom deployments (via custom GIT_HASH implementation)
Conclusion
Version skew protection is crucial for maintaining application reliability and user experience.
While you can implement this yourself, using a solution like Makerkit provides a battle-tested implementation out of the box, saving development time and ensuring robust version control.
For teams building Next.js applications, Makerkit's built-in version skew protection is just one of many features that make it an excellent choice for starting your SaaS project. It combines best practices with practical implementation, allowing you to focus on building your product rather than solving infrastructure challenges.
💡 A production-ready Next.js SaaS Boilerplate with batteries included
Looking for a Next.js SaaS Starter Kit? Makerkit is one of the most trusted SaaS Starter Kits out there. If you're looking for a Next.js SaaS Starter Kit, Makerkit can help you build a SaaS from scratch.