Deploy or update a serverless function with custom business logic.
Example:
Input: {
app_id: "app_abc123",
name: "send-welcome-email",
code: "export async function handler(req, ctx) { ... }",
trigger: {
type: "http",
config: { method: "POST", path: "/welcome", auth: "required" }
}
}
Output: {
function_id: "fn_xyz789",
name: "send-welcome-email",
url: "https://api.butterbase.ai/v1/app_abc123/fn/send-welcome-email",
status: "deployed"
}
Function signature:
export async function handler(request: Request, context: {
db: PostgresClient, // Query your app database
env: Record<string, string>, // Access envVars
user: { id: string } | null, // Current user (if auth: required)
waitUntil: (promise: Promise) => void, // Keep alive for background work after response
idempotency: { // Webhook / event dedup primitive
claim: (key: string, opts?: { scope?: string; ttlSeconds?: number }) => Promise<boolean>
}
}): Promise<Response>
Console output: console.log(), console.info(), console.warn(), console.error(), and console.debug() calls are captured and stored with invocation logs. View them via manage_function (action: "get_logs").
IMPORTANT: Handlers MUST return a Response object (Web API standard).
Do NOT return plain objects like { status: 200, body: "..." }.
Idempotent webhook handlers with ctx.idempotency.claim():
Third-party webhook providers (Stripe, Telegram, GitHub, Slack, Twilio, Discord)
retry delivery on non-2xx responses with the same event id. Use ctx.idempotency.claim()
to atomically dedupe — it returns true if you're the first to see this key, false if
another invocation already claimed it.
export async function handler(req, ctx) {
const event = await req.json();
if (!(await ctx.idempotency.claim(event.id))) {
// Already processed — ack the retry without re-doing work.
return new Response('duplicate', { status: 200 });
}
await processEvent(event);
return new Response('ok', { status: 200 });
}
Options:
- scope: 'stripe' | 'telegram' | ... (default: 'default'). Namespace claims so
keys from different providers can never collide.
- ttlSeconds: mark the claim with an expiry. Cleanup is your responsibility:
DELETE FROM _idempotency_keys WHERE expires_at < now();
Background work with ctx.waitUntil():
Use ctx.waitUntil(promise) to keep the function alive after the response is sent.
This is useful for fire-and-forget tasks like sending emails or logging.
Background work has a 30-second timeout. ctx.db is available inside waitUntil promises.
export async function handler(req, ctx) {
ctx.waitUntil(fetch("https://api.email.com/send", { method: "POST", body: "..." }));
return new Response(JSON.stringify({ accepted: true }), {
headers: { "Content-Type": "application/json" }
});
}
Example:
export async function handler(req, ctx) {
const data = { hello: "world" };
return new Response(JSON.stringify(data), {
status: 200,
headers: { "Content-Type": "application/json" }
});
}
Row-Level Security in Functions:
Functions respect RLS policies based on how they're invoked:
- Invoked with end-user JWT → butterbase_user role (RLS enforced)
* ctx.db queries see only the user's data
* ctx.user.id contains the authenticated user ID
* Use case: User-facing operations
- Invoked with platform API key → butterbase_service role (RLS bypassed)
* ctx.db queries see all data
* ctx.user is null
* Use case: Admin operations, background jobs
- Invoked by cron trigger → butterbase_service role (RLS bypassed)
* ctx.db queries see all data
* ctx.user is null
* Use case: Scheduled tasks, cleanup jobs
Trigger types:
- http: Invoke via HTTP request (GET, POST, etc)
- cron: Schedule periodic execution (e.g., "0 9 * * *" = daily at 9am)
- websocket: Trigger on WebSocket event from client via realtime connection
- s3_upload: Trigger on file upload [not yet implemented]
- webhook: Receive webhooks from external services [not yet implemented]
Common errors:
- VALIDATION_INVALID_SCHEMA: Check code exports a handler function
- RESOURCE_NOT_FOUND: App doesn't exist
- Syntax error: Code must be valid TypeScript/JavaScript
Idempotency: Safe to call multiple times (updates existing function with same name).
Next steps: Use invoke_function to test, then manage_function (action: "get_logs") to debug.