Skip to main content

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.