# Edge Functions

> Fetch handlers for Supabase Edge Functions, Deno, Bun and Workers.

Source: https://bettersupabase.com/docs/frameworks/edge

```ts title="supabase/functions/api/index.ts"
import { createEdge } from "better-supabase/edge";
import { betterSupabase } from "../_shared/supabase.ts";

const bs = createEdge(betterSupabase, { cors: true });

Deno.serve(
  bs.handler(async (request, { db }) => {
    const { name } = await request.json();
    return db.customers.create({ name, organizationId: "..." });
  }),
);
```

`bs.handler(fn)` returns a plain `(request) => Promise<Response>`. The handler
runs as the caller, so RLS applies, and its return value is converted the
same way as in the other adapters:

* A `Result` becomes JSON on success, or Problem Details on failure.
* `undefined` becomes `204`.
* A `Response` is sent as is.
* A thrown `DbException` becomes its Problem Details, and any other error a 500.

Anyone not in `allow` (default `['user']`) gets a 401 or 403 before your code
runs.

For a function the app calls with a typed input and output, use
[`defineFunction`](/docs/platform/edge-functions): it checks the body with a
Standard Schema and exports a contract for `functions()` in
`better-supabase/client`.

With [`.claims(schema)`](/docs/auth#typed-claims) on the definition, `auth`
in the handler is typed by the schema's output:

```ts
Deno.serve(
  bs.handler((_request, { auth }) => ({
    role: auth.kind === "user" ? auth.claims.user_role : null,
  })),
);
```

## REST resources [#rest-resources]

`bs.resources` serves the same REST routes as [Hono resources](/docs/frameworks/hono#resources)
without a router:

```ts
Deno.serve(
  bs.resources(
    {
      customers: { list: customerList },
      tags: { operations: ["list", "get"] },
    },
    { basePath: "/api" },
  ),
);
```

On Supabase, the function name is the first path segment, so `basePath` is
`/<function-name>`. The longest matching table path wins; an unknown path
answers 404.

Each entry takes the same options as a Hono resource, including `hooks`,
`actions` (typed with `defineAction` from `better-supabase/edge`) and
`permissions`. Pass `authorizer` to `createEdge` to decide them:

```ts
import { createEdge, defineAction } from "better-supabase/edge";

const bs = createEdge(betterSupabase, { cors: true, authorizer });

Deno.serve(
  bs.resources(
    {
      customers: {
        permissions: { delete: "customers.delete" },
        actions: {
          archive: defineAction({
            method: "POST",
            path: "/{id}/archive",
            handler: (ctx, { id }) =>
              ctx.db.customers.update(String(id), { status: "archived" }),
          }),
        },
      },
    },
    { basePath: "/api" },
  ),
);
```

See [Resources](/docs/specs/resources) for hooks, actions and permissions.

## Routes [#routes]

`bs.routes` serves several handlers from one function. Keys are
`"METHOD /path"` or `"/path"` (any method), with `:name` segments and a
trailing `/*`. `params` in each handler is typed from its key. A route can be
a handler or an object with the same guards as Hono's `bs.require`:

```ts
Deno.serve(
  bs.routes(
    {
      "GET /customers/:id": (_request, { db, params }) =>
        db.customers.findById(params.id),
      "POST /customers/:id/archive": {
        roles: ["admin"],
        roleClaim: "user_role",
        requireTenant: true,
        handler: (_request, { db, params }) =>
          db.customers.update(params.id, { status: "archived" }),
      },
    },
    { basePath: "/api" },
  ),
);
```

An unknown path returns 404, and a known path with another method returns 405
with an `Allow` header. A route object can also name a `permission`, which
the `authorizer` passed to `createEdge` decides with `{ type: "route" }` as
the resource; without an authorizer, the route refuses every caller.

## Background work [#background-work]

Event sink sends that a handler started (see
[event sinks](/docs/standards/events#sends-after-the-response)) are handed to
`waitUntil`, so the runtime keeps the invocation alive until they finish. On
Workers the handler uses the `ctx` that `fetch(request, env, ctx)` receives.
On Supabase, pass the runtime's function:

```ts
const bs = createEdge(betterSupabase, {
  waitUntil: (promise) => EdgeRuntime.waitUntil(promise),
});
```

## CORS [#cors]

`cors: true` answers preflights and adds the headers supabase-js sends
(`authorization`, `x-client-info`, `apikey`, `content-type`) for any origin.
Restrict it with an allow-list:

```ts
createEdge(betterSupabase, {
  cors: { origin: ["https://app.example.com"], maxAge: 600 },
});
```

A request from an origin that isn't listed gets no CORS headers, and error
responses get the same headers as successful ones. The headers come from
`withCors` in `@supabase/middleware/cors`: a preflight is an `OPTIONS`
request with `Access-Control-Request-Method`, and an allow-list adds
`Vary: Origin`. `corsConfig(options)` returns the `withCors` config for your
own pipeline.

## Entry arrays with `toEdge` [#entry-arrays-with-toedge]

`toEdge(entries, handler)` runs an `@supabase/middleware` entry array around
a handler and returns the same fetch handler shape. The host's second
argument seeds the pipeline, so on Workers `getEnv` inside the entries reads
the bindings:

```ts title="src/worker.ts"
import { withCors } from "@supabase/middleware/cors";
import { toEdge } from "better-supabase/edge";
import { createServer, withBetterSupabase } from "better-supabase/server";

const bs = createServer(betterSupabase);

export default {
  fetch: toEdge(
    [withCors({ origin: "https://app.example.com" }), withBetterSupabase(bs)],
    async (_request, ctx) =>
      Response.json(await ctx.db.notes.findMany().orThrow()),
  ),
};
```

The handler gets the contributions and the Workers execution context, and
returns a `Response`. Put `withCors` first so a 401 from the guard carries the
CORS headers too.