Upgrade Quickstart - 0.4 to 0.5
The full migration story has ~80 numbered breaking changes across three packages. Most apps only need to do five things.
If you hit something this page doesn’t cover, jump to Migration 0.4 to 0.5 for the executive summary or the per-package MIGRATION.md links at the bottom.
1. Bump three packages together
- "svelte-adapter-uws": "^0.4.x",
- "svelte-realtime": "^0.4.x",
- "svelte-adapter-uws-extensions": "^0.4.x"
+ "svelte-adapter-uws": "^0.5.0",
+ "svelte-realtime": "^0.5.0",
+ "svelte-adapter-uws-extensions": "^0.5.0" - npm install uNetworking/uWebSockets.js#v20.60.0
+ npm install uNetworking/uWebSockets.js#v20.67.0 - "engines": { "node": ">=20.0.0" }
+ "engines": { "node": ">=22.0.0" } The peer-dep chain rejects mismatched versions with an install warning, so do all three at once. The @sveltejs/kit peerDep stays at ^2.0.0 for compatibility.
2. Export unsubscribe from hooks.ws.js
- export { message, close } from 'svelte-realtime/server';
+ export { message, close, unsubscribe } from 'svelte-realtime/server'; The unsubscribe hook fires when a client drops a topic without disconnecting. Without re-exporting it, presence and replay state for those topics leaks until the connection closes.
If you use the Redis presence extension, also update the destructure:
- export const { subscribe, close } = presence.hooks;
+ export const { subscribe, unsubscribe, close } = presence.hooks; 3. Audit async access predicates
In 0.4, async access / filter / live.gate predicates and async subscribe / subscribeBatch hooks silently allowed every request. The framework compared Promise<false> against false, which is always a truthy-vs-falsy mismatch, so the deny branch was unreachable. 0.5 fixes this; predicates now correctly await.
This means streams that were silently open in 0.4 will start denying in 0.5. That is the framework now correctly doing its job, but it will surface as “users who could load this page yesterday get FORBIDDEN today” if your access logic ever rejected anyone.
Audit checklist:
// Anywhere you have one of these patterns:
live.stream('topic', loader, { access: async (ctx) => /* ... */ })
live.stream('topic', loader, { filter: async (ctx, event, data) => /* ... */ })
live.gate(async (ctx) => /* ... */)
live.access.any(asyncCheck1, asyncCheck2) // composition layer also fixed
live.access.all(asyncCheck1, asyncCheck2)
// In hooks.ws.js:
export async function subscribe(ws, topic, ctx) { /* ... */ }
export async function subscribeBatch(ws, topics, ctx) { /* ... */ } Test each one with both an allowing user and a denying user. The deny path was the broken one.
The leaf helpers (live.access.owner, live.access.team, live.access.role, live.access.org, live.access.user) are sync and unchanged.
4. Plan idempotency cache invalidation if you use it
If your app uses live.idempotent or the extensions task runner, 0.5 namespaces the cache key as 'rpc:' + path + ':' + userKey (and 'task:' + name + ':' + idempotencyKey for tasks). This closes a privilege-escalation bug where one RPC’s cached result could be replayed under another RPC’s name.
In-flight cache entries from before the upgrade become invisible after deploy, because the namespaced key does not match the old un-namespaced key. Pick one:
- Low-traffic window deploy. Schedule the upgrade when nobody is mid-retry. Acceptable for most apps.
- Accept handler re-runs. Any in-flight retry that lands on the new build re-runs the handler. Fine if handlers are idempotent at the database layer.
- Wait it out. Redis stores TTL old keys within ~48 hours by default. After that, only namespaced keys exist.
If you do not use live.idempotent or the task runner, skip this step.
5. Plan the Postgres ws_* -> svti_* rename if you use Postgres extensions
If your app uses any of createReplay, createIdempotencyStore, createJobQueue, or createTaskRunner from the Postgres extensions and your tables already exist from a 0.4 deployment, the new defaults are svti_* instead of ws_*. Pick one:
Option A (zero-downtime, no SQL): pin the old names.
createReplay(pg, { table: 'ws_replay' });
createIdempotencyStore(pg, { table: 'ws_idempotency' });
createJobQueue(pg, { table: 'ws_jobs' });
createTaskRunner(pg, { table: 'ws_tasks' }); Option B (rename in place): see the SQL block in the extensions MIGRATION.md.
Fresh deployments get svti_* automatically; no action.
Anything else?
The above covers what 80% of apps actually need. The other 20% are scenarios that might apply:
- Native (non-browser) clients hitting
/__ws/authmust now stampx-requested-with: XMLHttpRequestorSec-Fetch-Site: same-origin(CSRF defense). Bundled browser client does this automatically. - Apps using
allowedOrigins: 'same-origin'withoutORIGINenv orHOST_HEADERenv or native TLS now throw at startup. Set one of those env vars, or opt out viawebsocket.unsafeSameOriginWithoutHostPin: trueafter auditing. - Hand-rolled WebSocket clients decoding
__presence:{topic}frames must swap from the four-event format (list/join/leave/updated) to the two-event diff format (presence_state/presence_diff). The bundledpresence()Svelte store handles this transparently. - Tests asserting
$status === 'closed'must switch to$status === 'failed'or$status === 'disconnected'depending on intent. - Apps reading
$messages?.errorpatterns must switch to the separatemessages.errorReadablestore.
For everything else, see the full migration documents:
- Migration 0.4 to 0.5 - executive summary
- svelte-realtime/MIGRATION.md - tiered, 30+ items
- svelte-adapter-uws/MIGRATION.md - tiered, 28 items
- svelte-adapter-uws-extensions/MIGRATION.md - tiered, 21 items
- What’s new in 0.5 - feature tour for new adopters (if you are upgrading you have probably already read this)
Sanity check
After the bump, run:
npm install
npm run build
npm test # if you have a test suite Boot your dev server and exercise the auth-gated paths. If 0.5 surfaces a new FORBIDDEN somewhere, your 0.4 build was leaking and now it isn’t. Treat it as a bug found, not a regression.
If everything builds and tests pass, you are done. Ship it.
Was this page helpful?