0.51.0
April 7, 2026This release makes WebSocket support effectively invisible — add a subscription, run serve(), and you're done. It also fixes the built-in analytics dashboard so it works in every framework adapter, and switches the default URL prefix from /api/rpc to /api.
Zero-config WebSocket in serve()
serve() now detects the runtime (Node / Bun / Deno) and wires the matching crossws adapter automatically. WebSocket is auto-enabled whenever the router contains a subscription — no flags, no manual attachWebSocket, no runtime-specific setup.
import { s } from './rpc/instance'
import { appRouter } from './rpc/router'
s.serve(appRouter, { port: 3000 })
// Same code runs on Node, Bun, and Deno.
// WebSocket upgrades on /_ws are handled automatically if a subscription exists.Opt out or tune via options:
s.serve(appRouter, {
port: 3000,
ws: false, // disable entirely
// or: ws: { compress: true, keepalive: 30 }
})See serve() docs and WebSocket docs.
Every procedure reachable over WebSocket
The $route({ ws: true }) flag is gone. Previously queries and mutations were HTTP-only unless explicitly opted into WS — that restriction is removed. Every procedure in the router is now reachable over WebSocket. Subscriptions are the sole signal that WS should be attached.
WSLink on the client side now:
- Honors
protocol: 'messagepack'for binary-encoded frames - Returns an
AsyncIterableIteratorfor subscriptions so you canfor awaitover streamed values
import { createClient, wsLink } from 'silgi/client'
import type { AppRouter } from './server/router'
const client = createClient<AppRouter>({
link: wsLink({ url: 'ws://localhost:3000/_ws', protocol: 'messagepack' }),
})
// Queries and mutations flow over the same socket
const user = await client.users.byId.query({ id: 1 })
// Subscriptions stream naturally
for await (const event of client.events.stream.subscribe({ topic: 'orders' })) {
console.log(event)
}Auto WebSocket for Nitro / h3
When s.handler() detects subscriptions in the router, it now automatically intercepts the /_ws path and attaches crossws hooks. Nitro (and therefore Nuxt) picks this up without requiring a manual server/routes/_ws.ts file — zero config.
The obsolete examples/nuxt/server/routes/_ws.ts is removed.
Analytics dashboard in every fetch adapter
The built-in analytics dashboard now works in every fetch-based adapter — SvelteKit, Next.js, Astro, Remix, SolidStart. Previously the analytics option was wired into serve() / handler() only; in adapters it was accepted but the dashboard path didn't match after prefix stripping, so hitting /api/analytics returned Procedure not found.
// src/routes/api/[...path]/+server.ts
import { createHandler } from 'silgi/sveltekit'
import { appRouter } from '$lib/server/rpc'
const handler = createHandler(appRouter, {
context: (event) => ({ user: event.locals.user }),
analytics: true,
})
export const GET = handler
export const POST = handlerDashboard at http://localhost:5173/api/analytics, regardless of framework.
What changed under the hood
- New
normalizeAnalyticsPathhelper accepts bothapi/analytics*and bareanalytics*post-strip paths, so the dashboard is reachable no matter whatprefixthe adapter strips. - The dashboard bundle no longer hardcodes absolute
/api/analytics/*URLs. All fetches derive fromwindow.location.pathname, making the bundle mount-path agnostic. - Login HTML derives cookie path and fetch base from
location.pathname.
Breaking: default adapter prefix is now /api
The default prefix in fetch-based adapters was previously /api/rpc (SvelteKit, Next.js, Astro, SolidStart) or /rpc (Remix). It is now /api across all of them.
If you relied on the old default, set prefix explicitly:
const handler = createHandler(appRouter, {
prefix: '/api/rpc', // pre-0.51.0 default
context: (event) => ({ user: event.locals.user }),
})Recommended new convention:
| Framework | Route file | URL |
|---|---|---|
| SvelteKit | src/routes/api/[...path]/+server.ts | POST /api/users/list |
| Next.js | app/api/[...path]/route.ts | POST /api/users/list |
| Astro | src/pages/api/[...path].ts | POST /api/users/list |
| SolidStart | src/routes/api/[...path].ts | POST /api/users/list |
| Remix | app/routes/api.$.tsx | POST /api/users/list |
Breaking: $route({ ws: true }) flag removed
The ws property on $route() was used to mark individual procedures as WebSocket-enabled. That flag is gone — all procedures are reachable over WS when the transport is attached, and subscriptions alone decide whether WS is attached.
If you had procedures marked with $route({ ws: true }), just drop the flag:
// Before
const createOrder = s
.$route({ method: 'POST', ws: true })
.$input(schema)
.$resolve(({ input }) => createOrderImpl(input))
// After
const createOrder = s
.$route({ method: 'POST' })
.$input(schema)
.$resolve(({ input }) => createOrderImpl(input))Internal refactor: wrapHandler()
Analytics and Scalar wrapping logic was duplicated across silgi.ts, serve.ts, and _fetch-adapter.ts. Consolidated into a single wrapHandler() in core/handler.ts — adding new wrappers now requires a single change.
Smaller changes
createWSHooksis now internal (_createWSHooks);attachWebSocket,serve, andhandlerare the only public WS entry points.- All adapter JSDoc examples show
analytics: trueand the/api/[...path]route pattern. - Docs pages for every adapter (SvelteKit, Next.js, Astro, SolidStart, Remix, Express, AWS Lambda, NestJS) updated to the new
/apiconvention. FetchAdapterConfigWithEventnow documents theprefixoption.- New test coverage:
test/client/ws-link.test.ts(flag-less query + subscription iterator). - Lint pass:
Array#toSorted()inalerts.ts.