The Basics
Connect, route, respond
Define your functions once, wire them to a channel. Pikku routes incoming messages by action name.
const createTodo = pikkuFunc({
title: 'Create Todo',
description: 'Create a new todo item',
func: async ({ db }, { text }) => {
const todo = await db.createTodo({ text })
return { todo }
},
permissions: { user: isAuthenticated }
})
wireChannel({
domain: 'todos',
onConnect: async () => {},
onDisconnect: async () => {},
onMessageWiring: {
create: { func: createTodo },
list: { func: listTodos, auth: false },
}
})
Action-based routing
Messages include an action key — Pikku routes to the right function automatically
Lifecycle hooks
onConnect and onDisconnect let you set up and tear down per-connection state
Same auth system
Permissions, sessions, and middleware work identically to HTTP wiring
Action Routing
One channel, many actions
Every message carries an action key. Pikku strips it from the data, routes to the right function, and re-adds it to the response.
Client sends
{ action: "create", text: "Buy milk" }Pikku routes to
createTodo({ text: "Buy milk" })Response sent
{ action: "create", todo: { ... } }wireChannel({
domain: 'todos',
onConnect: async () => {},
onDisconnect: async () => {},
onMessageWiring: {
auth: { func: authenticate, auth: false },
subscribe: { func: subscribeTodos },
list: { func: listTodos },
create: { func: createTodo },
}
})
Routing key stripped from data. Your function receives { text: "Buy milk" } — not the raw message. The action key is re-added to the response automatically.
Auth
Authenticate once, session everywhere
Send an auth message over the WebSocket, set the session, and every subsequent action has access to it.
Session via setSession
Call setSession() inside any action to establish an authenticated session for the connection.
Per-action auth override
Set auth: false on individual actions like authenticate. Everything else requires a valid session by default.
Auto propagation
Once the session is set, every subsequent message on that connection automatically carries the session — no re-auth needed.
const authenticate = pikkuFunc({
title: 'Authenticate',
func: async ({ setSession }, { token }) => {
const session = await verifyJWT(token)
setSession(session)
return { success: true }
}
})
wireChannel({
domain: 'todos',
onConnect: async () => {},
onDisconnect: async () => {},
onMessageWiring: {
// No auth required — this is how you log in
auth: { func: authenticate, auth: false },
// These require a session (default)
subscribe: { func: subscribeTodos },
create: { func: createTodo },
}
})
Pub/Sub
Broadcast with EventHub
Subscribe connections to topics on connect. When one client publishes, all subscribers receive the update in real time.
Client A
subscribe("todos:updated")Client B
publish("todos:updated")Client C
subscribe("todos:updated")wireChannel({
domain: 'todos',
onConnect: async ({ eventHub, channel }) => {
// Subscribe this connection to a topic
eventHub.subscribe('todos:updated', (data) => {
channel.send(data)
})
},
onDisconnect: async () => {},
onMessageWiring: {
create: {
func: pikkuFunc({
title: 'Create Todo',
func: async ({ db, eventHub }, { text }) => {
const todo = await db.createTodo({ text })
// Broadcast to all subscribers
eventHub.publish('todos:updated', {
event: 'created',
todo
})
return { todo }
}
})
},
}
})
Stateful or serverless. EventHub works in-process for stateful servers (uWebSockets.js) and backs onto PostgreSQL for serverless deployments (AWS Lambda + API Gateway).
Type-Safe Client
Full IntelliSense on the wire
Pikku generates a typed WebSocket client from your channel wirings. Every action, every payload, every subscription — autocompleted.
import { PikkuWebSocket } from '.pikku/pikku-websocket.gen.js'
const pikku = new PikkuWebSocket(ws)
// Get a typed route — action name is autocompleted
const todosRoute = pikku.getRoute('todos')
// Typed send — input and output inferred from your func
const result = await todosRoute.send('create', {
text: 'Buy milk'
})
// Typed subscribe — callback payload matches publish type
todosRoute.subscribe('todos:updated', (data) => {
console.log(data.event, data.todo)
})
Generated from wirings
PikkuWebSocket is auto-generated with typed overloads for every channel action you've wired.
Typed send & subscribe
route.send() infers input/output per action. route.subscribe() types the callback payload from your EventHub events.
Works everywhere
Built on the standard WebSocket API — works in the browser, Node, React Native, or anywhere with a WebSocket implementation.
Middleware
Hooks at every level
Two types of middleware: wire middleware wraps function calls, channel middleware wraps outbound messages via channel.send().
Wire middleware
funcWraps each function call. Apply per-channel or per-action — rate limiting, audit logging, validation. Same model as HTTP middleware.
Channel middleware
sendWraps channel.send() — intercepts every outbound event. Transform payloads, filter events (pass null to drop), or fan out (pass an array).
import { pikkuChannelMiddleware } from '@pikku/core'
// Channel middleware intercepts channel.send()
// Signature: (services, event, next) => void
const addTimestamp = pikkuChannelMiddleware(
async ({ logger }, event, next) => {
logger.info({ phase: 'before-send', event })
// Transform the event before it reaches the client
await next({ ...event, sentAt: Date.now() })
}
)
// Drop events by passing null to next()
const filterSensitive = pikkuChannelMiddleware(
async (_services, event, next) => {
if (event.internal) return await next(null)
await next(event)
}
)
// Apply to channel via tag or inline
addChannelMiddleware('todos', [addTimestamp, filterSensitive])
// Or inline on the wireChannel config
wireChannel({
domain: 'todos',
channelMiddleware: [addTimestamp],
// ...
})
Channel middleware controls what the client sees. Pass a modified event to next() to transform, null to drop, or an array to fan out into multiple events. Both types run in onion order.
Deploy
Stateful or serverless
The same wireChannel code runs on both. Your functions don't change — only the runtime does.
Stateful
uWebSockets.js, ws
In-process EventHub, persistent connections. Best for low-latency real-time apps.
Serverless
AWS Lambda, Cloudflare
PostgreSQL-backed EventHub, managed WebSocket API. Best for scale-to-zero workloads.
Start wiring WebSockets in 5 minutes
One command to scaffold a project with WebSocket wiring already configured.
MIT Licensed · Works with uWebSockets.js, ws, AWS Lambda & Cloudflare