Platform

Edge Functions

TypeScript functions that run inside a hardened V8 isolate — with fetch, console, Deno.env, and full access to your project's database and storage.

Write a .ts file, deploy it, get an authenticated HTTPS endpoint. Your function runs inside an isolated-vm V8 sandbox — not a container, not Deno, not Node — so cold starts stay in the low hundreds of milliseconds and the blast radius of any single function is bounded to memory, time, and the project it belongs to.

The runtime at a glance

Every edge function is compiled from TypeScript, cached by the server (invalidated on every save), and executed inside a fresh V8 isolate. The isolate is not Node.js and not a full Deno runtime — it's a minimal JavaScript host where we polyfill the APIs you actually need for a backend handler.

V8 isolate sandbox

Each invocation runs in a fresh ivm.Isolate. No filesystem, no process, no require — just the globals documented below.

30s timeout · 128 MB default

Wall-clock is enforced via Promise.race; the isolate is disposed immediately on timeout. Memory is a hard cap, not a best-effort limit.

50 concurrent invocations

The isolate pool is capped per node; additional requests receive a 429 Too Many Isolates rather than overwhelming the host.

SSRF guard on fetch()

Outbound fetch blocks loopback, link-local, RFC 1918 private ranges, the cloud-metadata endpoint, and non-HTTP schemes. HTTPS URLs to public hosts go through unchanged.

Why V8 isolates, not Deno or Node?

Isolates start in ~10 ms and allocate a few MB of memory each — an order of magnitude lighter than spinning up a Deno or Node process per request. The trade-off is that you cannot npm installarbitrary modules at runtime. In practice, backend handlers rarely need more than fetch and the Orbitnest client, both of which are built in.

Quickstart

Create a function from the Functions tab in the Studio dashboard, or via the CLI. Every function is a single file that export defaults an async handler.

hello.ts
// This is the minimum viable function.
// Request and Response are standard Web API types.
export default async function handler(req: Request): Promise<Response> {
  const { name } = await req.json().catch(() => ({ name: "world" }));
  return new Response(JSON.stringify({ greeting: `Hello, ${name}!` }), {
    headers: { "content-type": "application/json" },
  });
}

Invoke it from anywhere — the SDKs below, curl, or an HTTP client in your app:

bash
curl -X POST https://api.orbitnest.io/api/projects/<slug>/functions/v1/hello \
  -H "Authorization: Bearer <anon-or-service-role-key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada"}'

Globals available inside your function

The isolate ships with a small, deliberately curated set of Web and Deno-compatible APIs. Nothing else leaks in — no process, no require, no native modules. If it's not in this list, it's not there.

fetch(url, init?)

A standards-compliant fetch that delegates to the host. Works with Headers, Request,Response, JSON bodies, streams, and timeouts. Every outbound request is SSRF-screened before the socket is opened.

typescript
const res = await fetch("https://api.example.com/v1/data", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ ok: true }),
});
const json = await res.json();

Blocked destinations

The SSRF guard rejects file://, ftp://,127.0.0.1, localhost, ::1,169.254.169.254, 10.0.0.0/8,172.16.0.0/12, 192.168.0.0/16, and the internal Docker network. The request fails fast withSSRF blocked.

console.log / error / warn / info

Calls are streamed back to the host in real time and persisted to the function's log table. You'll see them live in the Studio logs viewer and queryable via the REST API.

typescript
console.log("received request", { method: req.method });
console.error("third-party call failed", err.message);

Deno.env.get / Deno.env.toObject

Environment variables are exposed via the Deno.envnamespace — a deliberate convention so code written against the Deno/Supabase edge style keeps working. The values come from two sources, merged at invocation time:

  • Global secrets — defined for the whole project under Functions → Environment Variables. Stored encrypted at rest and decrypted once per invocation into the isolate.
  • Per-function overrides — set from the function detail page. Take precedence over the global values of the same name.
  • Auto-seededSUPABASE_URL,SUPABASE_ANON_KEY, andSUPABASE_SERVICE_ROLE_KEY (kept for compatibility), plus ORBITNEST_URL and ORBITNEST_PROJECT_SLUG.
typescript
export default async function handler(req: Request) {
  // Single lookup — returns string | undefined
  const geminiKey = Deno.env.get("GEMINI_API_KEY");
  if (!geminiKey) {
    return new Response("GEMINI_API_KEY not configured", { status: 500 });
  }

  // Full snapshot when you need many at once
  const env = Deno.env.toObject();

  const ai = await fetch("https://generativelanguage.googleapis.com/v1/models", {
    headers: { "x-goog-api-key": geminiKey },
  });
  return new Response(await ai.text(), { status: ai.status });
}

How secrets are kept secret

Values marked is_secret: true are encrypted with AES-256-GCM at rest using the server'sENVIRONMENT_SECRET. They're decrypted once when your function is invoked, injected as a JSON snapshot into the isolate, and never written to logs — our secret redaction layer strips anything matching bearer tokens, key=value pairs, long base64 strings, and common connection-string patterns from error messages before they're returned to the client.

Headers, Request, Response, URL, URLSearchParams

Standard Web APIs, as you'd expect. Response accepts a string, Blob, ArrayBuffer,ReadableStream, or a serialisable object viaJSON.stringify.

crypto, setTimeout, setInterval

globalThis.crypto.randomUUID(),crypto.getRandomValues(), and the Subtle Crypto API are available for ID generation and signing. Timer functions are present but do not extend the 30 s wall-clock budget — the isolate is still disposed on the hard deadline.

Environment variables & secrets

Define variables once, reference them from any function. The Studio UI distinguishes plain variables (visible in the dashboard) from secrets (masked in the UI, encrypted in the database, redacted from logs).

sql
-- Roughly what's happening under the hood
SELECT name, value, is_secret, description
  FROM env_variables
 WHERE project_id = $1 AND (is_secret = false OR requester_has_service_role);
-- Secret rows arrive masked in the UI as ***ENCRYPTED*** and are only
-- decrypted in-memory when an edge function is invoked.

To set a variable:

  • Dashboard — Functions → Environment Variables →Add variable. Tick Mark as secret for credentials.
  • REST APIPOST /api/projects/<slug>/env-variables with{ name, value, is_secret, description? }.
  • MCP — ask any MCP-aware LLM client"set GEMINI_API_KEY on loopwise as a secret" and it'll call orbitnest_create_env_variable for you.

Naming rules

Variable names are uppercased and normalised server-side. They must match ^[A-Z0-9_]+$ (e.g. DATABASE_URL,API_KEY_1). Lower-case names and names starting with a digit are rejected.

Talking to your database from a function

The cleanest path is to import the OrbitNest SDK and use the auto-seeded service-role key. Because code bundling happens on the server, isolates cannot fetch arbitrary npm modules at runtime — they can only use what the platform bundles for you, and the SDK is one of those.

typescript
import { createClient } from "orbitnest";

const client = createClient(
  Deno.env.get("ORBITNEST_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);

export default async function handler(req: Request) {
  const { data, error } = await client
    .from("subscriptions")
    .select("id, plan, current_period_end")
    .eq("user_id", new URL(req.url).searchParams.get("uid")!);

  if (error) return new Response(error.message, { status: 500 });
  return Response.json({ subscriptions: data });
}

Bypassing RLS from a function

Using the service_role key inside a function bypasses row-level security by design — that's the point of server-side code. Use the anon key and pass the caller's JWT if you want RLS to apply.

Invoking from your app

The SDK wrappers take care of the URL, auth header, and response parsing.

Flutter

dart
final result = await OrbitNest.instance
  .functions
  .invoke('hello', body: {'name': 'Ada'});

if (result.error != null) {
  debugPrint('function failed: ${result.error}');
} else {
  debugPrint('greeting: ${result.data['greeting']}');
}

Node / TypeScript

typescript
import { createClient } from "@orbitneststudio/node";

const client = createClient({
  projectSlug: "loopwise",
  apiKey: process.env.ORBITNEST_ANON_KEY!,
});

const { data, error } = await client.functions.invoke("hello", {
  body: { name: "Ada" },
});

Plain HTTP

bash
curl -X POST https://api.orbitnest.io/api/projects/<slug>/functions/v1/<name> \
  -H "Authorization: Bearer <anon-or-service-role-key>" \
  -H "Content-Type: application/json" \
  -d '{"field":"value"}'

Logs & debugging

Every console.* call is captured, tagged with the invocation ID, and streamed to two places at once:

  • Live tail — the Logs panel on each function's detail page shows calls as they happen with sub-second latency.
  • Persisted history — stored inedge_function_execution_logs with full request body, response status, duration, and any captured error — 30 day retention by default.

Error responses surface only a sanitised message to the caller. The full stack trace and un-redacted context are available to authenticated admins via the logs UI.

Limits & behaviour

Wall-clock timeout

30 s per invocation. Configurable per call via SDK ({ timeout: ms }) up to a hard ceiling of 60 s.

Memory

128 MB per isolate by default. Values above 256 MB are rejected at deploy time.

Concurrency

50 concurrent isolates per node. New requests during saturation receive 429 with a Retry-After header.

Payload size

2 MB request body, 2 MB response body. Streaming responses supported via ReadableStream.

Log retention

30 days for invocation records, 7 days for live-tailconsole.* entries beyond what fits in the detail view.

Blocked at deploy

eval, the Function constructor, andimports from non-https: URLs are rejected during upload, not at runtime.

FAQ

Do you support Deno?

We support the Deno namespace surfaceDeno.env.get / Deno.env.toObject — because that's the convention most backend snippets on the web assume. We don't run the full Deno runtime; the standard library, file-system APIs, and network permission flags are not present. Stick to what's documented under Globals.

Can I npm install a package?

Not at runtime — isolates can't fetch modules. At deploy time we bundle your function, so you can import anything the platform provides (the Orbitnest SDK, a few utility packages) and reference external HTTPS URLs at runtime via fetch.

How cold is a cold start?

First call after a code change: 100–300 ms. Subsequent calls reuse the compiled bundle from an in-memory cache keyed by function ID and updatedAt — so a hot function responds in < 30 ms plus your handler's wall time.

Are idle functions charged?

No. Functions don't run until invoked. There's no "keep warm" cost because there's nothing to keep warm — isolates are created on demand from the cached bundle.

Can I stream a response?

Yes. Return a Response whose body is aReadableStream. The isolate is kept alive until the stream closes or the 30 s budget runs out, whichever comes first.

Coming soon

Scheduled triggers (cron), queue-driven invocations, and per-function rate limits are on the roadmap. Until then, use a scheduler in your app (Flutter WorkManager, Node node-cron, or a GitHub Action) to poke a function on a schedule.
flow
┌─────────┐  HTTPS   ┌──────────────────────┐
│ Client  │ ──────▶  │  Orbitnest API       │
└─────────┘          │  ┌────────────────┐  │
                     │  │ auth + SSRF    │  │
                     │  │ env injection  │  │
                     │  └──────┬─────────┘  │
                     │         │            │
                     │   ┌─────▼─────────┐  │
                     │   │ V8 isolate    │──┼──▶ fetch (allow-listed)
                     │   │ your handler  │  │
                     │   └─────┬─────────┘  │
                     │         │            │
                     │   ┌─────▼─────────┐  │
                     │   │ logs + redact │  │
                     │   └─────┬─────────┘  │
                     └─────────┼────────────┘
                               ▼
                        Response to client