Edge Functions
Deno serverless functions deployed at the edge, one URL per function
Edge functions are server-side TypeScript that runs on Deno, deployed globally at the edge. Use them for anything that can't happen in the browser: webhook receivers, AI proxies, custom auth flows, scheduled jobs. Each function gets its own URL.
https://{project-ref}.functions.supabase.co/{name}Swarmz also proxies every function under your project's API endpoint:
https://api.swarmz.net/v1/functions/{name}Both URLs hit the same code. See the API reference for the full request/response contract.
Writing a function
Functions live in supabase/functions/{name}/index.ts. Each one is a standalone Deno module that exports a request handler via Deno.serve. No build step, no bundler — Deno imports run directly from URLs or npm: specifiers.
Here's a hello-world that echoes a JSON body back:
// supabase/functions/hello/index.ts
Deno.serve(async (req) => {
if (req.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const body = await req.json();
return new Response(
JSON.stringify({
message: `Hello, ${body.name ?? "world"}!`,
received: body,
timestamp: new Date().toISOString(),
}),
{
headers: { "Content-Type": "application/json" },
},
);
});Hit it with curl:
curl -X POST https://api.swarmz.net/v1/functions/hello \
-H "Content-Type: application/json" \
-d '{"name": "Swarmz"}'Deploying
Two ways to deploy:
-
Ask the agent. "Deploy the hello function." It'll run the deploy and report back with the URL.
-
From the editor terminal. Open the Terminal tab inside the editor and run:
supabase functions deploy helloDrop the function name to deploy every function in
supabase/functions/.
Deploys are atomic — the new version goes live across all edge regions in a few seconds. The previous version stays available until the new one is fully rolled out, so there's no cold gap.
Secrets
Don't bake API keys into your code. Set them as secrets — they're injected as environment variables at runtime and never appear in your repo.
supabase secrets set OPENAI_API_KEY=sk-...
supabase secrets set STRIPE_SECRET_KEY=sk_live_...Read them inside the function with Deno.env.get:
const apiKey = Deno.env.get("OPENAI_API_KEY");
if (!apiKey) {
return new Response("Missing OPENAI_API_KEY", { status: 500 });
}A few env vars are auto-provided and don't need to be set:
SUPABASE_URL— your project URLSUPABASE_ANON_KEY— anon key for client-side callsSUPABASE_SERVICE_ROLE_KEY— service role key (bypasses RLS)
The service role key bypasses row-level security. Only use it inside trusted server code — never ship it to the browser.
Auth verification
By default, functions are publicly callable. To require a valid Swarmz JWT, set verify_jwt = true in supabase/config.toml:
[functions.hello]
verify_jwt = trueWhen enabled, the platform validates the Authorization: Bearer <jwt> header before your code runs. Inside the function, decode the JWT to identify the user:
import { createClient } from "npm:@supabase/supabase-js@2";
Deno.serve(async (req) => {
const authHeader = req.headers.get("Authorization");
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: authHeader! } } },
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });
return new Response(JSON.stringify({ userId: user.id }), {
headers: { "Content-Type": "application/json" },
});
});Leave verify_jwt = false for webhook endpoints, public APIs, or anywhere you handle auth yourself (HMAC signatures, API keys, etc.).
CORS
Edge functions don't get CORS handled automatically. If your frontend calls a function from a browser, you need to respond to the preflight OPTIONS request and return the right headers on every response.
The pattern we use everywhere — drop this into a _shared/cors.ts and import it from each function:
// supabase/functions/_shared/cors.ts
const ALLOWED_ORIGINS = [
"http://localhost:5173",
"https://your-app.com",
];
export function getCorsHeaders(req: Request): Record<string, string> {
const origin = req.headers.get("Origin") ?? "";
const isAllowed =
origin.includes("localhost") || ALLOWED_ORIGINS.includes(origin);
return {
"Access-Control-Allow-Origin": isAllowed ? origin : ALLOWED_ORIGINS[0],
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
"Access-Control-Max-Age": "86400",
};
}
export function handleCors(req: Request): Response | null {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: getCorsHeaders(req) });
}
return null;
}Use it in your function:
import { getCorsHeaders, handleCors } from "../_shared/cors.ts";
Deno.serve(async (req) => {
const preflight = handleCors(req);
if (preflight) return preflight;
// ...your logic
return new Response(JSON.stringify({ ok: true }), {
headers: { ...getCorsHeaders(req), "Content-Type": "application/json" },
});
});Limits
| Limit | Value |
|---|---|
| Memory | 256 MB |
| Default timeout | 30 seconds |
| Max timeout | 5 minutes |
| Cold start | ~50–150ms |
| Request body size | 6 MB |
| Response body | unbounded (streaming) |
Long-running work doesn't belong in an edge function — once you cross 30 seconds you're fighting the platform. For batch jobs or anything that takes minutes, use scheduled jobs on a cron and store results in Postgres.
Cold starts only happen on the first request after idle (or when a new version deploys). Warm functions execute in single-digit milliseconds.
Logging
Anything you console.log lands in your project's logs. View them in Cloud → Functions → Logs, filtered by function name and time range.
Deno.serve(async (req) => {
console.log("incoming:", req.method, req.url);
try {
const body = await req.json();
console.log("payload:", body);
return new Response("ok");
} catch (err) {
console.error("parse failed:", err);
return new Response("bad request", { status: 400 });
}
});Logs are retained for 7 days on Free, 30 days on Pro, and 90 days on Business. console.error shows up in red and triggers email alerts if you've enabled them.
Use cases
The functions you'll write tend to fall into a handful of patterns:
- Webhook receivers — Stripe checkout completion, SendGrid bounce events, GitHub push hooks. Verify the signature, then update Postgres or kick off downstream work.
- AI proxies — call OpenAI, Anthropic, or Replicate from server code so your API key never touches the browser. Stream the response back via SSE or a
ReadableStream. - Scheduled jobs — pair an edge function with a Postgres cron job (
pg_cron) for nightly cleanup, weekly digests, or polling external services. - Custom auth flows — magic links to non-email channels (SMS, Slack), service-account exchanges, SSO callbacks.
- Heavy data work — batch operations that would be slow over a single round-trip from the browser.
When in doubt, ask the agent: "Add a Stripe webhook handler that records subscription events in a billing_events table." It scaffolds the function, sets the secret, and deploys.