Skip to main content
For developers · the technical deep-dive

One function. Every wiring.

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

HTTP
WebSocket
SSE
Queue
Cron
RPC
MCP
CLI
AI Agent
Workflow
Trigger
Gateway
The difference

Four handlers that drift apart — or one function that doesn't.

Without Pikkurepeated + fragile
everything.ts
// 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 Pikku1 function + wirings
cards.ts
// 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.
Same function, any transport

Pick a protocol. The function never changes.

Write once
getCard.ts
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 }
})
Same auth & permissions across all protocols
One place to fix bugs and add features
Type-safe inputs and outputs everywhere
Pick a protocol
HTTP API
Wiring code
wiring.tsHTTP API
wireHTTP({
method: 'get',
route: '/cards/:cardId',
func: getCard
})
AI agents

Your functions are already agent tools.

No adapters. No schema writing. No separate auth layer. Pass your existing Pikku functions directly — the agent gets your full backend.

src/agents/support.agent.ts
// 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
})
Zero glue code

Pass any Pikku function as a tool — the agent inherits its type signature, description, and input schema automatically.

Auth follows the agent

Agents inherit the caller's session, permissions, and middleware. The rules that protect your HTTP endpoints protect every tool the agent can call.

Any LLM, same interface

Bring OpenAI, Anthropic, or any provider. Pikku handles tool calling, streaming, and context — you just swap the model name.

Read the AI Agents docs
Workflows

Multi-step processes that survive anything.

Write sequential logic like normal code. Pikku handles persistence, retries, and resumption — even across server restarts.

Deterministic replay

Completed steps are cached and never re-executed. A workflow that fails on step 4 resumes from step 4 — not from the beginning.

Sleep for hours, days, or weeks

workflow.sleep('5min') suspends execution without holding a server connection. Perfect for trial expirations, reminders, and follow-ups.

Survives restarts

State is persisted between steps. Deploy a new version mid-workflow and execution continues from where it left off.

Read the Workflows docs
src/workflows/onboarding.workflow.ts
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 }
}
)
Addons

Install a backend feature in one line.

Stripe billing. SendGrid emails. One wireAddon() call each. Install, configure secrets, call via namespaced RPC — fully typed.

src/wiring.ts
// 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'
}
})
src/functions/checkout.func.ts
// 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 }
}
})
Drop-in, not bolt-on

Install a package, add one wireAddon() call, and its functions appear as namespaced RPC calls. No glue code, no adapters.

Fully typed across boundaries

The CLI generates TypeScript definitions for every addon function — rpc.invoke('stripe:checkoutCreate', …) autocompletes with exact input and output types.

Secrets you control

Addons declare what secrets they need. You map them to your own infrastructure with secretOverrides.

Shared infrastructure

Addons reuse your existing logger, database, and services. Each addon gets its own namespace, so nothing collides.

Read the Addons docs
Gateway

One handler for every messaging platform.

WhatsApp, Slack, Telegram, WebChat — write one function. The adapter normalizes every platform into the same message format.

Webhook auto-verification

WhatsApp challenges, Slack url_verification, Telegram tokens — handled by the adapter, invisible to your code.

Normalized messages

Every platform delivers the same message shape — senderId, text, attachments, metadata. Your handler never knows which platform sent it.

Three transport types

Webhook for cloud APIs, WebSocket for browser chat widgets, listener for persistent connections (Baileys, Signal CLI, Matrix).

Same middleware, same auth

Rate limiting, logging, permissions — your existing middleware works on gateways too.

Read the Gateway docs
src/gateway.wiring.ts
// 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,
})
src/gateway.functions.ts
// 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}` }
}
})
Deploy anywhere

Change your runtime. Keep your functions.

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 →

Built for production

Production-grade out of the box.

Type-safe clients

Auto-generated HTTP, WebSocket, and RPC clients with full IntelliSense.

Auth & permissions

Cookie, bearer, API key auth with fine-grained permissions — built in.

Services

Singleton and per-request dependency injection, type-safe and testable.

Middleware

Before/after hooks for logging, metrics, tracing — across all protocols.

Schema validation

Runtime validation against TypeScript input schemas. Supports Zod.

Zero lock-in

Standard TypeScript, tiny runtime, MIT licensed. Bring your own everything.

The console

Every function, every wire, one screen.

Browse functions, run agents, manage secrets, and trigger workflows — without writing tooling code.

Pikku Console — browse and inspect all functions, wirings, and services
Learn about the Console →
From the teams who switched

Built for the problems developers actually have.

"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."
Alex HarleyCo-founder @ Superbridge
"Ever been annoyed at having to write your code different in a Lambda than in an express handler? Pikku fixes that."
Christoph SitterCTO @ spot

Stop rewriting the same function.

Write it once. Pikku wires it everywhere.