Topics
A topic is a server→client stream the client opts into. Declare it as a serverToClient entry with subscribe: true:
serverToClient: {
prices: { payload: z.object({ symbol: z.string(), price: z.number() }), subscribe: true },
}Client: subscribe
const sub = client.subscribe('prices', (p) => render(p)) // p is typed
await sub.ready // resolves when the server accepts; rejects if denied/disconnected
sub.unsubscribe()subscribe returns immediately with a Subscription. Await .ready if you need to know the subscription was accepted — it rejects with FORBIDDEN if authorizeSubscribe denies it, or DISCONNECTED if the socket drops first. On reconnect, topics auto re-subscribe.
Server: publish
Topics are server-publish only — clients cannot publish. Publish from anywhere on the server:
srv.forRole('user').publish('prices', { symbol: 'AAPL', price: 192.3 }) // role topic
srv.publish('announce', { msg: 'maintenance at 5pm' }) // shared topic- A topic in a role block is published with
srv.forRole(role).publish(...)and reaches that role's subscribers. - A topic in the shared block is published with
srv.publish(...)and reaches every role's subscribers.
Authorizing subscriptions
Gate private topics with authorizeSubscribe — return false or throw to deny (the client's .ready rejects FORBIDDEN):
const srv = createSocketServer(api, {
server, authenticate,
authorizeSubscribe: async (topic, ctx, conn) => {
if (topic.startsWith('org:')) return ctx.user.orgs.includes(topic.slice(4))
return true
},
})Subscribing to a topic that isn't on the connection's role surface is rejected with NOT_FOUND.
Client → others
Clients can't publish — that's by design. To let a client fan something out, send a request; the handler validates/authorizes, then publishes:
srv.implement({
user: {
setPrice: async ({ symbol, price }, ctx) => {
if (!ctx.user.canTrade) throw new SocketError('FORBIDDEN')
srv.forRole('user').publish('prices', { symbol, price })
return { ok: true }
},
},
})Parameterized topics
Topics are typed by exact contract key. Parameterized names like room:{id} aren't type-inferred yet — use a concrete key and carry the id in the payload (filter client-side), or use a room for server-controlled grouping.
Next: Roles & auth.