v2.0.0
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: falsesets no version cookie. Without the cookie,isClientOutdated(event)always returnsfalse.
Your own skewProtection options take precedence. To remove the module from Nuxt SEO, set skewProtection: false.
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:
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:
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. isConnectedistruein 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-hostNitro 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.
# Upgrade Guide: nuxt-skew-protection v1 to v2.0.0
You are helping me upgrade nuxt-skew-protection from v1 to v2.0.0 in my Nuxt project.
## Requirements
- Nuxt `^4.6.0 || ^5.0.0`
- Node `^22.22.3 || ^24.15.0 || >=26.0.0`
If my project uses `@nuxtjs/seo`, upgrade `@nuxtjs/seo` to v6 instead. It installs this module with `updateStrategy: 'polling'` and `cookie: false`.
## Breaking Changes
### 1. Default update strategy is polling
**What changed:** An unset `updateStrategy` polls on every platform. v1 used SSE on most server presets and WebSockets on `cloudflare-durable`.
**Search my codebase for:** `skewProtection`, `connectionTracking`, `useActiveConnections`
**Regex pattern:** `skewProtection\s*:|connectionTracking|useActiveConnections`
**Before:**
```ts
skewProtection: { connectionTracking: true }
```
**After:**
```ts
skewProtection: { updateStrategy: 'sse', connectionTracking: true } // 'ws' on cloudflare-durable
```
**Action:** If the app needs instant updates or uses connection tracking, set `updateStrategy` explicitly. Otherwise, make no change.
### 2. `experimental.appManifest: false` throws
**What changed:** The build fails if the project disables the Nuxt app manifest.
**Search my codebase for:** `appManifest`
**Regex pattern:** `appManifest\s*:\s*false`
**Before:**
```ts
experimental: { appManifest: false }
```
**After:**
```ts
// remove the override, or disable the module:
skewProtection: { enabled: false }
```
### 3. Tabs share push connections
**What changed:** With a push strategy and `multiTab: true` (default), visible tabs share one connection. Hidden tabs pause. Connection ids, statistics, and `send()`{lang="ts"} belong to the owner tab.
**Search my codebase for:** `$skewConnection`, `skew:connection:open`, `isConnected`
**Regex pattern:** `\$skewConnection|skew:connection:|isConnected`
**Action:** If code needs one connection per tab, set `multiTab: false` or `connectionTracking: true`.
```ts
skewProtection: { updateStrategy: 'sse', multiTab: false }
```
### 4. Default storage driver is `fs-lite`
**What changed:** The default driver changes from `fs` to `fs-lite`. The base directory stays `node_modules/.cache/nuxt-seo/skew-protection`.
**Search my codebase for:** `driver: 'fs'`
**Regex pattern:** `driver:\s*['"]fs['"]`
**Action:** Make no change. Set `storage.driver: 'fs'` to keep the old driver.
### 5. Robots and Site Config are optional peers
**What changed:** The module no longer installs `@nuxtjs/robots` or `nuxt-site-config`. Without Robots, connections do not filter bot traffic.
**Search my codebase for:** `@nuxtjs/robots` in `package.json` and `nuxt.config.ts`
**Action:** To keep bot filtering, run `npx nuxt module add @nuxtjs/robots`. Use Robots v7 and Site Config v5.
### 6. DevTools panel needs a host package
**What changed:** The DevTools panel loads from `nuxtseo-devtools-host`.
**Action:** If I use the panel, run `pnpm add -D nuxtseo-devtools-host`.
### 7. Nitro hook payload types
**What changed:** `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`.
**Search my codebase for:** `skew:authorize-stats`, `skew:subscribe-stats`
**Regex pattern:** `skew:(authorize|subscribe)-stats`
**Action:** If a handler types `event` as `H3Event` only, widen the type.
## Verification Checklist
1. `pnpm install` (update lockfile)
2. `npx nuxt typecheck` (catch type errors)
3. `pnpm build` (check that the build does not throw on `appManifest`)
4. `pnpm test` (catch runtime regressions)
5. Deploy twice, then check that `/_nuxt/builds/latest.json` lists both versions
---
Please scan my codebase for all affected patterns listed above and generate a complete migration plan. For each file that needs changes, show me the exact diff.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@^2Changelog
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)