Skip to main content
Wire Type: WebSocket

Real-time channels,
same functions.

wireChannel gives bidirectional messaging with action routing, pub/sub, and per-message auth — all using your existing Pikku functions.

The Basics

Connect, route, respond

Define your functions once, wire them to a channel. Pikku routes incoming messages by action name.

createTodo.func.tsfunc.ts
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 }
})
todos.channel.tschannel.ts
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: { ... } }
todos.channel.ts
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.

todos.channel.ts
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")
subscriber

Client B

publish("todos:updated")
publisher

Client C

subscribe("todos:updated")
subscriber
todos.channel.ts
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.

client.tsauto-generated types
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

func

Wraps each function call. Apply per-channel or per-action — rate limiting, audit logging, validation. Same model as HTTP middleware.

Channel middleware

send

Wraps channel.send() — intercepts every outbound event. Transform payloads, filter events (pass null to drop), or fan out (pass an array).

channel-middleware.tschannel middleware
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.

$ npm create pikku@latest

MIT Licensed  ·  Works with uWebSockets.js, ws, AWS Lambda & Cloudflare