Write your backend once. Pikku wires it to HTTP, WebSocket, queues, cron, AI agents, workflows and more — same auth, same validation, zero rewrites.


// Same logic, copied per protocol
app.get('/cards/:id', auth, validate, async (req, res) => {
const card = await db.getCard(req.params.id)
res.json(card)
})
ws.on('getCard', auth, validate, async (msg, socket) => {
const card = await db.getCard(msg.cardId) // <- again
socket.send(JSON.stringify(card))
})
// + queue, cron, CLI, RPC, SSE... each drifts.
// --- Workflow? Add Inngest / Temporal ----
inngest.createFunction({ id: 'onboarding' }, ...,
async ({ step }) => {
await step.run('create-profile', () => createProfile(id))
await step.sleep('wait', '5m')
await step.run('send-welcome', () => sendWelcome(id))
})
// New SDK, new schema, new deploy pipeline.
// --- AI agent? Add Vercel AI / LangChain -
const tools = { getCard: tool({
parameters: z.object({ cardId: z.string() }),
execute: async ({ cardId }) => db.getCard(cardId),
})} // Auth? Permissions? You're on your own.
// Three frameworks. Three auth layers. One backend.
// With Pikku — write it once
const getCard = pikkuFunc({
func: async ({ db, audit }, { cardId }) => {
const card = await db.getCard(cardId)
await audit.log('getCard', { cardId })
return card
},
permissions: { user: isAuthenticated }
})
// Wire it to anything — same auth, same logic
wireHTTP({ method: 'get', route: '/cards/:cardId', func: getCard })
wireChannel({ name: 'cards', onMessage: { getCard } })
wireQueueWorker({ queue: 'fetch-card', func: getCard })
wireCLI({ program: 'cards', commands: { get: getCard } })
// Workflows just reference your functions
const onboarding = pikkuWorkflowFunc(
async ({}, { userId }, { workflow }) => {
await workflow.do('Create profile', 'createProfile', { userId })
await workflow.sleep('Wait 5 min', '5m')
await workflow.do('Send welcome', 'sendWelcome', { userId })
}
)
// AI agents too — same functions, same auth
const support = pikkuAgent({
tools: [getCard, getOrders, createTicket],
model: 'claude-sonnet-4-5'
})
// Auth, permissions, and validation carry over. Done.
const getCard = pikkuFunc({
title: 'Get Card',
description: 'Retrieve a card by ID',
func: async ({ db, audit }, { cardId }) => {
const card = await db.getCard(cardId)
await audit.log('getCard', { cardId })
return card
},
permissions: { user: isAuthenticated }
})


wireHTTP({
method: 'get',
route: '/cards/:cardId',
func: getCard
})
No adapters. No schema writing. No separate auth layer. Pass your existing Pikku functions directly — the agent gets your full backend.
// These already exist in your backend — no changes needed
import { getCustomer, getOrders, createTicket } from './functions'
export const supportAgent = pikkuAgent({
name: 'support',
instructions: `You are a helpful support agent.
Look up the customer's account and recent orders.`,
tools: [getCustomer, getOrders, createTicket],
model: 'claude-sonnet-4-5'
})
// Wire it just like any HTTP endpoint
wireHTTP({
method: 'post',
route: '/api/chat',
func: supportAgent
})
Pass any Pikku function as a tool — the agent inherits its type signature, description, and input schema automatically.
Agents inherit the caller's session, permissions, and middleware. The rules that protect your HTTP endpoints protect every tool the agent can call.
Bring OpenAI, Anthropic, or any provider. Pikku handles tool calling, streaming, and context — you just swap the model name.
Write sequential logic like normal code. Pikku handles persistence, retries, and resumption — even across server restarts.
Completed steps are cached and never re-executed. A workflow that fails on step 4 resumes from step 4 — not from the beginning.
workflow.sleep('5min') suspends execution without holding a server connection. Perfect for trial expirations, reminders, and follow-ups.
State is persisted between steps. Deploy a new version mid-workflow and execution continues from where it left off.
export const onboardingWorkflow = pikkuWorkflowFunc(
async ({ workflow }, { email, userId }) => {
// Each step is persisted — safe to retry
const user = await workflow.do(
'Create user profile',
'createUserProfile',
{ email, userId }
)
await workflow.do(
'Add to CRM',
async () => crm.createUser(user)
)
// Suspend for 5 minutes — no server held
await workflow.sleep('Wait before welcome email', '5min')
await workflow.do(
'Send welcome email',
'sendEmail',
{ to: email, template: 'welcome' }
)
return { success: true }
}
)
Stripe billing. SendGrid emails. One wireAddon() call each. Install, configure secrets, call via namespaced RPC — fully typed.
// One line per addon
wireAddon({
name: 'stripe',
package: '@pikku/addon-stripe'
})
wireAddon({
name: 'email',
package: '@pikku/addon-sendgrid',
secretOverrides: {
SENDGRID_API_KEY: 'MY_EMAIL_KEY'
}
})
// Call addon functions via namespaced RPC
const checkout = pikkuFunc({
func: async ({}, { plan }, { rpc }) => {
const session = await rpc.invoke(
'stripe:checkoutCreate',
{ plan, currency: 'usd' }
)
await rpc.invoke(
'email:mailSend',
{ to: session.email, template: 'receipt' }
)
return { url: session.url }
}
})
Install a package, add one wireAddon() call, and its functions appear as namespaced RPC calls. No glue code, no adapters.
The CLI generates TypeScript definitions for every addon function — rpc.invoke('stripe:checkoutCreate', …) autocompletes with exact input and output types.
Addons declare what secrets they need. You map them to your own infrastructure with secretOverrides.
Addons reuse your existing logger, database, and services. Each addon gets its own namespace, so nothing collides.
WhatsApp, Slack, Telegram, WebChat — write one function. The adapter normalizes every platform into the same message format.
WhatsApp challenges, Slack url_verification, Telegram tokens — handled by the adapter, invisible to your code.
Every platform delivers the same message shape — senderId, text, attachments, metadata. Your handler never knows which platform sent it.
Webhook for cloud APIs, WebSocket for browser chat widgets, listener for persistent connections (Baileys, Signal CLI, Matrix).
Rate limiting, logging, permissions — your existing middleware works on gateways too.
// Webhook — platform POSTs to you
wireGateway({
name: 'whatsapp',
type: 'webhook',
route: '/webhooks/whatsapp',
adapter: whatsAppAdapter,
func: handleMessage,
})
// WebSocket — real-time web chat
wireGateway({
name: 'webchat',
type: 'websocket',
route: '/chat',
adapter: webChatAdapter,
func: handleMessage,
})
// One handler for all platforms
const handleMessage = pikkuFunc({
func: async ({ database, logger }, { senderId, text }) => {
logger.info(`${senderId}: ${text}`)
await database.saveMessage(senderId, text)
// Return value is auto-sent via the adapter
return { text: `Got it! You said: ${text}` }
}
})
The same code runs on Express, Fastify, AWS Lambda, Cloudflare Workers, Next.js and more. Switching runtimes never touches your functions.
Plus any custom runtime via the adapter interface. Build your own →
Auto-generated HTTP, WebSocket, and RPC clients with full IntelliSense.
Cookie, bearer, API key auth with fine-grained permissions — built in.
Singleton and per-request dependency injection, type-safe and testable.
Before/after hooks for logging, metrics, tracing — across all protocols.
Runtime validation against TypeScript input schemas. Supports Zod.
Standard TypeScript, tiny runtime, MIT licensed. Bring your own everything.
Browse functions, run agents, manage secrets, and trigger workflows — without writing tooling code.

"So many places in my code base have like three entry points: CLI, public (sometimes protected) HTTP API and internally from within the API. Would be so nice having everything just an invoke away. With Nest it's a pain because you basically have to start the whole API up just to run CLI command."
"Ever been annoyed at having to write your code different in a Lambda than in an express handler? Pikku fixes that."
Write it once. Pikku wires it everywhere.