Introduction

Nuxt Skew Protection v2 supports Nuxt 4.6 and Nuxt 5. It uses Nuxt's native polling by default, so no tab holds a persistent connection.

v2 ships in Nuxt SEO v6. Nuxt SEO v6 installs Skew Protection by default.

Check the breaking changes before you upgrade. For a guided migration, use the LLM upgrade prompt with Claude or ChatGPT.

Included in Nuxt SEO v6

@nuxtjs/seo v6 installs this module with two default overrides:

const nuxtSeoDefaults = { skewProtection: { updateStrategy: 'polling', cookie: false } }
  • updateStrategy: 'polling' is the same as the v2 default.
  • cookie: false sets no version cookie. Without the cookie, isClientOutdated(event) always returns false.

Your own skewProtection options take precedence. To remove the module from Nuxt SEO, set skewProtection: false.

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/seo'],
  skewProtection: false,
})

Asset retention needs build history. Keep node_modules/.cache/nuxt-seo/skew-protection between CI builds, or configure a shared storage driver.

Polling by Default

v1 selected the update strategy from the Nitro preset. Most server presets used Server-Sent Events (SSE), so every open tab held a connection.

v2 uses Nuxt's native manifest polling on every platform. Nuxt checks /_nuxt/builds/latest.json once per hour by default. The module adds no polling timer. (#83)

For instant updates, select a push strategy:

nuxt.config.ts
export default defineNuxtConfig({
  skewProtection: {
    updateStrategy: 'sse', // or 'ws', pusherAdapter(), ablyAdapter()
  },
})

One Push Connection per Browser

If you select a push strategy, visible tabs share one connection. Web Locks select the owner tab without heartbeat timers. (#83)

  • Hidden tabs release ownership and request no connection.
  • If the owner tab closes or hides, a waiting visible tab takes the connection.
  • Every tab still receives version updates through BroadcastChannel.

Browsers without Web Locks or BroadcastChannel keep one connection per tab. connectionTracking: true also keeps one connection per tab.

Nuxt 4.6 and Nuxt 5

v2 uses portable server APIs from nuxt/server and native fetch. It adds typed entries for app and server functions. (#77)

import { useSkewProtection } from '#skew-protection/app'
import { isClientOutdated } from '#skew-protection/server'

Auto imports still work. The nuxt-skew-protection/server alias also still works.

Fewer Dependencies

v2 removes the ofetch, pkg-types, and tinyexec dependencies. nuxt-site-config and @nuxtjs/robots become optional peers. (#77)

v2 also removes the zod dependency. The Pusher and Ably adapters parse their config without it, so pusherConfigSchema and ablyConfigSchema become parsePusherConfig and parseAblyConfig. A custom adapter that imports zod must install it. (#94)

Since v1

These changes shipped in v1.5.0 to v1.6.2. Nuxt SEO v6 users get them for the first time.

  • v1.5.0 records chunk metadata without asset bundling. (#43)
  • v1.5.0 turns off asset storage when native Vercel Skew Protection is active. (#44)
  • v1.5.0 keeps adapter secrets out of client bundles. (#38)
  • v1.6.0 loads custom realtime adapters through defineAdapter. See External Providers. (#68)
  • v1.6.0 ships an Agent Skill in the package. (#62)
  • v1.6.0 makes reloadStrategy: 'idle' wait for user inactivity. (#69)
  • v1.6.2 keeps existing service workers, such as a PWA worker. (#75)

Breaking Changes

Nuxt 4.6 and Node 22

v2 requires Nuxt ^4.6.0 || ^5.0.0. v1 accepted Nuxt >=4.0.0.

v2 requires Node ^22.22.3 || ^24.15.0 || >=26.0.0. (#77)

Default updateStrategy Is 'polling'

If you do not set updateStrategy, v2 polls on every platform. Open tabs see a new deployment within the Nuxt check interval. (#83)

v1 used 'sse' on most server presets and 'ws' on cloudflare-durable. To keep push updates, set the strategy explicitly.

// Before (v1): SSE on Node.js, Vercel, and Netlify without config
export default defineNuxtConfig({
  modules: ['nuxt-skew-protection'],
})

// After (v2): select SSE explicitly
export default defineNuxtConfig({
  modules: ['nuxt-skew-protection'],
  skewProtection: {
    updateStrategy: 'sse', // use 'ws' on cloudflare-durable
  },
})

connectionTracking and useActiveConnections() need 'sse' or 'ws'. If you use them, set updateStrategy explicitly.

To keep polling with faster checks, shorten the Nuxt interval:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    checkOutdatedBuildInterval: 5 * 60 * 1000, // 5 minutes
  },
})

experimental.appManifest: false Throws

The module needs deployment manifests for its asset metadata. If you disable the Nuxt app manifest, v2 stops the build. (#83)

nuxt-skew-protection requires experimental.appManifest. Enable it or disable the module.
// Before (v1): the module ran without the app manifest
export default defineNuxtConfig({
  experimental: { appManifest: false },
})

// After (v2): remove the override, or disable the module
export default defineNuxtConfig({
  skewProtection: { enabled: false },
})

Shared Push Connections

If you select a push strategy, tabs share one connection by default. This changes the state each tab sees. (#83)

  • Hidden tabs pause their subscription.
  • Connection ids, statistics, and send() calls belong to the owner tab.
  • isConnected is true in visible subscribed tabs that do not own the connection.

To keep one connection per tab, set multiTab: false or enable connectionTracking.

// Before (v1): one SSE connection per tab
const before = { skewProtection: { updateStrategy: 'sse' } }

// After (v2): opt out of the shared connection
const after = { skewProtection: { updateStrategy: 'sse', multiTab: false } }

Default Storage Driver Is fs-lite

The default storage driver changes from fs to fs-lite. The base directory and the file paths stay the same. (#77)

// Before (v1) default
const before = { storage: { driver: 'fs', base: 'node_modules/.cache/nuxt-seo/skew-protection' } }

// After (v2) default
const after = { storage: { driver: 'fs-lite', base: 'node_modules/.cache/nuxt-seo/skew-protection' } }

To keep the fs driver, set storage.driver: 'fs'.

Robots and Site Config Are Optional

v1 installed @nuxtjs/robots (>=5.6.7) and nuxt-site-config for you. v2 makes both optional peers: Robots ^7.0.0 and Site Config ^5.0.0. (#77)

  • Without Nuxt Robots, SSE and WebSocket connections do not filter bot traffic.
  • Without Site Config, debug output uses the request origin.

To keep bot filtering, install Nuxt Robots:

npx nuxt module add @nuxtjs/robots

@nuxtjs/seo installs both modules, so Nuxt SEO users need no change.

DevTools Panel Needs nuxtseo-devtools-host

The DevTools panel loads from the shared nuxtseo-devtools-host package. Without it, the Skew Protection tab shows an install button. (#77)

pnpm add -D nuxtseo-devtools-host

Nitro Hook Payload Types

The skew:connection:* and skew:subscribe-stats payloads gain an optional peer. The event field of skew:subscribe-stats and skew:authorize-stats also accepts RequestEvent from nuxt/server. (#77)

If a typed hook handler expects only H3Event, widen its type.

LLM Upgrade Prompt

Paste this into Claude, ChatGPT, or any LLM to migrate your codebase.

Upgrading

If you use @nuxtjs/seo, upgrade it to v6. It includes Skew Protection v2.

# pnpm
pnpm add nuxt-skew-protection@^2

# npm
npm install nuxt-skew-protection@^2

# yarn
yarn add nuxt-skew-protection@^2

Changelog

Compare changes

Enhancements

  • Prepare Skew Protection 2 for Nuxt 4.6 (#77)

Performance

  • Default to polling and share push connections (#83)

Fixes

  • Align skill metadata with the Agent Skills spec (#90)

Documentation

  • Add the module icon and unify the README badges (#78)
  • Update the Agent Skill for v1.6.2 (#79)
  • Rewrite passive voice (#87)
  • Replace an unsupported button color (#88)

Chore

  • Schedule weekly package maintenance (#80)
  • Declare the playground as an ES module (#89)

Contributors

Was this page helpful?