/function-dev
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 -y skills add butterbase-ai/butterbase-skills --skill function-dev --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/function-dev
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
function-dev.SKILL.mdname: function-dev
description: 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
Serverless Function Development on Butterbase
Guide for developing and deploying serverless functions on Butterbase's Deno runtime. Covers handler signatures, trigger types, database access, environment variables, and testing.
---
1. Handler Signature
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!---
2. Trigger Types
HTTP Trigger
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 `null`
---
Cron Trigger
Execute 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 9am
Cron functions run as `butterbase_service` (RLS bypassed). `ctx.user` is always `null`.
---
WebSocket Trigger
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.
---
S3 Upload Trigger _(placeholder — not yet implemented)_
{
"trigger": {
"type": "s3_upload",
"config": { "prefix": "uploads/", "contentTypes": ["image/*"] }
}
}---
3. Database Access
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
);SELECT
const { rows } = await ctx.db.query(
'SELECT * FROM posts WHERE author_id = $1 AND published = true',
[ctx.user.id]
);INSERT
await ctx.db.query(
'INSERT INTO logs (event, user_id) VALUES ($1, $2)',
['page_view', ctx.user.id]
);
UPDATE
await ctx.db.query(
'UPDATE posts SET title = $1, updated_at = now() WHERE id = $2 AND author_id = $3',
[newTitle, postId, ctx.user.id]
);
RLS Behavior by Invocation Type
| 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 |
---
4. Environment Variables
- **Set at deploy time**: pass `envVars` parameter to `deploy_function`
- **Update without redeploying**: use `update_function_env`
- **Access in handler**: `ctx.env.VARIABLE_NAME`
- **Encrypted at rest**: values are never exposed in logs or API responses
Common 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;
---
5. Complete Working Examples
Example 1 — Protected API Endpoint (auth: required)
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" }
}
)---
Example 2 — Webhook Receiver (auth: none)
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:
Read more
name: function-dev description: 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
Serverless Function Development on Butterbase
Guide for developing and deploying serverless functions on Butterbase's Deno runtime. Covers handler signatures, trigger types, database access, environment variables, and testing.
---
1. Handler Signature
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!---
2. Trigger Types
HTTP Trigger
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 `null`
---
Cron Trigger
Execute 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 9am
Cron functions run as `butterbase_service` (RLS bypassed). `ctx.user` is always `null`.
---
WebSocket Trigger
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.
---
S3 Upload Trigger _(placeholder — not yet implemented)_
{
"trigger": {
"type": "s3_upload",
"config": { "prefix": "uploads/", "contentTypes": ["image/*"] }
}
}---
3. Database Access
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
);SELECT
const { rows } = await ctx.db.query(
'SELECT * FROM posts WHERE author_id = $1 AND published = true',
[ctx.user.id]
);INSERT
await ctx.db.query( 'INSERT INTO logs (event, user_id) VALUES ($1, $2)', ['page_view', ctx.user.id] );
UPDATE
await ctx.db.query( 'UPDATE posts SET title = $1, updated_at = now() WHERE id = $2 AND author_id = $3', [newTitle, postId, ctx.user.id] );
RLS Behavior by Invocation Type
| 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 |
---
4. Environment Variables
- **Set at deploy time**: pass `envVars` parameter to `deploy_function`
- **Update without redeploying**: use `update_function_env`
- **Access in handler**: `ctx.env.VARIABLE_NAME`
- **Encrypted at rest**: values are never exposed in logs or API responses
Common 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;
---
5. Complete Working Examples
Example 1 — Protected API Endpoint (auth: required)
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" }
}
)---
Example 2 — Webhook Receiver (auth: none)
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:
Claude Code plugin for Butterbase — the AI-Native Backend-as-a-Service. This plugin gives Claude deep knowledge of Butterbase's 42+ MCP tools, guides you through common workflows, and auto-configures the MCP server connection.
Repo: butterbase-ai/butterbase-skills
Other skills on butterbase-skills.
- /agents
Use when designing, deploying, or debugging a Butterbase Agent (declarative LLM/tool graph), registering an MCP server for tool use, or wiring access controls and rate limits. Agents are first-class app resources defined by a `graph_spec` and invoked over
Open skill - /ai
Use when calling the app's AI gateway from agent tools — chat completions, embeddings, listing models, configuring defaults or BYOK, reading token/cost usage
Open skill - /auth-setup
Use when configuring OAuth providers (Google/GitHub/Apple/X/etc.), setting up post-login auth hooks, tuning JWT lifetimes, or generating service API keys
Open skill - /build-app
Use when building a new Butterbase app from scratch, creating a full-stack application, or when the user asks to set up a complete backend with database, auth, and deployment
Open skill - /contributing
Use when contributing to the Butterbase codebase, adding new MCP tools, creating API routes, writing migrations, or understanding the monorepo architecture
Open skill - /debug-rls
Use when users report access denied errors, see wrong data, RLS policies are not working, or when troubleshooting Row-Level Security issues in Butterbase
Open skill

