---
name: pikku-services
description: '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).'
installGroups: [core]
---

# Pikku Services (Dependency Injection)

## Agent Operating Procedure

Use this skill as an execution checklist, not reference material.

1. Discover before editing. Prefer OpenCode tools such as `pikku-meta` when available; otherwise run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.

Pikku uses factory functions for dependency injection. Singleton services are created once at startup. Wire services are created fresh per request/job/command.

## Before You Start

```bash
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.

## API Reference

### `pikkuServices(factory)`

Create singleton services — instantiated once at server startup.

```typescript
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.

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

### Auto-Generated Service Manifest

After `npx pikku prebuild`, Pikku generates a manifest of which services are actually used:

```typescript
// .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'>>
```

## Usage Patterns

### Basic Singleton Services

```typescript
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(),
    }
  }
)
```

### Per-Request Wire Services

```typescript
const createWireServices = pikkuWireServices(
  async (singletonServices, wire) => {
    return {
      userSession: createUserSessionService(wire),
      dbTransaction: new DatabaseTransaction(singletonServices.database),
    }
  }
)
```

### Using Services in Functions

Functions destructure services from the first parameter:

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

### Dynamic Import Optimization

Use the generated manifest to conditionally import heavy dependencies:

```typescript
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 }
})
```

### Audit Wire Service

`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.

```typescript
// 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:

```typescript
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). Run `pikku fabric validate` to confirm the migration is in place.

### Built-in Services

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

## Complete Example

```typescript
// 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 }
  },
})
```
