Edge Functions skill for Codex

Run at the edge only what belongs there. Codex decides route by route whether code belongs at the edge, based on where its data lives, then writes a portable Web-standard handler with thin per-provider adapters. It avoids Node-only dependencies, verifies JWTs locally with crypto.subtle, keeps A/B variants cacheable, reaches databases through HTTP drivers or poolers, and compares TTFB from several regions before and after the move.

  • Cloud Platforms
  • $edge-functions
  • 158 lines
  • 3 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- edge-functions

Installs to ~/.agents/skills/edge-functions/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $edge-functions, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Decides what belongs at the edge and writes portable edge code for Cloudflare Workers, Vercel Edge and Routing Middleware, Netlify Edge Functions, Deno Deploy, Supabase Edge Functions, CloudFront Functions and Lambda@Edge - Web-standard Request and Response, CPU and bundle limits, missing Node APIs, database access over HTTP or pooled connections, geolocation, A/B bucketing, auth gates and waitUntil. Use when moving logic to the edge, an edge function fails on a Node API, latency got worse after going edge, or picking a runtime per route.

Use it when

  • Deciding which routes should run at the edge versus in a region
  • An edge function fails on a missing Node API or bundle size limit
  • Latency got worse after moving database-backed code to the edge
  • Writing geolocation, A/B, redirect or auth logic in front of a site

Use something else when

  • You are building a full Cloudflare Workers app with bindings: use cloudflare-workers
  • The work is CDN cache headers and purging rather than code: use cdn-caching

How to check the result

Tests run in the target runtime rather than Node, bundle size is within the plan limit, and TTFB from three regions improves or stays flat after the move.

The mistake it prevents

Moving a page that makes several sequential database queries to the edge, multiplying cross-region round trips and making it slower.

~/.agents/skills/edge-functions/SKILL.md

--- name: edge-functions description: Decides what belongs at the edge and writes portable edge code for Cloudflare Workers, Vercel Edge and Routing Middleware, Netlify Edge Functions,… license: MIT ---

Edge code earns its place when it answers from data that is already at the edge (cache, KV, the request itself) or decides quickly where a request should go. It loses when it makes several round trips to a database in one region. A good edge function is small, uses only Web-standard APIs, finishes in a few milliseconds of CPU, and has a tested fallback when a binding or upstream is unavailable.

Inspect first

  1. Read AGENTS.md, the framework and hosting config (wrangler.jsonc, vercel.json, netlify.toml, next.config, supabase/functions/), and which routes already run at the edge (export const runtime = "edge", middleware or proxy files, edge function directories).
  2. List the data each candidate route touches and where that data lives (region of the database, KV, cache, third-party API).
  3. Check the dependencies the route imports. Anything using fs, net, child_process, native addons or large SDKs will not run or will blow the bundle limit.

Should this run at the edge?

Route doesEdge?
Redirects, rewrites, header changes, bot or geo blockingYes
Auth gate that verifies a JWT signature locally (no DB call)Yes
A/B bucketing, feature flags from an edge storeYes
Personalizing cached HTML with small fragments (HTMLRewriter, streaming)Yes
Read from an edge KV or cache, fall back to originYes
One query to a regional databaseMaybe: only with an HTTP driver or pooler, and placement near the DB
Several sequential database queries, ORM-heavy pagesNo: run regionally next to the database
CPU-heavy work (image processing, PDF, large JSON transforms)No
Long-running jobs, websockets with server stateNo, or use Durable Objects / a dedicated service

If the answer is “maybe”, measure: time to first byte from three regions, edge versus regional, with the real database.

Runtime constraints that bite

  • No filesystem, no child_process, no raw TCP in most runtimes. Cloudflare offers connect() for TCP and a nodejs_compat flag for many Node built-ins; others vary.
  • CPU time per request is capped (milliseconds on free tiers). Await-time on fetch usually does not count against CPU, but wall-clock limits still apply.
  • Bundle size caps are a few MB compressed. Tree-shake and avoid SDKs that pull in Node polyfills; use the provider REST API with fetch instead.
  • Global variables persist only for the lifetime of an isolate. They are a cache, never a store, and different requests may share or not share them.
  • eval and new Function are blocked in several runtimes; some template engines and schema validators that compile code at runtime will fail.

For per-provider limits, placement controls and entry-point signatures, read references/provider-runtimes.md when targeting a specific platform or comparing two.

Portable handler shape

Write the core as a pure (Request) => Promise<Response> and adapt per provider:

// src/edge/handler.ts
import { jwtVerify, createRemoteJWKSet } from "jose";

const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));
const CHECKOUT_BLOCKED = new Set<string>(["XX"]); // ISO codes from legal or compliance, not guesses

export async function handle(req: Request, country: string | undefined): Promise<Response | null> {
  const url = new URL(req.url);

  if (country && CHECKOUT_BLOCKED.has(country) && url.pathname.startsWith("/checkout")) {
    return new Response("Not available in your region", { status: 451 });
  }

  if (url.pathname.startsWith("/app")) {
    const token = req.headers.get("cookie")?.match(/(?:^|;\s*)session=([^;]+)/)?.[1];
    if (!token) return Response.redirect(new URL("/login", url), 302);
    try {
      await jwtVerify(token, JWKS, { issuer: "https://auth.example.com", audience: "web" });
    } catch {
      return Response.redirect(new URL("/login", url), 302);
    }
  }
  return null; // continue to origin
}
// Cloudflare Worker adapter
export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext) {
    return (await handle(req, req.cf?.country as string | undefined)) ?? fetch(req);
  },
};

jose works in every Web-standard runtime because it uses crypto.subtle. Prefer libraries that state edge or Workers support.

Sticky A/B bucketing

function bucket(req: Request): { variant: "a" | "b"; setCookie?: string } {
  const existing = req.headers.get("cookie")?.match(/(?:^|;\s*)ab_home=(a|b)/)?.[1] as "a" | "b" | undefined;
  if (existing) return { variant: existing };
  const variant = crypto.getRandomValues(new Uint8Array(1))[0] < 128 ? "a" : "b";
  return { variant, setCookie: `ab_home=${variant}; Path=/; Max-Age=2592000; SameSite=Lax; Secure` };
}

Rewrite to /home-a or /home-b so each variant stays cacheable on its own URL; never vary the cache on the cookie itself.

Data access from the edge

OptionUse when
Edge KV / Edge Config / cache APIRead-mostly config, flags, rendered fragments
HTTP database drivers (Neon serverless, PlanetScale, Turso/libSQL, Supabase REST)One or two queries per request
Connection pooler close to the DB (Cloudflare Hyperdrive, PgBouncer, Prisma Accelerate)Existing Postgres with TCP drivers
Run the function near the DB (Cloudflare Smart Placement, Vercel function region)Many queries; you keep edge routing but compute moves to the data

Never open a fresh TCP connection to a regional Postgres from hundreds of edge locations per request; connection storms take the database down.

Background work

Use the runtime’s waitUntil for logging, analytics and cache writes after the response: ctx.waitUntil(...) on Workers, context.waitUntil on Netlify, waitUntil from @vercel/functions on Vercel. Unawaited promises without it may be cancelled.

Verification

  • Local runtime test, not Node: wrangler dev or @cloudflare/vitest-pool-workers, netlify dev, vercel dev, supabase functions serve, deno test.
  • Bundle size checked against the plan limit (wrangler deploy --dry-run --outdir dist then inspect, or the build output size report).
  • TTFB compared from at least three regions before and after the move (curl -w "%{time_starttransfer}\n" -o /dev/null -s <url> from each, or a synthetic monitor).
  • Failure path tested: upstream down, missing cookie, invalid JWT, unknown country.
  • Production deploys only after explicit user approval.

Gotchas

  • Middleware that runs on every request, including static assets, adds latency and cost. Restrict it with a matcher to the paths that need it.
  • Response.redirect requires an absolute URL in several runtimes; build it with new URL(path, req.url).
  • Date.now() and timers may be coarse or frozen during a request in some isolates (a Spectre mitigation); do not use them for fine-grained benchmarking inside the function.
  • Environment variables are read differently per runtime (env binding in Workers, Netlify.env.get, Deno.env.get, process.env on Vercel). Centralize access behind one helper.
  • Geolocation headers are best-effort; never use them as an authorization control.

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Edge Functions"
  short_description: "Run at the edge only what belongs there"
  default_prompt: "$edge-functions Should our Next.js API routes run on the edge runtime? Check each one"
references/provider-runtimes.md56 lines, 4.2 KB
# Edge runtimes by provider

Limits change with plans and releases. Treat the magnitudes below as a guide for design
decisions and confirm exact numbers in the provider's current limits page before relying
on them. If network is unavailable in the sandbox, say which numbers were not re-checked.

## Comparison

| Runtime | Engine | Entry point | CPU budget (magnitude) | Node APIs | Placement control |
|---|---|---|---|---|---|
| Cloudflare Workers | V8 isolates (workerd) | `export default { fetch(req, env, ctx) }` | ~10 ms free; paid defaults to tens of seconds, configurable via `limits.cpu_ms` | Many via `nodejs_compat` flag | Smart Placement (`"placement": { "mode": "smart" }`) moves execution near back ends |
| Vercel Routing Middleware / Edge runtime | V8 isolates | `middleware.ts` / `proxy.ts` (Next.js 16 renames middleware to proxy), or `export const runtime = "edge"` | Must start responding within seconds; can stream longer | Subset; Vercel now recommends the Node.js runtime for most functions | Function region (`preferredRegion`, `regions`) for Node functions |
| Netlify Edge Functions | Deno | `export default async (req, context) => ...` in `netlify/edge-functions`, `export const config = { path: "/x" }` | ~50 ms CPU | Deno plus `node:` specifiers for many modules | None; runs at the nearest edge |
| Deno Deploy | Deno | `Deno.serve(handler)` | Plan dependent | `node:` built-ins and npm specifiers | Region selection per project |
| Supabase Edge Functions | Deno | `Deno.serve(async (req) => ...)` in `supabase/functions/<name>/index.ts` | Seconds of CPU, longer wall clock | `npm:` and `node:` specifiers | Can be pinned to the project region via header or invoke option |
| CloudFront Functions | Restricted JS runtime | `function handler(event) { ... }` | Sub-millisecond; no network calls, tiny code size | None | Runs at every edge location |
| Lambda@Edge | Node.js / Python Lambda | Lambda handler on viewer or origin events | Seconds (viewer events shorter than origin events) | Full Node | Regional edge caches; deployed from us-east-1, no environment variables |
| Fastly Compute | WebAssembly (JS, Rust, Go) | Fetch event handler compiled to Wasm | Plan dependent | None (Wasm) | Runs at Fastly POPs |

## Picking per route

- Pure header and URL manipulation on a CloudFront distribution: CloudFront Functions,
  not Lambda@Edge. Use Lambda@Edge only for network calls or body access.
- Next.js on Vercel: keep routing middleware thin (matcher limited, no DB calls); put data
  work in Node.js functions in the database region.
- Netlify sites: Edge Functions for geo, auth gates and rewrites in front of static or
  cached pages; regular Functions for anything that queries a database repeatedly.
- Supabase projects: Edge Functions are fine for webhooks and light APIs; for heavy work
  call a regional service or a database function.
- Cloudflare: Workers can be both edge and "near the data" with Smart Placement, and
  Durable Objects give strongly consistent state at a chosen location.

## Local testing commands

```bash
npx wrangler dev                       # Cloudflare Workers in workerd locally
netlify dev                            # Netlify Edge Functions via a local Deno runtime
vercel dev                             # Vercel, approximate for edge routes
supabase functions serve <name>        # Supabase Edge Functions locally (needs Docker)
deno test --allow-net                  # Deno Deploy code and shared Web-standard handlers
```

Unit-test the shared `(Request) => Response` core with the runtime's own test runner
where possible (`@cloudflare/vitest-pool-workers`, `deno test`), because Node test
runners hide missing-API failures.

## Environment variables and secrets

| Runtime | Read | Set |
|---|---|---|
| Cloudflare Workers | `env.MY_SECRET` (binding) | `wrangler secret put MY_SECRET` |
| Vercel | `process.env.MY_SECRET` | `vercel env add` |
| Netlify Edge | `Netlify.env.get("MY_SECRET")` | `netlify env:set` |
| Deno Deploy / Supabase | `Deno.env.get("MY_SECRET")` | Dashboard, or `supabase secrets set` |
| CloudFront Functions | Not supported; use KeyValueStore for config | `aws cloudfront-keyvaluestore` APIs |
| Lambda@Edge | Not supported; bake config in or fetch from SSM at cold start | Deploy-time |

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc