Edge Functions
Sandboxed TypeScript that runs on demand. Write a handler, deploy, then call it from anywhere — your frontend, a cron job, or a webhook source.
Every function is a single TypeScript module that exports a default async handler.
export default async function handler(req: {
method: string;
headers: Record<string, string>;
query: Record<string, string>;
body: string | null;
user: { id: string; email?: string } | null;
env: Record<string, string>;
}) {
console.log("invoked", req.method);
return {
status: 200,
headers: { "x-fn": "zmesh" },
body: { ok: true, you_sent: req.body, you_are: req.user },
};
}The runtime is Deno with --allow-net only. No filesystem, no subprocess, no Deno.env. Use the env field on req instead — it's populated from the per-function env vars you configure in Settings.
Code is saved as a draft on every edit. The public URL only updates when you click Deploy.
- Open Project → Edge Functions and click New function.
- Edit the handler in the Code tab, then hit Save.
- Use the Test tab to invoke the current draft with a sample request.
- Hit Deploy to publish — the version counter bumps and the public URL goes live.
The @zmesh/client SDK ships a typed invoker that auto-attaches the auth token.
import { createClient } from "@zmesh/client";
const zmesh = createClient({
url: "<api-url>",
appId: "<your-app-id>",
projectId: "<your-project-id>", // required only for functions
});
const res = await zmesh.functions.invoke("send-welcome", {
body: { to: "alice@example.com" },
});
if (res.ok) {
console.log(res.status, res.data);
} else {
console.error("function failed", res.status, res.body);
}When the user is signed in, the SDK attaches their bearer token automatically. Auth-gated functions reject anonymous callers with 401.
Edge functions are plain HTTP endpoints. cURL, Postman, server code — anything works.
# Public function — no headers required
curl -X POST <api-url>/v1/fn/<your-project-id>/send-welcome \
-H "Content-Type: application/json" \
-d '{"to":"alice@example.com"}'
# Auth-required function — pass app id + user bearer token
curl -X POST <api-url>/v1/fn/<your-project-id>/private-fn \
-H "X-App-Id: <your-app-id>" \
-H "Authorization: Bearer <user-jwt>" \
-H "Content-Type: application/json" \
-d '{"hi":1}'Toggle this in the function's Settings tab.
req.user is null.X-App-Id + a valid end-user bearer token from this project. req.user contains { id, email }.Per-function key/value pairs.
// In Settings → Environment variables, save:
// { "RESEND_API_KEY": "re_abc123" }
export default async function handler(req) {
const apiKey = req.env.RESEND_API_KEY;
const r = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ from: "me@x.com", to: req.body }),
});
return { status: r.status, body: await r.json() };
}Values are stored at rest and only injected into the sandbox for the duration of the invocation.
Tunable per function in the Settings tab.
- Timeout — 100 ms to 60 s (default 10 s). Exceeding it returns
504. - Memory — 64 MB to 1024 MB (default 256 MB). Caps the V8 old-generation heap.
- Source size — 256 KB max.
- Permissions — outbound HTTP only. No filesystem, env, subprocess, FFI or worker access.
The handler's return value becomes the HTTP response.
return {
status: 201, // optional, defaults to 200
headers: { "x-fn": "zmesh" }, // optional
body: { ok: true }, // object → JSON-stringified,
// string → sent as-is
};Default Content-Type is application/json unless you override it in headers.
Every invocation is recorded.
Open the function's Logs tab to see the last 50 calls — status code, method, duration, captured console.log output and any thrown errors.