ichibaseichibase

Edge Functions

Deno/TypeScript functions you deploy per project. Each one is reachable at /functions/<name> on your project domain and runs in an isolated V8 worker that is spawned fresh for every request. From inside a function you can reach your own Postgres, Mongo, Redis, auth, and storage with zero config.

The handler

A function is a single module that registers a handler with Deno.serve. The handler takes a standard Request and returns a Response(or a promise of one) — nothing else is required.

// /functions/hello/index.ts
Deno.serve(async (req: Request): Promise<Response> => {
  const url = new URL(req.url);
  const name = url.searchParams.get('name') ?? 'world';
  return Response.json({ hello: name });
});

The runtime captures the handler you pass to Deno.serveand calls it once per HTTP request — it does not actually bind a port. Every documented Deno.serve shape works: Deno.serve(handler), Deno.serve(options, handler), Deno.serve({ handler }), and Deno.serve({ fetch: handler }). If a module registers more than one, the first one wins. A function that never calls Deno.serve fails to load.

Each request gets its own brand-new worker, which is terminated as soon as your handler returns (or when the timeout fires). There is no shared in-memory state between requests — treat every invocation as cold. Use Postgres, Redis, or storage for anything that must persist.

Connecting to your project

The runtime injects your project's credentials as environment variables, and the per-service SDK packages on JSR read them automatically. Import only what a function needs to keep cold-start small — createPostgrest(), createMongo(), createAuth(), createStorage(), and (on Pro/Business) createRedis() each pull ICHIBASE_PROJECT_URL and ICHIBASE_SERVICE_KEY from Deno.env with no arguments.

// /functions/recent-orders/index.ts
import { createPostgrest } from 'jsr:@ichibase/postgrest@^0.3.1';

const pg = createPostgrest(); // reads ICHIBASE_PROJECT_URL + ICHIBASE_SERVICE_KEY

Deno.serve(async (req) => {
  // Headers the runtime sets for you, when present:
  //   x-ichibase-role      'anon' | 'service_role'  (from the caller's apikey)
  //   x-ichibase-user-id   only if a valid Bearer user JWT was sent
  //   x-ichibase-user-email
  const userId = req.headers.get('x-ichibase-user-id');

  const { data, error } = await pg
    .from('orders')
    .select('id, total, status')
    .order('created_at', { ascending: false })
    .limit(10);

  if (error) return new Response(error.detail ?? 'error', { status: error.status });
  return Response.json({ user_id: userId, orders: data });
});

The SDK uses ICHIBASE_SERVICE_KEYby default — the admin role, which bypasses row-level security. To run a query as the calling end-user so their RLS policies apply, pass their JWT: pg.asUser(userJwt). The variables the runtime exposes:

VariableWhenWhat
ICHIBASE_PROJECT_URLalwayshttps://<slug>.ichibase.net
ICHIBASE_PROJECT_IDalwaysYour project slug
ICHIBASE_ANON_KEYalwaysich_pub_… — anon role, safe for browsers
ICHIBASE_SERVICE_KEYalwaysich_admin_… — admin, server-side only
ICHIBASE_DB_URLalwaysDirect Postgres connection string (raw SQL / transactions)
ICHIBASE_MONGO_URLif Mongo enabledInternal Mongo gateway URL
ICHIBASE_REDIS_URLPro / Business onlyDirect Redis connection string

You can also set your own custom env vars from dashboard → Functions → Env vars and read them with Deno.env.get('MY_VAR'). Platform internals (ICHIBASE_JWT_SECRET, the function admin token, the timeout value) are deliberately scrubbed from the worker's environment and are not readable from your code.

Don't log secrets.The worker runs with a narrow permission set — no subprocess, no FFI, filesystem reads scoped to the function's own directory — but if your code console.logs a service key or connection string to a third-party log aggregator, that's a leak. Keep env values out of your logs.

Deploying

Open dashboard → Edge functions → + New function, give it a lowercase name (^[a-z][a-z0-9_-]{0,30}$), write your code in the editor, and hit Deploy (or Cmd/Ctrl + S). The code is stored in your project database; on save the functions runtime container is told to reload, materializes the new code to disk, and serves it within about a second. There is no separate CLI, build step, or bundler — what you type is what runs.

Two toggles per function:

Enabled— a disabled function is not loaded and returns 404.

Require user JWT — when on, a request without a valid Authorization: Bearer <user_jwt> is rejected with 401 before your handler runs. Turn it off for webhooks or public endpoints. Either way the apikeygate still applies — every call must carry a valid project anon or service key.

Calling a function

From a client, use ichi.functions.invoke() — it sets the apikey, attaches the signed-in user's token automatically, JSON-encodes the body, and returns a { data, error } result. The path after the name (and query string) passes straight through to your handler, so route on new URL(req.url).pathname for sub-paths.

// POST + JSON body by default; the user's token is attached if logged in
const { data, error } = await ichi.functions.invoke('recent-orders', {
  body: { note: 'hello' },
});

// GET, or a sub-path / custom method
await ichi.functions.invoke('recent-orders', { method: 'GET' });
await ichi.functions.invoke('files', { path: '/items/42', method: 'GET' });

From inside one function you can call another by fetching ${ICHIBASE_PROJECT_URL}/functions/<other>— the call still passes through the dispatch and JWT checks.

Limits

Functions are bounded per plan: a maximum number of functions, a per-function code-size cap (in KB), a memory ceiling (in MB), and a wall-clock timeout (in ms) per invocation. The exact numbers for your project are shown at the top of the Edge functions page and on the editor. The editor enforces the code-size cap before you can deploy; a function that exceeds its timeout is terminated mid-execution and the caller gets a 504.

Because each request is a fresh worker, a new Postgres connection pool is created per invocation. If you open a raw npm:postgres connection (for transactions or raw SQL via ICHIBASE_DB_URL), use { max: 1 } and close it before your handler returns. For most work the @ichibase/* SDKs talk over HTTP and need no connection management.

Reserved functions: policies & triggers

Two function names are reserved. They run on the exact same engine as your regular functions — same Deno.servehandler shape, same SDK access — but the platform invokes them automatically on writes instead of on an HTTP route, and the request body is a structured event payload rather than the client's request.

_table_policy is the Postgres table-trigger function: per-table BEFORE/AFTER hooks that can allow, deny, or merge audit columns into writes that go through PostgREST. See Table triggers.

_mongo_after is the per-collection AFTER hook for Mongo writes, and _mongo_policy is the BEFORE validate/transform/deny middleware. See Mongo policies.

You author all three in the same Functions-style editor; they just deploy and run like any other function.

Writes the project owner makes outside the SDK — psql, the dashboard SQL editor, or a direct npm:postgresconnection — bypass table triggers, the same way service_role bypasses RLS. Policies and triggers only gate traffic that flows through PostgREST and the Mongo gateway.