.claude/skills/pikku-security/SKILL.md
Use when adding authentication, authorization, permissions, middleware, or security to a Pikku app. Covers pikkuAuth, pikkuPermission, pikkuMiddleware, built-in auth strategies (bearer, cookie, API key), and permission scoping. TRIGGER when: code uses pikkuAuth/pikkuPermission/pikkuMiddleware, user asks about auth, login, session, permissions, RBAC, middleware, or API keys. DO NOT TRIGGER when: user asks about JWT service setup (use pikku-services) or secrets/env vars (use pikku-config).
npx skillsauth add pikkujs/pikku pikku-securityInstall 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.
Pikku provides a layered security model: authentication middleware (who are you?), auth checks (are you logged in?), and permissions (can you do this?). All work across every transport (HTTP, WebSocket, CLI, queue, etc.).
pikku info middleware --verbose # See existing middleware and where it's applied
pikku info permissions --verbose # See existing permissions
pikku info functions --verbose # See which functions have auth/permissions
See pikku-concepts for the core mental model.
Functions access session via the wire object:
// In pikkuFunc (authenticated)
const getProfile = pikkuFunc({
func: async ({ db }, _data, { session }) => {
return await db.getUser(session.userId)
},
})
// Set session (e.g., after login)
const login = pikkuFunc({
auth: false,
func: async ({ jwt, db }, { email, password }, { setSession }) => {
const user = await db.verifyCredentials(email, password)
setSession({ userId: user.id, role: user.role })
return { token: jwt.sign({ userId: user.id }) }
},
})
// Clear session (logout)
const logout = pikkuFunc({
func: async ({}, _data, { clearSession }) => {
clearSession()
},
})
pikkuAuth(fn) — Session-Only ChecksUse for authentication gates that only need the session (no request data).
import { pikkuAuth } from '#pikku'
// Receives (services, session)
export const isAuthenticated = pikkuAuth(
async (_services, session) => !!session
)
export const isAdmin = pikkuAuth(
async (_services, session) => session?.role === 'admin'
)
pikkuPermission(fn) — Data-Aware ChecksUse when authorization depends on the actual request data.
import { pikkuPermission } from '#pikku'
// Receives (services, data, wire)
export const isOwner = pikkuPermission(
async ({ db }, { bookId }, { session }) => {
const book = await db.getBook(bookId)
return book?.authorId === session?.userId
}
)
export const hasBookAccess = pikkuPermission(
async ({ db }, { bookId }, { session }) => {
return await db.hasAccess(session?.userId, bookId)
}
)
{
admin: isAdmin, // OR: admins can access
owner: isOwner, // OR: owners can access
reviewer: [isAuthenticated, hasBookAccess], // AND: both must pass
}
// Logic: admin OR owner OR (isAuthenticated AND hasBookAccess)
pikkuMiddleware(fn) — Before/After Wrappingimport { pikkuMiddleware } from '#pikku'
const logRequest = pikkuMiddleware(async ({ logger }, wire, next) => {
logger.info('Before')
await next()
logger.info('After')
})
import { authBearer, authCookie, authAPIKey } from '@pikku/core/middleware'
// JWT bearer token — reads Authorization header
addHTTPMiddleware([authBearer()])
// Cookie-based sessions — auto-refreshes JWT
addHTTPMiddleware([
authCookie({
name: 'session',
expiresIn: { value: 30, unit: 'day' },
options: { httpOnly: true, secure: true },
}),
])
// API key — from x-api-key header or ?apiKey= query
addHTTPMiddleware([authAPIKey({ source: 'all' })])
Four levels of scoping, from broadest to narrowest:
// 1. Global: all HTTP routes
addHTTPMiddleware('*', [authBearer()])
// 2. Prefix-based: URL pattern
addHTTPMiddleware('/admin/*', [auditLog])
addHTTPPermission('/admin/*', { admin: requireAdmin })
// 3. Tag-based: any wiring with matching tag
addMiddleware('api', [rateLimiter])
addPermission('api', { auth: requireAuth })
// 4. Inline: per-wiring
wireHTTP({
route: '/books/:id',
func: getBook,
middleware: [cacheControl],
permissions: { owner: requireOwnership },
})
export const deleteBook = pikkuFunc({
func: async ({ db }, { bookId }) => {
await db.deleteBook(bookId)
},
permissions: {
admin: isAdmin,
owner: isOwner,
reviewer: [isAuthenticated, hasBookAccess],
},
})
// permissions.ts
import { pikkuAuth, pikkuPermission } from '#pikku'
export const isAuthenticated = pikkuAuth(
async (_services, session) => !!session
)
export const isAdmin = pikkuAuth(
async (_services, session) => session?.role === 'admin'
)
export const isBookOwner = pikkuPermission(
async ({ db }, { bookId }, { session }) => {
const book = await db.getBook(bookId)
return book?.authorId === session?.userId
}
)
// middleware.ts
import { pikkuMiddleware } from '#pikku'
export const auditLog = pikkuMiddleware(async ({ logger, db }, wire, next) => {
const start = Date.now()
await next()
await db.createAuditLog({
duration: Date.now() - start,
})
})
// wirings/security.wiring.ts
import { authBearer } from '@pikku/core/middleware'
import { addHTTPMiddleware, addHTTPPermission } from '#pikku'
// All routes require bearer auth
addHTTPMiddleware('*', [authBearer()])
// Admin routes need admin permission + audit logging
addHTTPMiddleware('/admin/*', [auditLog])
addHTTPPermission('/admin/*', { admin: isAdmin })
// functions/books.functions.ts
export const deleteBook = pikkuFunc({
title: 'Delete Book',
func: async ({ db }, { bookId }) => {
await db.deleteBook(bookId)
return { deleted: true }
},
permissions: {
admin: isAdmin,
owner: isBookOwner,
},
})
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.