scribasedocs

Concepts

Functions

Scribase edge functions are server-side TypeScript or JavaScript modules that run on the open-source Deno 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:

TypeScript
// 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:

TypeScript
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:

TypeScript
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:

Terminal
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:

Terminal
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.

Terminal
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:

TypeScript
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:

Terminal
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

TypeScript
const { functions, notes } = await scribase.functions.list(ref);
const { result } = await scribase.functions.invoke(ref, 'hello', { name: 'Alice' });
Terminal
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:

Terminal
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:

TypeScript
// .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
Terminal
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'):

Terminal
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:

TypeScript
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 });
});