Swarmz

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:

  1. Ask the agent. "Deploy the hello function." It'll run the deploy and report back with the URL.

  2. From the editor terminal. Open the Terminal tab inside the editor and run:

    supabase functions deploy hello

    Drop 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 URL
  • SUPABASE_ANON_KEY — anon key for client-side calls
  • SUPABASE_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 = true

When 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

LimitValue
Memory256 MB
Default timeout30 seconds
Max timeout5 minutes
Cold start~50–150ms
Request body size6 MB
Response bodyunbounded (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.

On this page