Skip to content

Writing & Deploying Functions

Write a function

A function is a Deno HTTP handler. The simplest form:

functions/hello.ts
Deno.serve(async (req) => {
const { name = 'world' } = await req.json().catch(() => ({}))
return Response.json({ message: `Hello, ${name}!` })
})

A more realistic one that uses the injected project context to read the database:

functions/recent-orders.ts
Deno.serve(async (req) => {
const base = req.headers.get('X-AnvilBase-URL')!
const pid = req.headers.get('X-AnvilBase-Project-ID')!
const key = req.headers.get('X-AnvilBase-Service-Role-Key')!
const res = await fetch(
`${base}/v1/rest/${pid}/orders?order=created_at.desc&limit=10`,
{ headers: { apikey: key } },
)
const orders = await res.json()
return Response.json({ orders })
})

Deploy

All three deploy paths — CLI, management API, and Console — write to the store of record (the edge_functions table + the project’s MinIO bucket). This is the canonical, durable deploy: the record persists across restarts, appears in the functions listing, and carries the function’s verify_jwt flag. The Deno runtime then lazily rehydrates the source from the store of record on the function’s first invocation (a cold cache-miss), so the function goes live without a separate runtime push.

CLI

Reads ./functions/<name>.ts or ./functions/<name>/index.ts (the Supabase layout) — whichever exists (override the directory with --path):

./functions/hello/index.ts
# Deploys ./functions/hello.ts
anvilbase functions deploy hello --project <id>
anvilbase functions deploy hello --project <id>
# Custom directory: deploys ./src/edge/webhook.ts
anvilbase functions deploy webhook --project <id> --path ./src/edge
# List deployed functions (reads the durable management store)
anvilbase functions list --project <id>

The CLI deploys through the management API with UPSERT semantics: it first tries PUT /api/v1/projects/<id>/functions/<name> (update) and falls back to POST /api/v1/projects/<id>/functions (create) when the function doesn’t exist yet. On success it prints the function’s invocation URL and confirms the deploy is persisted to the canonical store.

Durable by default

anvilbase functions deploy persists to the store of record (MinIO + the edge_functions table) just like the management API and Console — so a CLI-deployed function survives container restarts, appears in the functions listing, and carries its persisted verify_jwt flag. It becomes live on its first invocation via lazy rehydration. (Earlier versions wrote only the ephemeral Deno runtime filesystem and were lost on restart — that is no longer the case.)

A new CLI deploy defaults verify_jwt to true (secure-by-default); set a function public via the management API or Console (verify_jwt: false).

Management API

Create or update with the full code body:

Terminal window
# Create (verify_jwt defaults to true)
curl -X POST http://localhost:39001/api/v1/projects/<id>/functions \
-H "Authorization: Bearer $ANVILBASE_TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "hello",
"code": "Deno.serve(() => Response.json({ ok: true }))",
"description": "health probe",
"verify_jwt": false
}'
# Update code / settings
curl -X PUT http://localhost:39001/api/v1/projects/<id>/functions/hello \
-H "Authorization: Bearer $ANVILBASE_TOKEN" -H "Content-Type: application/json" \
-d '{"code":"Deno.serve(() => Response.json({ ok: true, v: 2 }))"}'
# List / get / delete
curl http://localhost:39001/api/v1/projects/<id>/functions -H "Authorization: Bearer $ANVILBASE_TOKEN"
curl http://localhost:39001/api/v1/projects/<id>/functions/hello -H "Authorization: Bearer $ANVILBASE_TOKEN"
curl -X DELETE http://localhost:39001/api/v1/projects/<id>/functions/hello -H "Authorization: Bearer $ANVILBASE_TOKEN"

Rules: function name is 1–100 chars ([A-Za-z0-9_-]); code ≤ 10 MB; 409 if the name already exists on create; quota breaches return 429.

Console

Project → FunctionsNew Function opens an editor. Write code, toggle Verify JWT, and Deploy. Logs and invocations appear in the same view.

Invoke

Terminal window
# verify_jwt: true → must send a valid token
curl -X POST "http://localhost:39001/v1/functions/<id>/hello" \
-H "Authorization: Bearer $USER_JWT" -H "Content-Type: application/json" \
-d '{"name":"Ada"}'
# verify_jwt: false → public
curl -X POST "http://localhost:39001/v1/functions/<id>/hello" \
-H "Content-Type: application/json" -d '{"name":"world"}'

From an SDK: functions.invoke()

Every AnvilBase SDK ships a functions.invoke(name, options) that is wire-compatible with supabase.functions.invoke() — it POSTs to /functions/v1/{name} with the client’s live auth header, JSON-serializes the body (unless it’s a string/binary), parses the response by content-type, and returns { data, error } (a non-2xx becomes error).

// TypeScript
const { data, error } = await db.functions.invoke('hello', { body: { name: 'Ada' } })
# Python
res = db.functions.invoke("hello", body={"name": "Ada"}) # res.data / res.error
// Go
res := client.Functions().Invoke("hello", &anvilbase.FunctionInvokeOptions{
Body: map[string]any{"name": "Ada"},
})

The Rust, Swift, Kotlin, and Elixir clients expose the same functions.invoke shape (a JSON/text/raw body, optional per-call headers, and an optional method, default POST).

Using project secrets (Deno.env)

Don’t hard-code third-party keys in function source. Store them in the project’s Secrets Vault and read them at runtime as environment variables via Deno.env.get(NAME) — the same shape as Supabase supabase secrets set:

Terminal window
# Set a secret (CLI / management API). Name and value are two positional args.
anvilbase secrets set STRIPE_KEY sk_live_... --project <id>
Deno.serve(async () => {
// Read the secret straight from the environment — no helper, no round-trip.
const stripeKey = Deno.env.get('STRIPE_KEY')!
const r = await fetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: { Authorization: `Bearer ${stripeKey}` },
body: new URLSearchParams({ amount: '1000', currency: 'usd' }),
})
return Response.json(await r.json())
})

How it works: a project’s secrets are decrypted and injected into that project’s worker process when the worker cold-starts. Each project has its own worker (see Runtime & Isolation), so a function only ever sees its own project’s secrets — never another tenant’s. Reserved/system environment names (PROJECT_ID, FUNCTIONS_DIR, DENO_*, …) cannot be overridden by a secret.

Propagation latency

A changed secret takes effect on the worker’s next cold start. A long-lived warm worker keeps the values it started with; redeploy the function (or wait for the idle worker to be reaped) to pick up a new value immediately.

service_role for public functions

A public (verify_jwt: false) function invoked without a credential does not receive the X-AnvilBase-Service-Role-Key header (see Overview → Authentication). If such a function needs service_role for a privileged DB write, store the service-role key as a secret and read it with Deno.env.get('SERVICE_ROLE_KEY').

Keep secrets out of source and out of client bundles — the function runs server-side, which is exactly where they belong.

Viewing invocation logs

Every invocation is captured — its status, duration, method, and timestamp — into a bounded per-project ring store (capture is fire-and-forget, so it never slows the invocation). Read the most recent invocations for a function via the management API:

Terminal window
# Newest-first; ?limit= is clamped to 1..=500 (default 50)
curl "http://localhost:39001/api/v1/projects/<id>/functions/hello/logs?limit=20" \
-H "Authorization: Bearer $ANVILBASE_TOKEN"
# → [{ "function_name":"hello", "status":200, "method":"POST",
# "duration_ms":12, "message":"", "created_at":"…" }, …]

The function’s stdout/stderr is streamed to the container log (and the console Functions view); this endpoint records the per-invocation status/duration metadata. The richer logs API is tracked for a later phase and reads the same store.

Receiving inbound webhooks

A function with verify_jwt: false is a clean way to receive third-party webhooks (Stripe, GitHub, etc.). Verify the provider’s signature inside the function:

Deno.serve(async (req) => {
const sig = req.headers.get('Stripe-Signature')
const body = await req.text()
// The signing secret comes from the project secrets (Deno.env), never source.
const signingSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET')!
if (!verifyStripeSignature(body, sig, signingSecret)) {
return new Response('bad signature', { status: 400 })
}
// …handle the event…
return new Response('ok')
})

Because the function is verify_jwt: false, Stripe can POST to it directly with no AnvilBase credential — the request reaches your handler, which does its own signature verification.

For AnvilBase → outbound webhooks (your own DB/auth events to external URLs), use the built-in Webhooks service instead.

Tips

  • Return Response/Response.json() — the runtime is a standard Deno HTTP handler.
  • Keep functions small and single-purpose; a project’s worker cold-starts fast but heavy dependencies add latency (warm reuse is a single loopback hop). See Runtime & Isolation.
  • Use verify_jwt: true unless you have a specific reason for a public endpoint.
  • Log to stdout; view logs in the console Functions view.

Next: Queues (PGMQ).