Skip to content
Development
Skill

/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

From plugin
butterbase-skills
53639 skills33 commands1 MCP

Claude Code

$ npx -y skills add butterbase-ai/butterbase-skills --skill function-dev --agent claude-code

SKILL.md

function-dev.SKILL.md
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:

Read more
@butterbase-ai
Ships withbutterbase-skills

Claude Code plugin for Butterbase — the AI-Native Backend-as-a-Service. This plugin gives Claude deep knowledge of Butterbase's nearly 40 MCP tools, guides you through common workflows, and auto-configures the MCP server connection.

53639 skills1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
536
Star history of butterbase-ai/butterbase-skills on GitHub, a chart by star-history.com

Other skills on butterbase-skills.

agents

agents

Use when designing, deploying, or debugging a Butterbase Agent (declarative LLM/tool graph), registering an MCP server for tool…

ai

ai

Use when calling the app's AI gateway from agent tools — chat completions, embeddings, listing models, configuring defaults or…

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills2w309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc2w293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react5d252k
@facebook@facebookInstall Skill

Questions about function-dev.

Which coding agents can use function-dev?
function-dev installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.