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.
Why V8 isolates, not Deno or Node?
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.
// 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:
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.
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
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.
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-seeded —
SUPABASE_URL,SUPABASE_ANON_KEY, andSUPABASE_SERVICE_ROLE_KEY(kept for compatibility), plusORBITNEST_URLandORBITNEST_PROJECT_SLUG.
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
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).
-- 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 API —
POST /api/projects/<slug>/env-variableswith{ 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_variablefor you.
Naming rules
^[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.
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
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
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
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
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 in
edge_function_execution_logswith 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
FAQ
Do you support Deno?
We support the Deno namespace surface —Deno.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
WorkManager, Node node-cron, or a GitHub Action) to poke a function on a schedule.┌─────────┐ HTTPS ┌──────────────────────┐
│ Client │ ──────▶ │ Orbitnest API │
└─────────┘ │ ┌────────────────┐ │
│ │ auth + SSRF │ │
│ │ env injection │ │
│ └──────┬─────────┘ │
│ │ │
│ ┌─────▼─────────┐ │
│ │ V8 isolate │──┼──▶ fetch (allow-listed)
│ │ your handler │ │
│ └─────┬─────────┘ │
│ │ │
│ ┌─────▼─────────┐ │
│ │ logs + redact │ │
│ └─────┬─────────┘ │
└─────────┼────────────┘
▼
Response to client