# Functions

Scribase edge functions are server-side TypeScript or JavaScript modules that
run on the open-source Deno [edge-runtime](https://github.com/supabase/edge-runtime)
— the same runtime Supabase uses — next to your database. They are served on
the Supabase-compatible `/functions/v1/<name>` path, so existing function code
and the clients that call it move over by changing the URL and keys.

## Writing a function

A function is a directory with an `index.ts` that serves requests:

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

## Environment available to a function

Every function receives the Supabase-compatible variables plus every secret set
on its environment (see Secrets below):

| Variable | Value |
|---|---|
| `SUPABASE_URL` | The environment's public API URL |
| `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY` | The environment's API keys |
| `SUPABASE_DB_URL` | The Postgres connection string (`scribase dev` also sets `DATABASE_URL` and `SCRIBASE_DB_URL`) |
| Your secrets | Each secret set with `scribase secrets set` or the console, by name |

The JWT signing secret and the runtime's own admin credential are never passed
to a deployed function. To verify a caller, forward their `Authorization`
header to auth (`supabase.auth.getUser(token)`) rather than checking the
signature yourself.

Query Postgres directly with any Deno Postgres driver:

```ts
import postgres from 'https://deno.land/x/postgresjs@v3.4.4/mod.js';

const sql = postgres(Deno.env.get('SUPABASE_DB_URL')!);

Deno.serve(async () => {
  const orders = await sql`select id, total from orders order by id desc limit 10`;
  return Response.json({ orders });
});
```

Or use supabase-js against your environment's API:

```ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

const client = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!);
```

## Deploying to a hosted environment

With the CLI, point `functions deploy` at the function's directory. It sends
`index.ts`, every sibling module (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.mts`,
`.json`) and an `import_map.json` if one is present, as one bundle:

```sh
scribase functions deploy acme store production hello ./functions/hello
scribase functions deploy acme store production hello ./functions/hello --import-map ./import_map.json
```

Each deploy is a new immutable version. The runtime boots it once and makes it
active only if the boot succeeds; otherwise the previous version keeps serving
and the command fails with the boot error. Manage versions with:

```sh
scribase functions list     acme store production
scribase functions get      acme store production hello [--version 3]
scribase functions rollback acme store production hello 3
scribase functions delete   acme store production hello
scribase functions invoke   acme store production hello --data '{"name":"Alice"}'
```

From CI without the CLI, send the function's name and source to the
environment's functions route. A deploy creates or replaces the function; a runtime that
rejects it surfaces a real error rather than a silent success.

```sh
jq -n --arg source "$(cat functions/hello/index.ts)" '{name: "hello", source: $source}' \
  | curl -X POST "$SCRIBASE_API_URL/v1/organizations/acme/projects/store/environments/production/functions" \
      -H "Authorization: Bearer $SCRIBASE_ACCESS_TOKEN" \
      -H 'Content-Type: application/json' --data-binary @-
```

With the SDK:

```ts
import { readFile } from 'node:fs/promises';

const ref = { organizationId: 'acme', projectId: 'store', environmentId: 'production' };
await scribase.functions.deploy(ref, {
  name: 'hello',
  source: await readFile('functions/hello/index.ts', 'utf8'),
});
```

Function names are letters, digits, hyphens, or underscores, starting with a
letter or digit.

## Logs

Each function's log holds one line per invocation (method, path, status,
duration) plus the function's own output: `console.log`, `console.info`,
`console.warn` and `console.error`, uncaught exceptions, and CPU, wall-clock or
memory limit terminations. Read it on the function's page in the console or
with:

```sh
scribase functions logs acme store production hello --limit 50
```

Lines are also shipped to the environment's logs page, where they are kept
after the runtime restarts.

## Listing and invoking through the control API

```ts
const { functions, notes } = await scribase.functions.list(ref);
const { result } = await scribase.functions.invoke(ref, 'hello', { name: 'Alice' });
```

```sh
curl -X POST "$SCRIBASE_API_URL/v1/organizations/acme/projects/store/environments/production/functions/hello/invoke" \
  -H "Authorization: Bearer $SCRIBASE_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name": "Alice"}'
```

`list` degrades to an empty array with a `notes` explanation if the runtime is
unreachable, so a dashboard never breaks; `invoke` and `deploy` surface errors.

## Calling a function from your app

Clients call functions on the compatible path, with a user's JWT or the anon key:

```sh
curl -X POST 'https://<your-environment-api-url>/functions/v1/hello' \
  -H 'Authorization: Bearer <anon-key-or-user-jwt>' \
  -H 'Content-Type: application/json' \
  -d '{"name": "Alice"}'
```

With supabase-js: `await supabase.functions.invoke('hello', { body: { name: 'Alice' } })`.

## Local development

`scribase dev` starts the edge-runtime (port `9000` by default) with
`<SCRIBASE_HOME>/functions/main` as its main service — `.scribase/functions/main`
unless you set `SCRIBASE_HOME`. The main service is a small router that maps
`/<name>` to a sibling function directory. Create it once:

```ts
// .scribase/functions/main/index.ts
declare const EdgeRuntime: {
  userWorkers: {
    create(options: Record<string, unknown>): Promise<{ fetch(req: Request): Promise<Response> }>;
  };
};

const FUNCTIONS_ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');

Deno.serve(async (req) => {
  const segments = new URL(req.url).pathname.split('/').filter(Boolean);
  // Accept both /hello and /functions/v1/hello.
  const name = segments[0] === 'functions' && segments[1] === 'v1' ? segments[2] ?? '' : segments[0] ?? '';
  if (!/^[A-Za-z0-9_-]+$/.test(name)) {
    return Response.json({ error: 'invalid function name' }, { status: 400 });
  }
  try {
    const worker = await EdgeRuntime.userWorkers.create({
      servicePath: `${FUNCTIONS_ROOT}/${name}`,
      memoryLimitMb: 150,
      workerTimeoutMs: 30_000,
      envVars: Object.entries(Deno.env.toObject()),
    });
    return await worker.fetch(req);
  } catch {
    return Response.json({ error: `function "${name}" is not deployed` }, { status: 404 });
  }
});
```

Then put each function next to it and start the stack:

```
.scribase/functions/
├── main/
│   └── index.ts      # the router above
└── hello/
    └── index.ts
```

```sh
scribase dev --services functions
curl -X POST http://localhost:9000/hello -H 'Content-Type: application/json' -d '{"name":"Alice"}'
```

The self-hosted Docker Compose stack ships the same router at
`deploy/compose/functions/main/index.ts`.

## Secrets

Values your function needs beyond the injected variables, such as third-party
API keys, are environment secrets. They are encrypted at rest, bound into every
function in the environment, and read with `Deno.env.get('NAME')`:

```sh
scribase secrets set   acme store production STRIPE_KEY --from-env STRIPE_KEY
printf %s "$WEBHOOK_SECRET" | scribase secrets set acme store production WEBHOOK_SECRET --stdin
scribase secrets list  acme store production
scribase secrets unset acme store production STRIPE_KEY
```

Prefer `--from-env` or `--stdin` over a literal value so the secret stays out of
your shell history. Secrets can also be managed on the environment's Secrets
page in the console.

Setting, rotating or removing a secret reaches deployed functions without a
redeploy: the next request after the change boots a fresh worker with the new
values, while requests already in flight finish on the old one.

## CORS

Return CORS headers from functions called by browsers:

```ts
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, content-type',
};

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders });
  }
  return Response.json({ ok: true }, { headers: corsHeaders });
});
```
