packages/cli/skills/pikku-fabric/SKILL.md
Build and convert apps for the Pikku Fabric platform. Covers SQLite/libSQL database setup with Kysely, fabric project layout, deploy provider config, `fabric.config.json`, and the pikku-verify workflow. TRIGGER when: user is working on a Fabric-hosted Pikku project, converting an app to Fabric format, or asking about Fabric deployment, database, or project conventions. DO NOT TRIGGER when: user is working on a generic (non-Fabric) Pikku deployment — use pikku-deploy-cloudflare, pikku-deploy-fastify, etc. instead.
npx skillsauth add pikkujs/pikku pikku-fabricInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill as an execution checklist, not reference material.
pikku fabric validate --json
This prints every missing file, misconfigured field, and dependency gap with a fixHint. Address all error findings before proceeding — they block deploy. Resolve warn findings before testing — they cause runtime failures. info findings are best-practice gaps that are safe to defer.pikku-meta when available; otherwise run the relevant pikku meta ... --json command and inspect only the focused output you need..pikku, node_modules, vendored packages, or broad build artifacts.pikku-verify or pikku all when functions, wirings, schemas, or generated clients may have changed.Fabric is a serverless deployment platform for Pikku apps. Every Fabric app runs on Cloudflare Workers with a SQLite database (via libSQL/Turso). This skill covers what's unique to Fabric. For general Pikku concepts, function authoring, HTTP wiring, and more, see pikku-concepts, pikku-http, pikku-services, etc.
Always run project discovery first:
yarn pikku meta context --json
In OpenCode, call the pikku-meta tool before grepping or editing a Fabric app.
section: "context" for the project map: functions, wires, workflows, capabilities, and source files.section: "clients" before frontend/RPC work.section: "functions" to list function ids, then section: "function", id: "<functionId>" for one function.section: "schemas" to list schema names. Only request full JSON Schema bodies with schemas: ["SchemaName"] for the specific schemas needed.Do not load every schema body by default; that wastes context and usually makes the model worse.
For database work in OpenCode:
pikku-db for the actual attached Fabric database state: tables, columns, foreign keys, and applied migrations.pikku-meta section: "schemas" for code-level JSON Schema contracts, not database introspection.Fabric apps use SQLite, accessed via Kysely with the libSQL HTTP adapter. NOT PostgreSQL, NOT D1.
services.tsimport { Kysely, CamelCasePlugin } from 'kysely'
import { LibsqlWebDialect } from '@pikku/kysely-sqlite'
import type { DB } from './types/db.types.js'
const databaseUrl = await variables.get('DATABASE_URL')
let kysely: Kysely<DB>
if (databaseUrl) {
kysely = new Kysely<DB>({
dialect: new LibsqlWebDialect({ url: databaseUrl }),
plugins: [new CamelCasePlugin()],
})
} else if (existingServices?.kysely) {
kysely = existingServices.kysely as Kysely<DB>
} else {
throw new Error('kysely not provided and DATABASE_URL is unset')
}
Fabric injects DATABASE_URL as a variable binding when the stage starts. In local dev, pikku db migrate uses a local dev.db SQLite file.
Migrations are plain .sql files in packages/functions/db/migrations/, numbered sequentially:
db/migrations/
0001-init.sql
0002-add-users.sql
Run migrations: pikku db migrate
Generate Kysely types after migrations: yarn db:types (uses kysely-codegen)
NEVER hand-edit src/types/db.types.ts — it is generated by kysely-codegen.
SERIAL/INTEGER PRIMARY KEY AUTOINCREMENT for IDsTEXT for strings, INTEGER for booleans (0/1) and timestamps (Unix ms)CHECK constraints sparingly — prefer app-level validationCamelCasePlugin)pikku.config.json (in the project root, not packages/functions/) must declare the Fabric deploy provider:
{
"deploy": {
"providers": {
"cloudflare": "@pikkufabric/deploy-cloudflare"
}
}
}
Without this, pikku deploy plan --provider cloudflare uses the OSS adapter which lacks Fabric's workflow service wiring.
The Fabric adapter automatically:
SQLiteKyselyWorkflowService when DATABASE_URL is boundworkflowQueues: true for the scaffoldNo manual workflow service setup is needed.
packages/functions/
src/
functions/ # Business logic — one pikkuFunc/workflow per file
wirings/ # Transport bindings
*.http.ts # wireHTTP / defineHTTPRoutes / wireHTTPRoutes
*.channel.ts # wireChannel
*.queue.ts # wireQueueWorker
*.schedule.ts # wireScheduler
*.mcp.ts # wireMCPTool
*.cli.ts # wireCLI
services.ts # pikkuServices factory (singleton)
middleware.ts # Shared middleware
permissions.ts # Shared permissions
types/
db.types.ts # Generated by kysely-codegen — NEVER hand-edit
db/
migrations/ # Plain .sql files, numbered sequentially
.pikku/ # Generated by pikku all — gitignored
pikku.config.json # Pikku + deploy config (project root)
fabric.config.json # Fabric environment + frontend config (project root)
fabric.config.jsonDeclares environments and frontend dev servers:
{
"projectId": "my-project-id",
"production": {
"branch": "main"
},
"frontends": {
"app": {
"cwd": "apps/next-app",
"primary": true,
"deploy": true,
"kind": "ssr",
"dev": {
"command": ["yarn", "dev"],
"port": 7105,
"healthPath": "/"
}
}
}
}
production.branch: the git branch that maps to the production stagefrontends: each entry declares a frontend app with its dev command and portIn Fabric apps, most features don't need HTTP wirings. Just write the function with expose: true — Pikku generates an RPC client and React Query hooks automatically.
export const listTasks = pikkuSessionlessFunc({
expose: true,
readonly: true,
func: async ({ kysely }, {}) => {
return { tasks: await kysely.selectFrom('tasks').selectAll().execute() }
},
})
Add wireHTTP only when you need a specific REST shape (webhooks, third-party callers).
expose: true.expose: true for public/generated client access unless the user explicitly wants a private function.missing description, the work is not finished yet.Functions with expose: true are versioned via versions.pikku.json. When you change a function's input or output schema, you must bump its version number — otherwise pikku all will report a breaking change and callers' generated clients become stale.
The pikku-verify tool catches this automatically.
Always call the pikku-verify tool after modifying functions, wirings, or schemas. It runs:
pikku all — regenerates all codegen, checks version compliancetsc --noEmit — validates TypeScript typesThe output card shows whether any breaking changes were detected.
These apply in every Fabric app:
process.env — use variables.get('NAME') and secrets.getSecret('NAME'). Declare with wireVariable / wireSecret.as any — fix types properly.Error — throw NotFoundError, ConflictError, BadRequestError, UnauthorizedError from @pikku/core/errors.permissions: field on the function config with a pikkuPermission factory.db.types.ts — always regenerate with yarn db:types after migrations.pikkuSessionlessFunc step functions in *.steps.ts files are auto-discovered by codegen.Start by running the structural validator — it tells you exactly what is missing:
pikku fabric validate --json
Fix every error and warn in the output before continuing. Then:
db/migrations/.pikkuFunc/pikkuSessionlessFunc, add wireHTTP or expose: true for transport.createSingletonServices in services.ts.process.env calls with wireVariable/wireSecret + variables.get().pikku.config.json at project root with srcDirectories, outDir, and clientFiles.fabric.config.json at project root with projectId, production.branch, and frontends.pikku all — verify codegen succeeds and there are no type errors.pikku fabric validate once more to confirm no structural issues remain.documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.
development
Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to "support RTL / right-to-left / bidi / mirror the layout", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.
development
Wire i18n into a Pikku frontend (Vite SPA, Vite SSR, or Next.js app-router) with react-i18next + i18next. English by default, every user-facing string goes through a `t()` token, and additional languages are served under `/de` `/es` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to "make this translatable / use tokens / add i18n". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.
development
Use when translating an n8n Code node body into a real Pikku function body. Triggered when the user opens or points at a stub generated by @pikku/n8n-import (look for `STUB — generated from n8n Code node` in the file's JSDoc), or when the user says 'translate this n8n code', 'port this n8n code node', 'finish the codeStub__... function', etc. The stub file is a `pikkuSessionlessFunc` with a Zod input/output, a JSDoc preserving the original n8n JavaScript verbatim, and a `throw new Error('… — implement me')` body.