Creating an Addon
Addons are standard Pikku applications marked for reuse. They contain functions, services, middleware, secrets, and variables — just like any Pikku project.
Configuration
Mark a project as an addon in a *.wiring.ts file:
import { wireAddon } from '#pikku'
wireAddon({ addon: true })
Package Structure
A typical addon looks like this:
my-package/
src/
functions/ # Pikku functions
my-service.ts # Custom service class
services.ts # Service factory (pikkuAddonServices)
my-package.secret.ts # Secret definitions (wireSecret)
my-package.variable.ts # Variable definitions (wireVariable)
index.ts # Export all functions
.pikku/ # Generated by CLI
pikku.config.json
package.json
Defining Secrets
Declare what secrets your package needs:
// sendgrid.secret.ts
import { z } from 'zod'
import { wireSecret } from '#pikku'
export const sendgridSecretsSchema = z.string().describe('SendGrid API key')
wireSecret({
name: 'api_key',
displayName: 'SendGrid API Key',
description: 'SendGrid API key for sending emails',
secretId: 'SENDGRID_API_KEY',
schema: sendgridSecretsSchema,
})
Creating Services
Use pikkuAddonServices to create your service factory. It receives the consumer's existing services (logger, secrets, variables) so you don't duplicate infrastructure:
// services.ts
import { SendgridService } from './sendgrid-api.service.js'
import { pikkuAddonServices } from '#pikku'
export const createSingletonServices = pikkuAddonServices(
async (config, { secrets }) => {
const apiKey = await secrets.getSecretJSON<string>('SENDGRID_API_KEY')
const sendgrid = new SendgridService(apiKey)
return { sendgrid }
}
)
The pikkuAddonServices helper ensures your factory receives typed secrets and the consumer's shared services (logger, variables, etc.).
Writing Functions
Functions use your injected services like any other Pikku function:
// functions/mail/send.function.ts
import { z } from 'zod'
import { pikkuSessionlessFunc } from '#pikku'
export const MailSendInput = z.object({
to: z.string(),
subject: z.string(),
body: z.string(),
})
export const MailSendOutput = z.object({
success: z.boolean(),
})
export const mailSend = pikkuSessionlessFunc({
description: 'Sends an email through SendGrid',
input: MailSendInput,
output: MailSendOutput,
func: async ({ sendgrid }, data) => {
await sendgrid.request('POST', '/mail/send', { body: data })
return { success: true }
},
})
Exporting Functions
Export all functions from your package's index.ts:
// index.ts
export { mailSend } from './functions/mail/send.function.js'
export { listCreate } from './functions/lists/create.function.js'
export { contactUpsert } from './functions/contacts/upsert.function.js'
Exporting Trigger Sources
Packages can export trigger source functions that consumers wire to their own handlers:
// functions/on-changes.trigger.ts
import { pikkuTriggerFunc } from '#pikku'
export const onChanges = pikkuTriggerFunc<
{ table: string; events: ('INSERT' | 'UPDATE' | 'DELETE')[] },
{ event: string; data: any }
>({
title: 'Postgres Changes',
description: 'Triggers on row changes in a PostgreSQL table',
func: async ({ postgres }, { table, events }, { trigger }) => {
// Set up LISTEN/NOTIFY on the table
// Call trigger.invoke() when changes occur
// Return teardown function
},
})
Consumers then wire this to their own handler function — see Consuming Addons.
Publishing
Your package.json must export the .pikku/ directory:
{
"files": ["dist", ".pikku"],
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js"
},
"./.pikku/*": "./.pikku/*"
}
}
Then publish normally:
npm publish
Best Practices
Service sharing: Use pikkuAddonServices — it handles receiving the consumer's logger, secrets, and variables automatically.
Secret naming: Use descriptive secretId names that make overrides intuitive: SENDGRID_API_KEY, STRIPE_CREDENTIALS.
Input/output schemas: Define Zod schemas for all functions. This enables validation, type generation for consumers, and MCP/Forge compatibility.
Versioning: Follow semantic versioning. Breaking changes to function signatures require major version bumps.