Skip to main content

Pikku 0.9 — cleaner naming, tag‑driven cross‑cutting, RPC exposure

· 5 min read

Pikku 0.9 is a clarity release. Fewer naming footguns, explicit RPC exposure, transport-wide tagging for middleware/permissions, and early interaction objects that make non-HTTP middleware practical. It also refactors the HTTP router to make swapping strategies feasible next, though that toggle isn’t user-facing yet.

tl;dr: predictable names, safer RPC boundaries, and a single tag‑driven model for cross‑cutting concerns across HTTP, channels, queues and scheduled tasks.

1) Consistent wiring names

Pikku now uses a consistent “wireX” shape across transports. Concretely:

// HTTP
wireHTTP({ method: 'get', route: '/health', func: healthCheck })

// WebSockets / channels
wireChannel({
name: 'chat',
onOpen: joinChat,
onMessage: handleChatMessage
})

// Queues
wireQueueWorker({ queueName: 'thumbnail', func: generateThumbnail })

// Scheduled tasks
wireScheduledTask({ schedule: '0 * * * *', name: 'hourlyReindex', func: reindex })

// MCP
wireMCPTool({ name: 'disableTool', description: 'Disable a tool by name', func: disableTool })

Note:

  • There is no wireRPC(...). RPC exposure is controlled per function (see section 4).

2) Naming: pikkuPermission and pikkuMiddleware

Standardized constructors bring consistency without losing type safety:

export const requireOwner = pikkuPermission<{ todoId: string }>(
async ({ todoService }, { todoId }, session) => {
const { ownerId } = await todoService.get(todoId)
return ownerId === session.userId
}
)

export const withSession = pikkuMiddleware(
async (services, { http }, next) => {
// attach session, enforce timeouts, set headers, etc.
return next()
}
)

3) Tags: like routers, but for all wirings

Tags make HTTP‑style routing power available to all transports.

// Example
addMiddleware(['admin', 'web'], [withSession, auditTrail])
addPermission('admin', [requireAdminUser])
addMiddleware('logging', [logMiddleware])

// Attach tags on wires
wireQueueWorker({
queueName: 'thumbnail',
func: generateThumbnail,
tags: ['logging'], // resolves queue-scoped middleware/permissions
})

Outcome: the same “attach by tag” model applies to HTTP, channels, queues, scheduled tasks, and RPC functions.

You can still directly set permissions / middleware on the function or wiring.

The order of execution is:


4) Interaction objects for Queue and ScheduledTask

Middleware/permissions can now read rich context for non‑HTTP transports.

Queue

export interface PikkuQueue {
/** The name of the queue being processed */
queueName: string
/** The current job ID */
jobId: string
/** Update job progress (0-100 or custom value) */
updateProgress: (progress: number | string | object) => Promise<void>
/** Fail the current job with optional reason */
fail: (reason?: string) => Promise<void>
/** Discard/delete the job without retrying */
discard: (reason?: string) => Promise<void>
}

Scheduled task

/**
* Represents a scheduled task interaction object for middleware
* Provides information about the current scheduled task execution
*/
export interface PikkuScheduledTask {
/** The name of the scheduled task being executed */
name: string
/** The cron schedule expression */
schedule: string
/** Current execution timestamp */
executionTime: Date
/** Skip the current task execution */
skip: (reason?: string) => void
}

Using them in middleware

export const queueGuard = pikkuMiddleware(async (s, { queue }, next) => {
if (queue?.queueName !== 'thumbnail') throw new Error('wrong-queue')
return next()
})

export const scheduleGuard = pikkuMiddleware(async (s, { scheduledTask }, next) => {
if (scheduledTask?.schedule === '0 * * * *') { /* throttle, log, etc. */ }
return next()
})

Status: these interaction objects are available in the codebase and rolling out to all runtimes. If a runtime hasn’t implemented one yet, the field is undefined. Use optional chaining until parity lands.

Using them in functions

This would work for SSE, Queues (if supported) and websocket connections.

export const progressUpdate = pikkuFunction<void, { progress: number }>(async ({ queue }, data, next) => {
for (let progress = 0; i < 100; i++>) {
await new Promise.delay(1000)
if (queue) {
queue.onProgress(progress)
}
if (channel) {
channel.send({ progress })
}
}
})

Status: these interaction objects are available in the codebase and rolling out to all runtimes. If a runtime hasn’t implemented one yet, the field is undefined. Use optional chaining until parity lands.


5) RPC exposure: expose: true

Public vs internal RPCs are explicit at the function definition via an expose flag and the function’s name.

// Example: sessionless RPC with recursion depth guard
export const rpcTest = pikkuSessionlessFunc<{ in: number }>({
func: async ({ logger, rpc }, data) => {
logger.debug(`RPC Test with RPC: ${rpc?.depth}`)
if (rpc?.depth && rpc?.depth < 10) {
data.in += 1
rpc.invoke('rpcTest', data)
}
return data
},
expose: true, // makes it available on the public RPC surface
})

Notes:

  • Internal RPCs: use expose: false (default) and omit them from any public client surface.
  • The rpc interaction object provides helpers like depth (for loop protection) and invoke(name, data) to chain RPC calls safely.
  • Two RPC clients are created, one for internal use, and another for public APIs.

6) Router refactor → swapping soon, not today

Internally, the HTTP router has been refactored toward a factory-based design so different lookup strategies can be swapped in. That said, the public switch is not shipped yet. It’s on the immediate roadmap; 0.9 just removes the architectural blockers so we can ship the toggle cleanly in the next minor.

What this means for you right now: no API change; future‑proofing is in place.


7) Agents workspace (early days)

A new Yarn workspace is accumulating “Agent files” to guide Claude/other LLMs to generate idiomatic Pikku code:

  • project structure
  • services
  • system
  • how to use kysely with pikku
  • errors and mapping

This is intentionally opinionated. Constraints improve model output quality.


Closing

0.9 is about sharp edges and fewer surprises: consistent names, explicit public boundaries, one tagging model across transports, and the ergonomics needed to treat queues & cron as first‑class citizens in middleware. The router work is teed up; once the switch exists, choosing a strategy will be a config change—not a refactor.