Platform API
Available in server hooks, load functions, form actions, API routes, and WebSocket hooks (hooks.ws).
Access it via event.platform in SvelteKit handlers or destructured from the second argument in WebSocket hooks.
platform.publish(topic, event, data, options?)
Send a message to all WebSocket clients subscribed to a topic.
Topic and event names are validated before being written into the JSON envelope - quotes, backslashes, and control characters will throw. This prevents JSON injection when names are built from dynamic values like user IDs. The validation is a single-pass char scan and adds no measurable overhead.
// src/routes/todos/+page.server.js
export const actions = {
create: async ({ request, platform }) => {
const formData = await request.formData();
const todo = await db.createTodo(formData.get('text'));
// Every client subscribed to 'todos' receives this
platform.publish('todos', 'created', todo);
return { success: true };
}
}; Cluster mode relay
In cluster mode, the message is automatically relayed to all other workers. Pass { relay: false } to skip the relay when the message originates from an external pub/sub source (Redis, Postgres LISTEN/NOTIFY, etc.) that already delivers to every process:
// Redis subscriber running on every worker -- relay would cause duplicates
sub.on('message', (channel, payload) => {
platform.publish(channel, 'update', JSON.parse(payload), { relay: false });
}); Message protocol
The adapter uses a JSON envelope format for all pub/sub messages: { topic, event, data }. Control messages from the client store (subscribe, unsubscribe, subscribe-batch) use { type, topic } or { type, topics }.
To avoid JSON-parsing every incoming message, the handler uses a byte-prefix discriminator: control messages start with {"type" (byte 3 is y), while user envelopes start with {"topic" (byte 3 is o). A single byte comparison skips JSON.parse entirely for user messages. Messages over 8 KB are also skipped (generous ceiling for subscribe-batch with many topics, well above any realistic control message).
Topic validation
Topics submitted by clients are validated before being accepted:
- Must be between 1 and 256 characters
- Must not contain control characters (code points below 32)
subscribe-batchaccepts at most 256 topics per message (the client only sends what it was subscribed to before a reconnect)
Topics prefixed with __ are reserved for adapter plugins (presence uses __presence:*, replay uses __replay:*). They are not blocked at the protocol level because plugins subscribe to them from the client, but application code should not use the __ prefix for its own topics.
platform.send(ws, topic, event, data)
Send a message to a single WebSocket connection. Wraps in the same { topic, event, data } envelope as publish().
This is useful when you store WebSocket references (e.g. in a Map) and need to message specific connections from SvelteKit handlers:
// src/hooks.ws.js - store connections by user ID
const userSockets = new Map();
export function open(ws, { platform }) {
const { userId } = ws.getUserData();
userSockets.set(userId, ws);
}
export function close(ws, { platform }) {
const { userId } = ws.getUserData();
userSockets.delete(userId);
}
// Export the map so SvelteKit handlers can access it
export { userSockets }; // src/routes/api/dm/+server.js - send to a specific user
import { userSockets } from '../../hooks.ws.js';
export async function POST({ request, platform }) {
const { targetUserId, message } = await request.json();
const ws = userSockets.get(targetUserId);
if (ws) {
platform.send(ws, 'dm', 'new-message', { message });
}
return new Response('OK');
} You can also reply directly from inside hooks.ws.js using platform.send() or ws.send() with the envelope format:
// src/hooks.ws.js
export function message(ws, { data, platform }) {
const msg = JSON.parse(Buffer.from(data).toString());
// Using platform.send (recommended):
platform.send(ws, 'echo', 'reply', { got: msg });
// Or using ws.send with manual envelope:
ws.send(JSON.stringify({ topic: 'echo', event: 'reply', data: { got: msg } }));
} platform.sendTo(filter, topic, event, data)
Send a message to all connections whose userData matches a filter function. Returns the number of connections the message was sent to.
This is simpler than manually maintaining a Map of connections - no hooks.ws.js needed:
// src/routes/api/dm/+server.js - send to a specific user
export async function POST({ request, platform }) {
const { targetUserId, message } = await request.json();
const count = platform.sendTo(
(userData) => userData.userId === targetUserId,
'dm', 'new-message', { message }
);
return new Response(count > 0 ? 'Sent' : 'User offline');
} // Send to all admins
platform.sendTo(
(userData) => userData.role === 'admin',
'alerts', 'warning', { message: 'Server load high' }
); Performance:
sendToiterates every open connection and runs your filter function against each one. It’s fine for low-frequency operations like sending a DM or notifying admins, but don’t use it in a hot loop. If you’re broadcasting to a known group of users, subscribe them to a shared topic and useplatform.publish()instead - topic-based pub/sub is handled natively by uWS in C++ and doesn’t touch the JS event loop.
Clustering:
sendToiterates the local worker’s connections only. There is no cross-worker relay forsendTo. For cross-worker targeted messaging, preferpublish()with a user-specific topic.
platform.connections
Number of active WebSocket connections:
// src/routes/api/stats/+server.js
import { json } from '@sveltejs/kit';
export async function GET({ platform }) {
return json({ online: platform.connections });
} platform.subscribers(topic)
Number of clients subscribed to a specific topic:
export async function GET({ platform, params }) {
return json({
viewers: platform.subscribers(`page:${params.id}`)
});
} platform.topic(name)
Scoped helper that reduces repetition when publishing multiple events to the same topic.
CRUD methods
Calling .created(), .updated(), or .deleted() is shorthand for platform.publish(name, 'created', data), etc.:
// src/routes/todos/+page.server.js
export const actions = {
create: async ({ request, platform }) => {
const todos = platform.topic('todos');
const todo = await db.create(await request.formData());
todos.created(todo); // shorthand for platform.publish('todos', 'created', todo)
},
update: async ({ request, platform }) => {
const todos = platform.topic('todos');
const todo = await db.update(await request.formData());
todos.updated(todo);
},
delete: async ({ request, platform }) => {
const todos = platform.topic('todos');
const id = (await request.formData()).get('id');
await db.delete(id);
todos.deleted({ id });
}
}; Counter methods
The topic helper also has counter methods for numeric state:
const online = platform.topic('online-users');
online.set(42); // -> { event: 'set', data: 42 }
online.increment(); // -> { event: 'increment', data: 1 }
online.increment(5); // -> { event: 'increment', data: 5 }
online.decrement(); // -> { event: 'decrement', data: 1 } | Method | Event published | Data |
|---|---|---|
.created(data) | created | The data argument |
.updated(data) | updated | The data argument |
.deleted(data) | deleted | The data argument |
.set(value) | set | The value |
.increment(n?) | increment | n (default 1) |
.decrement(n?) | decrement | n (default 1) |
platform.batch(messages)
Publish multiple messages in a single call. Useful when an action updates several topics at once:
platform.batch([
{ topic: 'todos', event: 'created', data: todo },
{ topic: `user:${userId}`, event: 'activity', data: { action: 'create' } },
{ topic: 'stats', event: 'increment', data: { key: 'todos_created' } }
]); Each entry is published with platform.publish(). Cross-worker relay is batched automatically, so this is more efficient than three separate publish() calls from a relay overhead perspective.
Summary
| Method | Description |
|---|---|
publish(topic, event, data, options?) | Broadcast to all subscribers of a topic |
send(ws, topic, event, data) | Send to a single connection |
sendTo(filter, topic, event, data) | Send to connections matching a filter |
connections | Number of active WebSocket connections |
subscribers(topic) | Number of subscribers on a topic |
topic(name) | Scoped helper with .created(), .updated(), .deleted(), .set(), .increment(), .decrement() |
batch(messages) | Publish multiple messages in one call |
Was this page helpful?