| name | pikku-middleware |
| description | Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware (including service-to-service bearer auth), per-route middleware, session-setting middleware, or understanding middleware execution order and priority. TRIGGER when: user wants middleware on some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global interceptors, or middleware priority/order questions. DO NOT TRIGGER when: user asks about permissions/authorization checks (use pikku-permissions), auth strategies like authBearer/authCookie (use pikku-security), or deployment. |
| installGroups | ["core"] |
Pikku Middleware
Agent Operating Procedure
- Discover before editing. Run
pikku info middleware --verbose and pikku info tags --json to understand the existing middleware and tag landscape.
- Identify the source files that own the behavior — wirings files, not generated output.
- Register middleware at module load time — in a
wirings/*.ts file, never inside a function body.
- Validate: run
pikku all --tsc after adding or changing middleware — it regenerates and then confirms type safety in one pass.
The pikkuMiddleware Factory
import { pikkuMiddleware } from '#pikku'
const myMiddleware = pikkuMiddleware(async (services, wire, next) => {
await next()
})
const telemetryMiddleware = pikkuMiddleware({
name: 'my-telemetry',
priority: 'highest',
func: async (services, wire, next) => {
const start = performance.now()
try {
await next()
} finally {
services.logger.info({ duration: Math.round(performance.now() - start) })
}
},
})
The wire object gives you:
wire.http — inbound HTTP context (headers, URL, cookies)
wire.setSession(session) — set the session for this request
wire.getSession() — read the current session
wire.session — the session set so far (may be undefined)
Throw a typed error to abort: UnauthorizedError, ForbiddenError, etc. from @pikku/core/errors.
Scoping: Five Levels
From broadest to narrowest:
addGlobalMiddleware([telemetryOuter()])
addHTTPMiddleware('*', [cors(), authBearer()])
addHTTPMiddleware('/admin/*', [auditLog])
addTagMiddleware('machine-agent', [bearerAuth])
wireHTTP({
route: '/books/:id',
func: getBook,
middleware: [cacheControl],
})
Global Middleware (addGlobalMiddleware)
Runs before everything else, across every wire type: HTTP, Queue, Channel, Trigger, Scheduler, Workflow, Agent, CLI, MCP. Use it for cross-cutting concerns (e.g. telemetry) that must wrap every invocation regardless of transport.
import { addGlobalMiddleware } from '@pikku/core'
import { telemetryOuter, telemetryInner } from '@pikku/core/middleware'
addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })])
addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })])
telemetryOuter ships with priority: 'highest', telemetryInner with priority: 'lowest' — so priority sorting places outer first regardless of array/call order.
HTTP & Prefix Middleware (addHTTPMiddleware)
import { addHTTPMiddleware } from '@pikku/core/http'
import { cors, authBearer } from '@pikku/core/middleware'
addHTTPMiddleware('*', [cors({ origin: 'https://app.example.com', credentials: true })])
addHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])
Tag Middleware (addTagMiddleware)
Tag middleware fires for any wiring (function or wire object) that carries a matching tag. This is the canonical approach for service-to-service bearer auth, rate limiting a group, or any cross-cutting concern scoped to a subset of routes.
Setting Tags
export const myFunc = pikkuSessionlessFunc({
auth: false,
tags: ['machine-agent'],
func: async (services, input) => { ... },
})
wireHTTP({
route: '/internal/action',
method: 'post',
auth: false,
tags: ['internal'],
func: myFunc,
})
Tags from the function definition and the wire object are merged — middleware from both tag sets runs.
Registering Tag Middleware
import { addTagMiddleware } from '.pikku/pikku-types.gen.js'
addTagMiddleware('machine-agent', [machineAgentBearerAuth])
Call at module load time — typically in the same wirings/*.ts file as the wireHTTP calls that use the tag.
Middleware Execution Order
Scope resolution order (broadest → narrowest):
global → httpGroup/* → httpGroup/prefix → wiringTags → wiringMiddleware → funcTags → funcMiddleware → function body
Within each scope, sorted by priority:
highest → high → medium (default) → low → lowest
Set priority using the config-object form of pikkuMiddleware:
const earlyMiddleware = pikkuMiddleware({
name: 'early',
priority: 'highest',
func: async (services, wire, next) => { ... },
})
Priority is the primary sort key; within the same level, registration order is preserved. Use priority when a middleware must run before/after others regardless of registration order (e.g. telemetry wrapping everything, session extraction before auth checks).
Service-to-Service Bearer Auth (canonical pattern)
A server that exposes RPCs only to a trusted caller (e.g. an API calling a machine-agent). Auth lives in a tag middleware — NOT in the function body. Authorization/permission checks belong in the permissions field (see pikku-permissions), never inside func.
On the server (the service being called): tag the function, register a pikkuMiddleware that reads the Authorization header on that tag.
let _token: string | null = null
export const setToken = (t: string) => { _token = t }
export const getToken = () => _token
import { timingSafeEqual } from 'node:crypto'
import { addTagMiddleware, pikkuMiddleware } from '../../.pikku/pikku-types.gen.js'
import { UnauthorizedError } from '@pikku/core/errors'
import { getToken } from '../lib/host-token.js'
const bearerAuth = pikkuMiddleware(async (_services, { http }, next) => {
const authHeader = http?.request?.header?.('authorization') || http?.request?.header?.('Authorization')
const token = getToken()
const expected = token ? `Bearer ${token}` : null
if (
!expected ||
!authHeader ||
authHeader.length !== expected.length ||
!timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected))
) {
throw new UnauthorizedError()
}
return next()
})
addTagMiddleware('machine-agent', [bearerAuth])
export const myFunc = pikkuSessionlessFunc({
expose: true,
auth: false,
tags: ['machine-agent'],
func: async (services, input) => { ... },
})
On the client (the caller): use the generated RPCInvoke type — never hand-write a fetch wrapper's types. See references/middleware-patterns.md.
More patterns
references/middleware-patterns.md covers the client-side RPCInvoke caller, session-setting middleware (set a session from an API key), and request logging / audit middleware.
After Changes
pikku all
pikku all --tsc