Skip to main content
TypeScript Function Framework

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.

$ npm create pikku@latest  ·  MIT Licensed  ·  Open Source

Built with Pikku

AgreeWeHeyGermanymartaBambooRoseCalligraphy Cut

The Difference

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

Without Pikkurepeated + fragile
// 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
// 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.

The function on the left works with every protocol on the right. Same auth, same validation, zero rewrites.

Write once

getCard.tsfunc.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

HTTP APIwiring.ts
wireHTTP({
method: 'get',
route: '/cards/:cardId',
func: getCard
})
AI AgentsAlpha

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. No adapters, no wrappers, no manual schema definitions.

Auth follows the agent

Agents inherit the caller's session, permissions, and middleware. The same rules that protect your HTTP endpoints automatically 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.

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 to PostgreSQL or Redis between steps. Deploy a new version mid-workflow and execution continues from where it left off.

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, no boilerplate.

Fully typed across boundaries

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

Secrets you control

Addons declare what secrets they need. You map them to your own infrastructure with secretOverrides — no env vars leaking across packages.

Shared infrastructure

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

GatewayNew

One handler for every
messaging platform

WhatsApp, Slack, Telegram, WebChat — write one function. The adapter normalizes every platform into the same message format. Three transport types cover every integration pattern.

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,
})

// Listener — persistent connection (Baileys, Signal)
wireGateway({
name: 'signal',
type: 'listener',
adapter: signalAdapter,
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}` }
}
})

Webhook with auto-verification

Registers POST and GET routes automatically. WhatsApp challenges, Slack url_verification, Telegram tokens — handled by the adapter, invisible to your code.

Normalized messages

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

Three transport types

Webhook for cloud APIs (WhatsApp, Slack, Telegram). 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. wire.gateway gives you platform, senderId, and send() in every handler.

Deploy Anywhere

Change your runtime.
Keep your functions.

Same code runs on Express, Fastify, AWS Lambda, Cloudflare Workers, Next.js, and more. Switching runtimes never requires touching your functions.

Plus any custom runtime via the adapter interface. Build your own →

Built For Production

Production-grade out of the box

Auth, validation, type-safe clients, middleware — all built in. No bolting on third-party packages for every new protocol.

MIT licensed. Standard TypeScript. No VC-backed lock-in.

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 — work 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.

Visual Control PlaneAlpha

Every function, every wire,
every secret — 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
Browse all functions & wiringsAgent playgroundRun & visualize workflowsManage secrets & variablesPer-environment control

Built for the problems developers actually have

From the teams who switched

"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 Harley

Co-founder @ Superbridge

"Ever been annoyed at having to write your code different in a Lambda than in an express handler? Pikku fixes that."

Christoph Sitter

CTO @ spot

Stop copying functions across protocols.

Write it once. Pikku wires it everywhere.

$ npm create pikku@latest

5-minute setup  ·  MIT Licensed  ·  Open Source