packages/cli/skills/pikku-websocket/SKILL.md
Use when adding real-time features, WebSocket channels, live updates, chat, or pub/sub to a Pikku app. Covers wireChannel, action routing, auth, EventHub pub/sub, channel middleware, and generated WebSocket client. TRIGGER when: code uses wireChannel, user asks about WebSocket, real-time, live updates, chat, pub/sub, or the generated WebSocket client. DO NOT TRIGGER when: user asks about HTTP/REST (use pikku-http), SSE (use pikku-http with sse: true), or WebSocket deployment specifics (use pikku-deploy-uws).
npx skillsauth add pikkujs/pikku pikku-websocketInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill as an execution checklist, not reference material.
pikku-meta when available; otherwise run the relevant pikku meta ... --json command and inspect only the focused output you need..pikku, node_modules, vendored packages, or broad build artifacts.pikku-verify or pikku all when functions, wirings, schemas, or generated clients may have changed.Wire Pikku functions to WebSocket channels with structured message routing, auth per-action, pub/sub via EventHub, and auto-generated type-safe clients.
pikku info functions --verbose # See existing functions and their types
pikku info tags --verbose # Understand project organization
Follow existing patterns. See pikku-concepts for the core mental model.
wireChannel(config)import { wireChannel } from '@pikku/core/channel'
wireChannel({
name: string, // Channel name (e.g. 'todos')
onConnect: async () => {}, // Called when client connects
onDisconnect: async () => {}, // Called when client disconnects
onMessageWiring: { // Action → function mapping
[actionName: string]: {
func: PikkuFunc,
auth?: boolean, // Override channel-level auth
permissions?: Record<string, PikkuPermission | PikkuPermission[]>,
}
},
channelMiddleware?: PikkuChannelMiddleware[],
})
pikkuChannelMiddleware(fn)import { pikkuChannelMiddleware } from '@pikku/core'
const middleware = pikkuChannelMiddleware(async (services, event, next) => {
// Transform or filter events before/after
await next(event) // Pass modified event, or next(null) to drop
})
addChannelMiddleware(domain, middlewares)addChannelMiddleware('todos', [addTimestamp, filterSensitive])
wireChannel({
name: 'todos',
onConnect: async () => {},
onDisconnect: async () => {},
onMessageWiring: {
create: { func: createTodo },
list: { func: listTodos, auth: false },
},
})
Clients send { action: 'create', data: {...} }. Pikku routes to the matching function.
const authenticate = pikkuFunc({
title: 'Authenticate',
func: async ({ setSession }, { token }) => {
const session = await verifyJWT(token)
setSession(session)
return { success: true }
},
})
wireChannel({
name: 'todos',
onConnect: async () => {},
onDisconnect: async () => {},
onMessageWiring: {
auth: { func: authenticate, auth: false }, // No session required
subscribe: { func: subscribeTodos }, // Session required
create: { func: createTodo },
},
})
Use EventHub for real-time broadcasting across connections:
wireChannel({
name: 'todos',
onConnect: async ({ eventHub, channel }) => {
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 })
eventHub.publish('todos:updated', {
event: 'created',
todo,
})
return { todo }
},
}),
},
},
})
const addTimestamp = pikkuChannelMiddleware(
async ({ logger }, event, next) => {
logger.info({ phase: 'before-send', event })
await next({ ...event, sentAt: Date.now() })
}
)
const filterSensitive = pikkuChannelMiddleware(
async (_services, event, next) => {
if (event.internal) return await next(null) // Drop event
await next(event)
}
)
// Apply globally to a domain
addChannelMiddleware('todos', [addTimestamp, filterSensitive])
// Or inline on wiring
wireChannel({
name: 'todos',
channelMiddleware: [addTimestamp],
onConnect: async () => {},
onDisconnect: async () => {},
onMessageWiring: { ... },
})
After npx pikku prebuild:
import { PikkuWebSocket } from '.pikku/pikku-websocket.gen.js'
const pikku = new PikkuWebSocket(ws)
const todosRoute = pikku.getRoute('todos')
// Send action (type-safe)
const result = await todosRoute.send('create', { text: 'Buy milk' })
// Subscribe to events
todosRoute.subscribe('todos:updated', (data) => {
console.log(data.event, data.todo)
})
// functions/chat.functions.ts
export const authenticate = pikkuFunc({
title: 'Authenticate',
func: async ({ jwt }, { token }, { setSession }) => {
const payload = await jwt.verify(token)
setSession({ userId: payload.userId })
return { success: true }
},
})
export const sendMessage = pikkuFunc({
title: 'Send Message',
func: async ({ db, eventHub }, { text }, { session }) => {
const message = await db.createMessage({
text,
userId: session.userId,
})
eventHub.publish('chat:message', { message })
return { message }
},
})
export const listMessages = pikkuSessionlessFunc({
title: 'List Messages',
func: async ({ db }, { limit }) => {
return { messages: await db.listMessages(limit) }
},
})
// wirings/chat.channel.ts
wireChannel({
name: 'chat',
onConnect: async ({ eventHub, channel }) => {
eventHub.subscribe('chat:message', (data) => {
channel.send(data)
})
},
onDisconnect: async () => {},
onMessageWiring: {
auth: { func: authenticate, auth: false },
send: { func: sendMessage },
history: { func: listMessages, auth: false },
},
})
documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.
development
Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to "support RTL / right-to-left / bidi / mirror the layout", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.
development
Wire i18n into a Pikku frontend (Vite SPA, Vite SSR, or Next.js app-router) with react-i18next + i18next. English by default, every user-facing string goes through a `t()` token, and additional languages are served under `/de` `/es` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to "make this translatable / use tokens / add i18n". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.
development
Use when translating an n8n Code node body into a real Pikku function body. Triggered when the user opens or points at a stub generated by @pikku/n8n-import (look for `STUB — generated from n8n Code node` in the file's JSDoc), or when the user says 'translate this n8n code', 'port this n8n code node', 'finish the codeStub__... function', etc. The stub file is a `pikkuSessionlessFunc` with a Zod input/output, a JSDoc preserving the original n8n JavaScript verbatim, and a `throw new Error('… — implement me')` body.