Creating an Addon
Addons are reusable Pikku packages β they contain functions, services, secrets, variables, credentials, and wirings, all publishable as npm packages that consumers can wire into their projects.
Scaffolding with the CLIβ
The fastest way to create an addon:
npx pikku new addon my-integration
This generates a complete addon project with package.json, tsconfig, pikku.config.json, service factory, API service class, types, and a test harness.
Scaffold Optionsβ
| Flag | Description |
|---|---|
--secret | Include a secret definition file (wireSecret) |
--variable | Include a variable definition file (wireVariable) |
--oauth | Include OAuth2 credential wiring, secret for app credentials, and OAuth2Client-based API service |
--credential apikey | Per-user API key credential (wireCredential with type: 'wire') |
--credential bearer | Per-user bearer token credential |
--credential oauth2 | Per-user OAuth2 credential (same as --oauth but using the credential system) |
--openapi ./spec.yaml | Generate functions from an OpenAPI spec |
--mcp | Add mcp: true to generated functions (expose as MCP tools) |
--camelCase | Convert snake_case properties to camelCase in generated Zod schemas |
--display-name "My Tool" | Human-readable display name (defaults to PascalCase of name) |
--description "..." | Package description |
--category General | Category for the addon registry (default: General) |
--dir ./packages | Output directory (defaults to scaffold.addonDir or cwd) |
--no-test | Skip generating the test harness |
Examplesβ
# Simple addon with a secret
npx pikku new addon sendgrid --secret
# OAuth2 integration (Google Sheets, Slack, etc.)
npx pikku new addon google-sheets --oauth
# Per-user API key credential
npx pikku new addon stripe --credential apikey
# Generate from an OpenAPI spec
npx pikku new addon github --openapi ./github-api.yaml --credential bearer
# Full options
npx pikku new addon hubspot \
--oauth \
--variable \
--mcp \
--openapi ./hubspot-openapi.yaml \
--camelCase \
--display-name "HubSpot CRM" \
--category "CRM"
Generated Structureβ
my-integration/
βββ src/
β βββ index.ts # Export your functions
β βββ services.ts # Service factory (pikkuAddonServices)
β βββ my-integration-api.service.ts # API client class
β βββ my-integration.types.ts # Zod schemas for API types
β βββ my-integration.secret.ts # Secret definition (with --secret)
β βββ my-integration.variable.ts # Variable definition (with --variable)
β βββ my-integration.credential.ts # Credential wiring (with --credential/--oauth)
βββ types/
β βββ application-types.d.ts # Service type declarations
βββ test/ # Test harness (separate pikku project)
β βββ src/
β β βββ addons.ts # wireAddon call
β β βββ services.ts # Test services factory
β β βββ my-integration-tests.function.ts
β β βββ my-integration.test.ts
β βββ pikku.config.json
β βββ package.json
βββ .pikku/ # Generated by CLI
βββ pikku.config.json
βββ tsconfig.json
βββ package.json
βββ versions.pikku.json # Contract version manifest
Manual Setupβ
If you prefer to set up an addon by hand, the key pieces are:
Mark as Addonβ
In pikku.config.json:
{
"tsconfig": "./tsconfig.json",
"srcDirectories": ["src", "types"],
"outDir": "./.pikku",
"addon": true
}
Or with metadata:
{
"addon": {
"displayName": "SendGrid",
"description": "SendGrid email integration",
"categories": ["Email"]
}
}
Define Secretsβ
Declare what secrets your addon needs:
// sendgrid.secret.ts
import { wireSecret } from '@pikku/core/secret'
import { z } from 'zod'
export const sendgridSecretsSchema = z.object({
apiKey: z.string(),
})
wireSecret({
name: 'sendgrid',
displayName: 'SendGrid API Key',
description: 'API key for sending emails',
secretId: 'SENDGRID_API_KEY',
schema: sendgridSecretsSchema,
})
Define Credentialsβ
For per-user credentials or OAuth2:
// google-sheets.credential.ts
import { wireCredential } from '@pikku/core/credential'
import { z } from 'zod'
wireCredential({
name: 'googleSheets',
displayName: 'Google Sheets',
type: 'wire',
schema: z.object({
accessToken: z.string(),
refreshToken: z.string().optional(),
}),
oauth2: {
appCredentialSecretId: 'GOOGLE_OAUTH_APP',
tokenSecretId: 'GOOGLE_OAUTH_TOKENS',
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
},
})
See Credentials for the full reference.
Create Servicesβ
Use pikkuAddonServices to create your service factory. It receives the consumer's shared services (logger, secrets, variables):
// 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 }
}
)
For per-user credentials, use pikkuAddonWireServices instead β the credential is resolved per-request via the wire:
// services.ts
import { StripeService } from './stripe-api.service.js'
import { pikkuAddonWireServices } from '#pikku'
export const createWireServices = pikkuAddonWireServices(
async ({ variables }, wire) => {
const cred = await wire.getCredential<{ apiKey: string }>('stripe')
const stripe = new StripeService(cred)
return { stripe }
}
)
Write Functionsβ
Functions use your injected services like any other Pikku function:
import { pikkuSessionlessFunc } from '#pikku'
import { z } from 'zod'
export const mailSend = pikkuSessionlessFunc({
description: 'Sends an email through SendGrid',
input: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
output: z.object({ success: z.boolean() }),
func: async ({ sendgrid }, data) => {
await sendgrid.request('POST', '/mail/send', { body: data })
return { success: true }
},
})
Export Functionsβ
Export all functions from src/index.ts:
export { mailSend } from './functions/mail/send.function.js'
export { listCreate } from './functions/lists/create.function.js'
OpenAPI Generationβ
When you pass --openapi, the CLI parses the spec and generates:
- Zod schemas for all request/response types
- Function files for each API operation
- Typed API service with correct endpoints
- MCP tool metadata (with
--mcp)
npx pikku new addon github --openapi ./github-openapi.yaml --camelCase --mcp
The --camelCase flag converts snake_case property names from the spec to camelCase in the generated Zod schemas. The generated contract hash is stored in pikku.config.json so you can detect when the upstream API changes.
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/*": "./dist/.pikku/*"
}
}
Build and publish:
npx pikku all && tsc && npm publish
Contract Versioningβ
The scaffold generates a versions.pikku.json manifest. Use version commands to track breaking changes:
npx pikku versions check # Validate contracts against manifest
npx pikku versions update # Update manifest with current hashes
Trigger Sourcesβ
Addons can export trigger source functions that consumers wire to their own handlers:
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, call trigger.invoke() on changes
},
})
Consumers wire this to their own handler β see Consuming Addons.