A production-ready toolkit for Contentstack Launch Edge Functions β security, auth, routing, Next.js fixes, and geo-awareness, plus a full local dev/test setup so you can try everything before you deploy.
npm install @aryanbansal-launch/edge-utils
npx launch-edge-quickstartThat's it. This one command scaffolds an edge function, sets up a local KV store to simulate Launch's redirect/rewrite/cache-priming config, seeds it with sample data, and starts a local dev server β printing exactly what's configured and what URL to try for each feature:
Ready β here's what's configured
β Redirects 2 rule(s) configured
curl -i http://localhost:8787/legacy/about β 301 to /about
β Rewrites 2 rule(s) configured
curl --compressed http://localhost:8787/home β serves /, URL unchanged
β Cache priming 2 URL(s), auto-primed on startup
curl -i http://localhost:8787/ β X-Cache: HIT once warmed
Run it again any time β it never overwrites files you already have (your handler, your launch.json), it only fills in what's missing. See Local testing for the full picture, or jump to Table of contents.
- Try it in one command β the fastest path
- Features
- Use this in production β add utilities to your real edge handler
- Local testing β quickstart, step-by-step, and how KV/cache simulation works
- Config-driven redirects (
launch.json) - Writing a handler
- API Reference
- CLI Commands
- Platform Support
- π‘οΈ Security β block AI crawlers, IP allow/deny lists, block default Launch domains
- π Edge Auth β Basic Auth for specific hostnames (e.g. staging)
- π Geo-Aware β read country/region/city from Launch's geo headers
- βοΈ Next.js Ready β fixes for RSC header issues on Launch proxies
- π Routing β code-level conditional redirects, plus config-driven redirects/rewrites via
launch.json - π§ͺ Full local dev setup β run your edge function and simulate Launch's redirect/rewrite/cache-priming config, entirely offline (Wrangler + Miniflare, no Cloudflare account)
- β‘ Lean β no runtime deps in the code you import at the edge; Wrangler is bundled so local testing needs no separate install
Add one or more helpers to your real Contentstack Launch edge handler.
npm install @aryanbansal-launch/edge-utils
npx create-launch-edge # scaffolds functions/[proxy].edge.js if it doesn't existThen import what you need and wire it into your handler β each utility returns a Response on match (return it immediately) or null (fall through to the next check):
import { blockAICrawlers, redirectIfMatch, passThrough } from "@aryanbansal-launch/edge-utils";
export default async function handler(request, context) {
const botCheck = blockAICrawlers(request);
if (botCheck) return botCheck;
const redirect = redirectIfMatch(request, { path: "/old-page", to: "/new-page", status: 301 });
if (redirect) return redirect;
return passThrough(request);
}Deploy through Contentstack Launch as usual β it runs functions/[proxy].edge.js at the edge before traffic reaches your app. See Writing a handler for a fuller example and the API Reference for every utility.
Test redirects, rewrites, cache priming, and your handler's own logic on your machine, without deploying. Wrangler's dev server runs on Miniflare, a Workers-compatible runtime β no Cloudflare account, no network calls.
npx launch-edge-quickstartNon-interactively creates whatever's missing β functions/[proxy].edge.js (a self-contained demo handler, if you don't have one yet), functions/dev-worker.edge.js (the local KV-aware shim), wrangler.toml, and a sample launch.json β never overwrites an existing handler or config, seeds the local KV, prints a status report with copy-pasteable curl commands per feature, then starts the dev server (which auto-primes the cache once ready).
Pass through wrangler dev args as usual: npx launch-edge-quickstart --port 8788. Add --no-prime to skip the automatic startup cache warm-up. Edit launch.json and re-run the same command any time to pick up changes.
Try it:
curl -i http://localhost:8787/legacy/about # redirect: 301 -> /about
curl -i http://localhost:8787/old-shop/tees/blue # wildcard redirect: 301 -> /shop/tees/blue
curl http://localhost:8787/home # rewrite: serves /, URL unchanged
curl -i http://localhost:8787/about # X-Cache: HIT (auto-primed on startup)Already have functions/[proxy].edge.js with your own code (auth, geo, bot-block, etc.)? Quickstart leaves it completely untouched β it only adds the KV plumbing around it. If you'd rather test only your handler, with no KV/redirect/rewrite/cache involved at all, remove the [[kv_namespaces]] block from wrangler.toml β with no KV bound, dev-worker.edge.js becomes a pure passthrough to your handler, no cache headers, no KV reads.
Use this instead of the one-shot command if you want to pick a specific preset via a wizard, or run scaffold/seed/serve as separate steps.
npx launch-edge-local # interactive wizard β pick a preset (see below)
npx launch-edge-seed-local # seed the local KV from launch.json
npx launch-edge-test-local # start the dev servernpx launch-edge-local shows a numbered menu:
| # | Preset | Try |
|---|---|---|
| 1 | Redirect (code-level) | /legacy-demo β 301 |
| 2 | JSON edge route | /api/edge-ping |
| 3 | Basic auth | prompts for demo/demo |
| 4 | Block AI crawlers | curl -A "GPTBot" ... β 403 |
| 5 | Next.js RSC fix | strips RSC header on configured paths |
| 6 | KV redirects | /legacy/about β 301 (from launch.json) |
| 7 | KV rewrites | /docs/intro serves /blog/intro |
| 8 | Cache priming | /__prime warms the cache, then /about is X-Cache: HIT |
Presets 6β8 also write a sample launch.json. If functions/[proxy].edge.js already exists you're asked before it's overwritten β say no to keep your own handler; the KV logic lives in dev-worker.edge.js regardless.
Align BACKEND_URL in wrangler.toml with your app's dev server (default http://127.0.0.1:3000) if your handler passes requests through to a real backend.
launch.json β the same file you deploy β is the single source of truth for redirects, rewrites, and cache priming. Locally, it's seeded into a Miniflare-emulated KV namespace (EDGE_CONFIG) so your setup can read it exactly as Launch would in production:
functions/[proxy].edge.jsβ your real handler. Deploys to Launch unchanged; never touched by the KV tooling.functions/dev-worker.edge.jsβ a local-dev-only shim that readsEDGE_CONFIG, applies redirects/rewrites/cache priming, then delegates everything else to your handler. It's feature-detected (if (env.EDGE_CONFIG)), so with no KV bound it's just a plain rewrite-to-origin dev worker.- Cache priming runs automatically β on startup (once the dev server reports ready) and again in the background on the first request served, mirroring how Launch warms pages after a deploy.
GET /__primeremains available to re-trigger manually.
For the full mental model (two-layer worker design, request lifecycle, KV/cache persistence, diagrams), see ARCHITECTURE.md.
- Cacheability: the Cache API only retains responses that are cacheable per HTTP semantics β a response with no
Cache-Controlheader is silently dropped, so cache priming won't show aHITfor it. SetCache-Controlon routes you want cached. - Fidelity: local KV/cache is functional, not timing-accurate β it won't reproduce real KV propagation delay or edge cache tiering.
- Persist dir:
launch-edge-seed-localandlaunch-edge-test-localboth default to.wrangler-local; if you override one with--persist-to, override the other the same way. - Hostname rules (
protectWithBasicAuth): matching checks both the request URL host and theHostheader, sohostnameIncludes: "localhost"works locally even though the rewritten request points at127.0.0.1. - Geo/IP headers: Miniflare doesn't inject Cloudflare's
cfmetadata, sogetGeoHeaders/getClientIPmay return empty values locally unless you set headers yourself.
See examples/local-dev/ for a minimal runnable project with the KV simulation wired up (npm install, then npm start).
For static, predictable redirects/rewrites/cache priming that don't need request-time logic, skip the edge function entirely β launch.json at your project root is read directly by Launch.
npx launch-configInteractively add redirects (one-by-one or bulk import from CSV/JSON), rewrites, and cache-priming URLs. See npx launch-config bulk import formats below for the file formats. Test it locally the same way as the edge function β see Local testing above.
Config vs. edge function β use launch.json for bulk, static rules (hundreds of SEO redirects, simple path changes); use the edge function for anything that needs request data (cookies, geo, A/B tests, headers). They compose: keep static rules in launch.json, dynamic logic in functions/[proxy].edge.js.
You can also generate launch.json in code with generateLaunchConfig β see the API Reference.
Edge Functions run as middleware before requests reach your origin: User Request β Edge Function β Your Application. Every utility follows the same pattern β call it, return its result if truthy, otherwise keep going:
import {
blockDefaultDomains,
handleNextJS_RSC,
blockAICrawlers,
ipAccessControl,
protectWithBasicAuth,
redirectIfMatch,
getGeoHeaders,
jsonResponse,
passThrough
} from "@aryanbansal-launch/edge-utils";
export default async function handler(request, context) {
// Security
const domainCheck = blockDefaultDomains(request);
if (domainCheck) return domainCheck;
const botCheck = blockAICrawlers(request);
if (botCheck) return botCheck;
const ipCheck = ipAccessControl(request, { allow: ["203.0.113.10", "198.51.100.5"] });
if (ipCheck) return ipCheck;
// Auth
const auth = await protectWithBasicAuth(request, {
hostnameIncludes: "staging.myapp.com",
username: "admin",
password: "securepass123"
});
if (auth && auth.status === 401) return auth;
// Framework fixes
const rscCheck = await handleNextJS_RSC(request, { affectedPaths: ["/shop", "/products", "/about"] });
if (rscCheck) return rscCheck;
// Routing
const redirect = redirectIfMatch(request, { path: "/old-page", to: "/new-page", status: 301 });
if (redirect) return redirect;
// Personalization
const geo = getGeoHeaders(request);
if (geo.country === "US") console.log(`US visitor from ${geo.city}, ${geo.region}`);
// Custom API endpoint
const url = new URL(request.url);
if (url.pathname === "/api/health") {
return jsonResponse({ status: "healthy", region: geo.region, timestamp: Date.now() });
}
// Default: pass to origin
return passThrough(request);
}Blocks common AI/SEO crawlers by user-agent. bots (string[], optional) overrides the default list (claudebot, gptbot, googlebot, bingbot, ahrefsbot, yandexbot, semrushbot, mj12bot, facebookexternalhit, twitterbot, case-insensitive). Returns 403 Response on match, null otherwise.
const response = blockAICrawlers(request); // default list
if (response) return response;
const response = blockAICrawlers(request, ["gptbot", "my-custom-bot"]); // custom listBlocks access via the default Launch domain (*.contentstackapps.com) so search engines only index your custom domain. options.domainToBlock (string, optional) overrides the default. Returns 403 Response on match, null otherwise.
const response = blockDefaultDomains(request);
if (response) return response;IP allow/deny list. deny takes precedence over allow. Returns 403 Response if blocked, null if allowed.
const response = ipAccessControl(request, { allow: ["203.0.113.0/24"], deny: ["203.0.113.50"] });
if (response) return response;Basic Auth gate for a specific hostname. hostnameIncludes matches against the request URL hostname, the Host header, or X-Forwarded-Host (useful locally, since rewriteRequestToOrigin sets that header). Returns 401 Response on failed/missing auth, null if hostnameIncludes doesn't match (chain continues).
const auth = await protectWithBasicAuth(request, {
hostnameIncludes: "staging.myapp.com",
username: "admin",
password: "securepass123"
});
if (auth && auth.status === 401) return auth;Not for production-grade security β Basic Auth credentials are base64-encoded, not encrypted. Learn more
Code-level conditional redirect. Returns a redirect Response if request matches path (and method, if given), else null. status defaults to 301.
const redirect = redirectIfMatch(request, { path: "/old-page", to: "/new-page", status: 301 });
if (redirect) return redirect;Learn more Β· For bulk/static redirects without code, see launch.json.
Fixes a class of Next.js React Server Components bugs where a cache incorrectly serves RSC JSON data instead of a full page load, by stripping the offending header on the paths you list.
const rsc = await handleNextJS_RSC(request, { affectedPaths: ["/shop", "/products", "/about"] });
if (rsc) return rsc;Reads Launch's geo headers. Returns { country, region, city, latitude, longitude } (all string | null).
const geo = getGeoHeaders(request);
if (geo.country === "FR") return Response.redirect("https://fr.mysite.com", 302);Returns the client's IP address from Launch's forwarded headers.
Builds a Response with Content-Type: application/json. init (status/headers) merges in as usual.
return jsonResponse({ status: "ok" });
return jsonResponse({ error: "Not found" }, { status: 404 });Forwards the request to the origin β equivalent to fetch(request). The typical default/fallback at the end of a handler chain.
Builds a LaunchConfig object in code, so you can generate launch.json programmatically (e.g. from CMS data) instead of using the interactive CLI.
interface LaunchRedirect { source: string; destination: string; statusCode?: number; response?: { headers?: Record<string, string> } }
interface LaunchRewrite { source: string; destination: string }import { generateLaunchConfig } from "@aryanbansal-launch/edge-utils";
import fs from "fs";
const config = generateLaunchConfig({
redirects: [{ source: "/old-blog/:slug", destination: "/blog/:slug", statusCode: 301 }],
rewrites: [{ source: "/api/:path*", destination: "https://api.mybackend.com/:path*" }],
cache: { cachePriming: { urls: ["/", "/about", "/products"] } }
});
fs.writeFileSync("launch.json", JSON.stringify(config, null, 2));These read the same redirect/rewrite/cache-priming shape as launch.json, but from a KV namespace β emulated locally by Miniflare (see Local testing), or a real namespace if you deploy this pattern to Cloudflare Workers directly.
redirectFromKV(request, { kv, key?, status? })β reads a redirect table from KV (default key"redirects"), returns a redirectResponseon match (exact path or trailing/*wildcard), elsenull.rewriteFromKV(request, { kv, key? })β reads a rewrite table from KV (default key"rewrites"), returns a newRequestwith the path swapped (client URL unchanged) on match, elsenull.primeCache({ urls, cache, keyBase?, fetchBase?, fetcher? })/primeCacheFromKV({ kv, cache, key?, ... })β warms the Cache API by fetching a list of URLs (or the list stored in KV, default key"cache:priming") and storing cacheable responses.serveWithCache(request, { cache, fetcher?, waitUntil? })β serves a GET from the Cache API when warm, else fetches and populates it; addsX-Cache: HIT | MISS.matchRule(pathname, rules)β the shared exact/wildcard matcher behind the two helpers above.loadRedirects/loadRewrites/loadCachePrimingUrlsβ lower-level readers returning parsed config from KV (empty array on missing/invalid keys).
rewriteRequestToOrigin(request, backendOrigin)β builds a newRequestpointed atbackendOrigin, preserving path/query/body. Used internally byfunctions/dev-worker.edge.jssopassThroughreaches your local app.
| Command | Purpose |
|---|---|
npx create-launch-edge |
Scaffold functions/[proxy].edge.js (production handler) and local-dev files. Creates only what's missing. |
npx launch-edge-quickstart |
One command: scaffold + seed + run the whole local KV/cache setup. See Local testing. |
npx launch-edge-local |
Interactive wizard β pick a preset (code or KV) and scaffold for it. |
npx launch-edge-seed-local |
Seed the local KV namespace from launch.json. |
npx launch-edge-test-local |
Start the local dev server (wrangler dev), with automatic cache priming. |
npx launch-config |
Interactive editor for launch.json β redirects, rewrites, cache priming, with CSV/JSON bulk import. |
npx launch-help |
Print a full reference of methods and CLI commands. |
All local-dev commands run from your project root and accept extra flags passed through to wrangler dev (e.g. --port 8788).
CSV (redirects.csv):
source,destination,statusCode
/old-blog/post-1,/blog/post-1,301
/products/old-sku-123,/products/new-sku-456,308
/legacy/*,/new/*,301JSON (redirects.json):
[
{ "source": "/old-blog/post-1", "destination": "/blog/post-1", "statusCode": 301 },
{ "source": "/products/old-sku-123", "destination": "/products/new-sku-456", "statusCode": 308 }
]Ready-to-use templates: examples/redirects.csv, examples/redirects.json.
Built for Contentstack Launch β assumes standard Web APIs (Request, Response, fetch), an edge runtime, and Launch's geo-location headers.
Not intended for Node.js servers, traditional hosting, or other edge platforms (Cloudflare Workers, Vercel Edge, etc.) β though the KV-backed helpers happen to work anywhere Workers-style KV/Cache APIs are available, since that's what powers the local simulation.
Contributions welcome β please open an issue or pull request.
Repository: https://github.com/AryanBansal-launch/launch-edge-utils
Distributed under the MIT License. See LICENSE for details.
- Contentstack Launch Documentation
- Edge Functions Guide
- NPM Package
- GitHub Repository
- Architecture: Local Edge Dev Setup β deep dive into how local testing, KV simulation, and cache priming work internally
Made with β€οΈ for the Contentstack Launch community