Use when developing, deploying, or debugging Butterbase serverless functions, or when the user needs to add backend logic like webhooks, scheduled jobs, or custom API endpoints
npx skills add https://github.com/butterbase-ai/butterbase-skills --skill function-dev
Guide for developing and deploying serverless functions on Butterbase's Deno runtime. Covers handler signatures, trigger types, database access, environment variables, and testing.
Every function exports a single handler function with this signature:
export async function handler(
request: Request,
context: {
db: PostgresClient, // RLS-aware DB client
env: Record<string, string>, // env vars set on the function
user: { id: string } | null, // present for HTTP+auth:required; null for cron
waitUntil: (p: Promise<unknown>) => void, // background work after Response (≤30s)
idempotency: {
claim: (key: string, opts?: { scope?: string; ttlSeconds?: number }) => Promise<boolean>
} // atomic dedup for webhook retries
}
): Promise<Response>
CRITICAL: The handler MUST return new Response() (Web API standard). Do NOT return plain objects.
Correct:
return new Response(JSON.stringify({ message: "ok" }), {
status: 200,
headers: { "Content-Type": "application/json" }
});
Wrong (will fail):
return { status: 200, body: "ok" }; // NOT a Response object!
Invoke the function via an HTTP request.
{
"trigger": {
"type": "http",
"config": { "method": "POST", "path": "/my-endpoint", "auth": "required" }
}
}
Auth options:
"required" — request must include a valid JWT; ctx.user is always set"optional" — JWT is parsed if present; ctx.user may be null"none" — public endpoint; no auth needed; ctx.user is always nullExecute the function on a schedule.
{
"trigger": {
"type": "cron",
"config": { "schedule": "0 9 * * *", "timezone": "UTC" }
}
}
Uses standard 5-field cron expressions:
"*/5 * * * *" — every 5 minutes"0 0 * * 0" — weekly, Sunday at midnight"0 3 * * *" — daily at 3am UTC"0 9 * * 1-5" — weekdays at 9amCron functions run as butterbase_service (RLS bypassed). ctx.user is always null.
Fire when a connected client sends a matching event over the realtime WebSocket.
{
"trigger": {
"type": "websocket",
"config": { "event": "chat-message" }
}
}
Fires when client sends matching event via realtime WebSocket connection. The request body contains the event payload sent by the client.
{
"trigger": {
"type": "s3_upload",
"config": { "prefix": "uploads/", "contentTypes": ["image/*"] }
}
}
Use ctx.db.query(sql, params) for all database queries. Always use parameterized queries to prevent SQL injection — NEVER use string interpolation.
// Always use $1, $2 placeholders — never string interpolation
const { rows } = await ctx.db.query(
'SELECT * FROM posts WHERE author_id = $1',
[ctx.user.id] // params array
);
const { rows } = await ctx.db.query(
'SELECT * FROM posts WHERE author_id = $1 AND published = true',
[ctx.user.id]
);
await ctx.db.query(
'INSERT INTO logs (event, user_id) VALUES ($1, $2)',
['page_view', ctx.user.id]
);
await ctx.db.query(
'UPDATE posts SET title = $1, updated_at = now() WHERE id = $2 AND author_id = $3',
[newTitle, postId, ctx.user.id]
);
| Invocation | Role | RLS |
|------------|------|-----|
| End-user JWT | butterbase_user | Enforced — ctx.db queries filtered by policies |
| API key (bb_sk_) | butterbase_service | Bypassed — sees all data |
| Cron trigger | butterbase_service | Bypassed — sees all data |
envVars parameter to deploy_functionupdate_function_envctx.env.VARIABLE_NAMECommon uses: API keys, webhook secrets, external service URLs.
const apiKey = ctx.env.OPENAI_API_KEY;
const webhookSecret = ctx.env.WEBHOOK_SECRET;
const serviceUrl = ctx.env.EXTERNAL_SERVICE_URL;
Returns the authenticated user's posts.
export async function handler(req, ctx) {
const { rows } = await ctx.db.query(
'SELECT id, title, created_at FROM posts WHERE author_id = $1 ORDER BY created_at DESC',
[ctx.user.id]
);
return new Response(JSON.stringify(rows), {
headers: { "Content-Type": "application/json" }
});
}
Deploy:
deploy_function(
app_id,
name: "my-posts",
code: ...,
trigger: {
type: "http",
config: { method: "GET", path: "/my-posts", auth: "required" }
}
)
Accepts an incoming webhook, validates the signature, and stores the event.
export async function handler(req, ctx) {
const body = await req.json();
const signature = req.headers.get("x-webhook-signature");
// Validate signature against ctx.env.WEBHOOK_SECRET
await ctx.db.query(
'INSERT INTO webhook_events (event_type, payload) VALUES ($1, $2)',
[body.type, JSON.stringify(body)]
);
return new Response("ok", { status: 200 });
}
Deploy with: trigger: { type: "http", config: { method: "POST", path: "/webhook", auth: "none" } }
Deletes expired sessions on a nightly schedule.
export async function handler(req, ctx) {
const result = await ctx.db.query(
"DELETE FROM sessions WHERE expires_at < now() RETURNING id"
);
return new Response(JSON.stringify({ deleted: result.rowCount }), {
headers: { "Content-Type": "application/json" }
});
}
Deploy with: trigger: { type: "cron", config: { schedule: "0 3 * * *" } }
Proxies a request to an external AI service using a stored API key.
export async function handler(req, ctx) {
const { prompt } = await req.json();
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${ctx.env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: "gpt-4",
messages: [{ role: "user", content: prompt }]
})
});
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" }
});
}
Deploy with envVars: { OPENAI_API_KEY: "sk-..." } and trigger: { type: "http", config: { method: "POST", auth: "required" } }
Always wrap handler logic in try/catch and return a proper error Response.
export async function handler(req, ctx) {
try {
const { id } = await req.json();
const { rows } = await ctx.db.query(
'SELECT * FROM items WHERE id = $1',
[id]
);
if (rows.length === 0) {
return new Response(JSON.stringify({ error: "Not found" }), {
status: 404,
headers: { "Content-Type": "application/json" }
});
}
return new Response(JSON.stringify(rows[0]), {
status: 200,
headers: { "Content-Type": "application/json" }
});
} catch (err) {
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
}
The standalone tools deploy_function and invoke_function are unchanged. Everything else (logs, env updates, listing, deletion) is handled by manage_function with an action parameter.
invoke_function(
app_id: "app_abc123",
function_name: "my-function",
method: "POST",
body: { key: "value" }
)
Returns the full HTTP response (status, headers, body, duration_ms). Use this immediately after deploying to verify behavior.
manage_function(
app_id: "app_abc123",
action: "get_logs",
function_name: "my-function",
level: "error"
)
Returns recent invocations with errors, stack traces, and captured console.log/warn/error output.
manage_function(
app_id: "app_abc123",
action: "get_logs",
function_name: "my-function",
limit: 100,
since: "2026-01-15T00:00:00Z"
)
Filters: limit (default 100), since (ISO timestamp), level ("error" or "all").
manage_function(app_id: "app_abc123", action: "list")
Returns each function's name, trigger, URL, status, and metrics (invocationCount, errorRate, avgDuration, lastInvoked).
| Mistake | Fix |
|---------|-----|
| Returning plain object instead of Response | Always use new Response(JSON.stringify(data), { headers: {...} }) |
| SQL injection via string interpolation | Use parameterized queries: $1, $2 placeholders |
| Not wrapping in try/catch | Always catch errors and return a Response with error status |
| Forgetting async on handler | Handler must be async function handler(...) |
| Exceeding timeout (30s default) | Increase timeoutMs in deploy_function or optimize the function |
| Not setting Content-Type header | Always include "Content-Type": "application/json" for JSON responses |
deploy_function(
app_id: "app_abc123",
name: "my-function",
code: "export async function handler(req, ctx) { ... }",
trigger: { type: "http", config: { method: "POST", auth: "required" } },
envVars: { MY_SECRET: "value" },
timeoutMs: 30000, // default: 30s, max: 300s
memoryLimitMb: 128 // default: 128MB
)
manage_function(
app_id: "app_abc123",
action: "update_env",
function_name: "my-function",
env: { MY_SECRET: "new-value", DELETE_ME: null } // null deletes the key
)
manage_function(
app_id: "app_abc123",
action: "delete",
function_name: "my-function"
)
https://api.butterbase.ai/v1/{app_id}/fn/{function-name}
For HTTP triggers, this is the URL clients call directly.
If a docs/butterbase/00-state.md exists in the working directory, prefer invoking via /butterbase-skills:journey-functions so the journey orchestrator stays in sync.
Run Python code in the cloud with serverless containers, GPUs, and autoscaling. Use when deploying ML models, running batch processing jobs, scheduling compute-intensive tasks, or serving APIs that require GPU acceleration or dynamic scaling.
Advanced GitHub Actions workflow automation with AI swarm coordination, intelligent CI/CD pipelines, and comprehensive repository management
Google Cloud Platform CLI - manage GCP resources including Compute Engine, Cloud Run, GKE, Cloud Functions, Storage, BigQuery, and more.
Expert backend architect specializing in scalable API design, microservices architecture, and distributed systems. Masters REST/GraphQL/gRPC APIs, event-driven architectures, service mesh patterns, and modern backend frameworks. Handles service boundary definition, inter-service communication, resilience patterns, and observability. Use PROACTIVELY when creating new backend services or APIs.
Run Python code in the cloud with serverless containers, GPUs, and autoscaling. Use when deploying ML models, running batch processing jobs, scheduling compute-intensive tasks, or serving APIs that require GPU acceleration or dynamic scaling.
Aspire skill covering the Aspire CLI, AppHost orchestration, service discovery, integrations, MCP server, VS Code extension, Dev Containers, GitHub Codespaces, templates, dashboard, and deployment. Use when the user asks to create, run, debug, configure, deploy, or troubleshoot an Aspire distributed application.
Audits Python + BigQuery pipelines for cost safety, idempotency, and production readiness. Returns a structured report with exact patch locations.
Microsoft Store Developer CLI (msstore) for publishing Windows applications to the Microsoft Store. Use when asked to configure Store credentials, list Store apps, check submission status, publish submissions, manage package flights, set up CI/CD for Store publishing, or integrate with Partner Center. Supports Windows App SDK/WinUI, UWP, .NET MAUI, Flutter, Electron, React Native, and PWA applications.
Take butterbase-ai/function-dev from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.