Consuming Addons
Once an addon is published, any Pikku application can consume it with minimal configuration. Functions become available via namespaced RPC calls, and trigger sources can be wired to your own handler functions.
Configuration
Wire addons in a *.wiring.ts file:
import { wireAddon } from '#pikku'
wireAddon({
name: 'postgres',
package: '@pikku/addon-postgres',
})
The name becomes the namespace prefix for all RPC calls to that addon's functions.
Setting Up Services
Your services.ts provides the base services that addons need — logger, secrets, and variables. Addons receive these automatically:
import type { SingletonServices } from '../.pikku/pikku-types.gen.js'
import { CreateSingletonServices } from '@pikku/core'
import {
ConsoleLogger,
LocalVariablesService,
LocalSecretService,
} from '@pikku/core/services'
import '../.pikku/pikku-bootstrap.gen.js'
export const createSingletonServices: CreateSingletonServices<
{},
SingletonServices
> = async (_config, existingServices) => {
const variables =
existingServices?.variables ?? new LocalVariablesService(process.env)
const secrets =
existingServices?.secrets ?? new LocalSecretService(variables)
return {
logger: existingServices?.logger ?? new ConsoleLogger(),
variables,
secrets,
}
}
The addon's service factory (created with pikkuAddonServices) receives your logger, secrets, and variables to build its own services on top.
Namespaced RPC Calls
Call addon functions using the namespace:functionName syntax:
import { pikkuSessionlessFunc } from '#pikku'
export const runDatabaseOperations = pikkuSessionlessFunc<void, void>({
func: async (_services, _data, { rpc }) => {
// Insert a row
await rpc.invoke('postgres:insert', {
table: 'users',
data: { name: 'alice', email: 'alice@example.com' },
})
// Query rows
const users = await rpc.invoke('postgres:select', {
table: 'users',
where: {
conditions: [{ column: 'name', operator: '=', value: 'alice' }],
},
})
// Update rows
await rpc.invoke('postgres:update', {
table: 'users',
data: { email: 'new@example.com' },
where: {
conditions: [{ column: 'name', operator: '=', value: 'alice' }],
},
})
// Delete rows
await rpc.invoke('postgres:deleteRows', {
table: 'users',
where: {
conditions: [{ column: 'name', operator: '=', value: 'alice' }],
},
})
},
})
Type Safety
The CLI generates type definitions for all addon functions. Your IDE provides full autocompletion and type checking for both inputs and outputs:
// TypeScript knows the exact input and output types
const result = await rpc.invoke('stripe:customerCreate', {
name: 'Jane Doe',
email: 'jane@example.com',
})
// result is fully typed based on the addon's function definition
This works through generated type maps that prefix addon function types with their namespace:
// Generated by the CLI — you don't write this
type FlattenedRPCMap = RPCMap & PrefixKeys<PostgresRPCMap, 'postgres'>
Using Trigger Sources
Addons can export trigger source functions that listen for events. You wire them to your own handler functions in a *.wiring.ts file:
// change.wiring.ts
import { wireTrigger, wireTriggerSource } from '#pikku'
import { onChanges } from '@pikku/addon-postgres'
import { onChange } from './on-change.function.js'
// Declare the trigger and its handler
wireTrigger({
name: 'onChange',
func: onChange,
})
// Wire the addon's trigger source with configuration
wireTriggerSource({
name: 'onChange',
func: onChanges,
input: { events: ['DELETE', 'INSERT', 'UPDATE'], table: 'users' },
})
Your handler function receives the trigger's output as typed input:
// on-change.function.ts
import { pikkuSessionlessFunc } from '#pikku'
export const onChange = pikkuSessionlessFunc<
{ event: string; data: any },
void
>({
func: async ({ logger }, data) => {
logger.info(`Database change: ${data.event}`, data.data)
},
})
This pattern works with any trigger source — Redis pub/sub, Telegram updates, file watchers, and more:
// subscribe.wiring.ts
import { wireTrigger, wireTriggerSource } from '#pikku'
import { subscribe } from '@pikku/addon-redis'
import { onMessage } from './on-message.function.js'
wireTrigger({
name: 'subscribe',
func: onMessage,
})
wireTriggerSource({
name: 'subscribe',
func: subscribe,
input: { channels: new Set(['notifications']), jsonParseBody: true },
})
See Triggers for more details on how triggers work.
Multiple Addons
Wire multiple addons, each under their own namespace:
wireAddon({ name: 'postgres', package: '@pikku/addon-postgres' })
wireAddon({ name: 'redis', package: '@pikku/addon-redis' })
wireAddon({ name: 'stripe', package: '@pikku/addon-stripe' })
// Each addon's functions are prefixed with its namespace
await rpc.invoke('postgres:insert', { table: 'orders', data: order })
await rpc.invoke('redis:keySet', { key: `order:${orderId}`, value: JSON.stringify(order) })
await rpc.invoke('stripe:chargeCreate', { amount: 2000, currency: 'usd', source: 'tok_visa' })
Secret Overrides
Addons define secrets with their own secretId names. If your application stores the same secret under a different name, you can override the mapping:
wireAddon({
name: 'email',
package: '@pikku/addon-sendgrid',
secretOverrides: {
SENDGRID_API_KEY: 'MY_EMAIL_API_KEY',
},
})
When the addon calls secrets.getSecretJSON('SENDGRID_API_KEY'), Pikku transparently looks up MY_EMAIL_API_KEY instead. The addon code doesn't need to know about your naming conventions.
Best Practices
Namespace naming: Use short, descriptive namespaces (postgres, stripe, redis) that identify the addon's purpose.
Provide secrets: Addons declare what secrets they need via wireSecret. Make sure you've configured those secrets in your secret store before calling addon functions.
Trigger configuration: When wiring trigger sources, the input parameter controls what the trigger listens for. Check the addon documentation for available options.