Three steps. Bulletproof contracts.
Every function's API surface is tracked, versioned, and enforced automatically.
Function name + input schema + output schema are hashed into a deterministic 16-character hex contract hash.
Hashes are stored in a versions.pikku.json manifest, committed to Git alongside your code. Every version has its own hash.
The CLI compares current contracts against the manifest on every build. Changed contract without a version bump? Build fails.
{
"manifestVersion": 1,
"contracts": {
"getItem": {
"latest": 1,
"versions": {
"1": "a1b2c3d4e5f6g7h8"
}
},
"listItems": {
"latest": 2,
"versions": {
"1": "i9j0k1l2m3n4o5p6",
"2": "q7r8s9t0u1v2w3x4"
}
}
}
}
One line in CI. Zero accidental breaking changes.
Add pikku versions check to your pipeline. Breaking changes fail the build with an actionable fix — before they reach production.
$ npx pikku versions check ✗ getItem — contract changed without version bump Input schema hash: a1b2c3d4 → f9e8d7c6 Output schema hash: i9j0k1l2 → z5y4x3w2 Run: npx pikku versions update after bumping to version 2
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx pikku versions check
v1 and v2 coexist.
Running workflows and AI agents keep working. New code gets the latest version. Both run across every wire — no migration needed.
export const GetItemOutputV1 = z.object({
itemId: z.string(),
name: z.string(),
priceCents: z.number(),
});
export const getItemV1 = pikkuSessionlessFunc({
expose: true,
version: 1,
input: GetItemInput,
output: GetItemOutputV1,
func: async ({ kysely }, { itemId }) => {
const row = await kysely
.selectFrom('item')
.select(['itemId', 'name', 'priceCents'])
.where('itemId', '=', itemId)
.executeTakeFirstOrThrow();
return row;
},
});
// v2 — adds stock and imageUrl to the response
export const getItemV2 = pikkuSessionlessFunc({
expose: true,
version: 2,
input: GetItemInput,
output: GetItemOutput,
func: async ({ kysely }, { itemId }) => {
const row = await kysely
.selectFrom('item')
.innerJoin(
'category',
'category.categoryId',
'item.categoryId',
)
.select([
'item.itemId',
'item.name',
'item.slug',
'item.description',
'item.priceCents',
'item.stock',
'item.imageUrl',
'item.isActive',
'item.createdAt',
'item.updatedAt',
'category.categoryId',
'category.name as categoryName',
'category.slug as categorySlug',
])
.where('item.itemId', '=', itemId)
.executeTakeFirstOrThrow();
return {
...row,
category: {
categoryId: row.categoryId,
name: row.categoryName,
slug: row.categorySlug,
},
};
},
});
What the system promises you.
Versioning you can trust — so you can evolve fast without worrying about what breaks.
Once a version is published, its contract can never silently change. Running workflows get exactly what they expect.
Modify a schema and the CLI knows immediately. No manual diffing, no guessing — it tells you exactly what changed.
You decide when to create a new version. The system won't let you accidentally ship a breaking change — you have to mean it.
Five steps. Every time.
A repeatable workflow that makes breaking changes intentional — never accidental.
Change function inputs or outputs
Detect the contract change
npx pikku versions checkIncrement version in your function
version: 2Record the new contract hash
npx pikku versions updateCheck in the updated manifest
git commit -am "bump getBook v2"Start versioning in 30 seconds.
One command to initialize the manifest. Every function contract is tracked from that moment on.
MIT Licensed · Works with Express, Fastify, Lambda & Cloudflare