Writing Skills
writing-skills
效率与协作技能,来自 obra/superpowers,查看原文获取完整用途和操作说明。
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
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
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.