packages/cli/skills/pikku-services/SKILL.md
Use when setting up dependency injection, creating custom services, or configuring the service layer in a Pikku app. Covers pikkuServices (singleton), pikkuWireServices (per-request), service typing, built-in services, and tree-shaking. TRIGGER when: code uses pikkuServices/pikkuWireServices, user asks about services.ts, dependency injection, service factories, or built-in services (ConsoleLogger, JoseJWTService). DO NOT TRIGGER when: user asks about auth middleware (use pikku-security) or secrets/variables (use pikku-config).
npx skillsauth add pikkujs/pikku pikku-servicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill as an execution checklist, not reference material.
pikku-meta when available; otherwise run the relevant pikku meta ... --json command and inspect only the focused output you need..pikku, node_modules, vendored packages, or broad build artifacts.pikku-verify or pikku all when functions, wirings, schemas, or generated clients may have changed.Pikku uses factory functions for dependency injection. Singleton services are created once at startup. Wire services are created fresh per request/job/command.
pikku info functions --verbose # See which services existing functions use
pikku info tags --verbose # Understand project organization
See pikku-concepts for the core mental model.
pikkuServices(factory)Create singleton services — instantiated once at server startup.
import { pikkuServices } from '#pikku'
const createSingletonServices = pikkuServices(
async (config, existingServices?) => {
// config: your CoreConfig object
// existingServices: optional, for chaining factories
return {
config,
logger: Logger,
jwt: JWTService,
database: DatabasePool,
// ...any custom services
}
}
)
pikkuWireServices(factory)Create per-request services — fresh instance for each HTTP request, queue job, CLI command, etc.
import { pikkuWireServices } from '#pikku'
const createWireServices = pikkuWireServices(
async (singletonServices, wire) => {
// singletonServices: all singleton services
// wire: transport context (session, channel, etc.)
// Pikku merges these with singleton services automatically
return {
userSession: UserSessionService,
dbTransaction: DatabaseTransaction,
}
}
)
After npx pikku prebuild, Pikku generates a manifest of which services are actually used:
// .pikku/pikku-services.gen.ts (auto-generated)
export const requiredSingletonServices = {
database: true, // used by getUser, deleteUser
audit: true, // used by deleteUser
cache: false, // not used by any wired function
jwt: true, // used by auth middleware
} as const
export type RequiredSingletonServices = Pick<
SingletonServices,
'database' | 'audit' | 'jwt'
> &
Partial<Omit<SingletonServices, 'database' | 'audit' | 'jwt'>>
const createSingletonServices = pikkuServices(
async (config, existingServices) => {
const logger = new ConsoleLogger()
const database = new DatabasePool(config.database)
await database.connect()
const jwt = new JoseJWTService(
async () => [{ id: 'my-key', value: JWT_SECRET }],
logger
)
return {
config,
logger,
database,
jwt,
books: new BookService(),
}
}
)
const createWireServices = pikkuWireServices(
async (singletonServices, wire) => {
return {
userSession: createUserSessionService(wire),
dbTransaction: new DatabaseTransaction(singletonServices.database),
}
}
)
Functions destructure services from the first parameter:
const getUser = pikkuFunc({
title: 'Get User',
func: async ({ db, logger, jwt }, { userId }) => {
logger.info('Fetching user', { userId })
const user = await db.getUser(userId)
return { user }
},
})
Use the generated manifest to conditionally import heavy dependencies:
import { requiredSingletonServices } from '.pikku/pikku-services.gen.js'
const createSingletonServices = pikkuServices(async (config) => {
const logger = new ConsoleLogger()
let jwt: JWTService | undefined
if (requiredSingletonServices.jwt) {
const { JoseJWTService } = await import('@pikku/jose')
jwt = new JoseJWTService(keys, logger)
}
let database: Database | undefined
if (requiredSingletonServices.database) {
database = await createDatabase(config.databaseUrl)
}
return { config, logger, jwt, database }
})
createInvocationAudit creates a per-request InvocationAuditLog that buffers audit events in memory and flushes them as a batch when the function-runner calls closeWireServices at the end of the request. If singletonServices.audit is not configured (local dev without Fabric), it returns a no-op DisabledInvocationAudit — no crash, events are silently dropped.
Pair with createAuditedKysely to auto-capture every Kysely query as an audit event.
// services.ts
import { createInvocationAudit } from '@pikku/core/services'
import { createAuditedKysely } from '@pikku/kysely'
export const createWireServices = pikkuWireServices(async (singletonServices, wire) => {
const audit = createInvocationAudit(singletonServices.audit, wire)
const kysely = singletonServices.kysely
? createAuditedKysely(singletonServices.kysely, { audit })
: undefined
return { audit, ...(kysely ? { kysely } : {}) }
})
The audit wire service is typed as AuditLog (from @pikku/core). Functions that emit custom events use it directly:
const deleteUser = pikkuFunc({
func: async ({ audit }, { userId }) => {
await audit.audit({ type: 'user.deleted', actor_user_id: userId })
// ...
},
})
closeWireServices (called automatically by the function-runner) invokes audit.close() → singletonServices.audit.write(batch) → platform-specific flush (e.g. CF Queue, libsql INSERT). No manual flushing needed.
Fabric note: Fabric provisions the audit queue and consumer worker automatically. The audit table schema is in
db/sqlite/0003-audit.sql(starter-template). Runpikku fabric validateto confirm the migration is in place.
| Service | Package | Purpose |
| -------------------------- | ---------------------- | -------------------------------- |
| ConsoleLogger | @pikku/core/services | Console-based logging |
| JoseJWTService | @pikku/jose | JWT sign/verify via jose |
| LocalSecretService | @pikku/core/services | Local development secrets |
| LocalVariablesService | @pikku/core/services | Local environment variables |
| PinoLogger | @pikku/pino | Structured logging via Pino |
| createInvocationAudit | @pikku/core/services | Per-request audit buffer |
| createAuditedKysely | @pikku/kysely | Auto-capture DB queries as audit events |
// services.ts
import { pikkuServices, pikkuWireServices } from '#pikku'
import { ConsoleLogger } from '@pikku/core/services'
import { JoseJWTService } from '@pikku/jose'
// Custom service
class TodoStore {
private todos: Map<string, Todo> = new Map()
async create(title: string, priority: string) {
const todo = { id: crypto.randomUUID(), title, priority, completed: false }
this.todos.set(todo.id, todo)
return todo
}
async get(id: string) {
return this.todos.get(id)
}
async list() {
return [...this.todos.values()]
}
async delete(id: string) {
this.todos.delete(id)
}
}
export const createSingletonServices = pikkuServices(async (config) => {
const logger = new ConsoleLogger()
const jwt = new JoseJWTService(
async () => [{ id: 'my-key', value: config.jwtSecret }],
logger
)
return {
config,
logger,
jwt,
secrets: new LocalSecretService(),
variables: new LocalVariablesService(),
todoStore: new TodoStore(),
}
})
export const createWireServices = pikkuWireServices(
async (singletonServices, wire) => ({
scopedLogger: new ScopedLogger(wire.session?.initial?.userId),
})
)
// functions/todos.functions.ts — services are auto-injected
export const createTodo = pikkuFunc({
title: 'Create Todo',
func: async ({ todoStore, logger }, { title, priority }) => {
const todo = await todoStore.create(title, priority)
logger.info('Created todo', { id: todo.id })
return { todo }
},
})
documentation
Deprecated — use pikku-middleware instead. Tag middleware (addTagMiddleware) is now documented as a section within the pikku-middleware skill, alongside global HTTP middleware, execution order, and the service-to-service bearer auth pattern.
testing
Use when adding authorization checks to Pikku functions or routes — pikkuPermission, pikkuAuth, per-function permissions, pattern-based permissions, or understanding OR/AND permission logic. TRIGGER when: user wants to restrict who can call a function, check resource ownership, add role-based access, or understand where permission checks belong. DO NOT TRIGGER when: user asks about middleware or request interception (use pikku-middleware), authentication strategies (use pikku-security), or session management.
testing
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.
documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.