# Architecture — Local Edge Dev Setup
This document explains **how `@aryanbansal-launch/edge-utils` runs and tests
Contentstack Launch edge functions on your machine**, and in particular how it
simulates the Launch platform's **KV-backed config** (redirects, rewrites, and
cache priming) using Wrangler + Miniflare — with no Cloudflare account and no
deploy.
It is aimed at contributors and advanced users who want the full mental model,
not just the commands. For task-oriented steps see [readme.md](readme.md); for a
runnable project see [`examples/local-dev/`](examples/local-dev/).
---
## 1. The problem this solves
Contentstack Launch runs your edge function (`functions/[proxy].edge.js`) at the
edge, and applies **platform-level config** — bulk redirects, rewrites, and
cache priming from `launch.json` — *around* that function. In production that
config lives in a platform store and is applied by the platform; locally you
have neither the platform nor the store.
We want to reproduce that behavior on a laptop so you can **see redirects fire,
rewrites swap paths, and the cache warm up** before deploying. The strategy:
- Run the Workers runtime locally via **Wrangler → Miniflare**.
- Simulate the platform config store with a **local KV namespace** (also
emulated by Miniflare).
- Read that config at request time with small, dependency-free **library
helpers**, from a thin **dev-only shim** that wraps your real handler.
```mermaid
flowchart LR
subgraph Production["Production (Contentstack Launch)"]
P1[Platform edge layer
redirects / rewrites / cache] --> P2["[proxy].edge.js
your handler"] --> P3[Origin app]
end
subgraph Local["Local (this toolkit)"]
L1[dev-worker.edge.js
KV-backed shim] --> L2["[proxy].edge.js
your handler, unchanged"] --> L3[Local app
BACKEND_URL]
end
Production -.mirrors.- Local
```
The key idea: **the shim plays the role of the platform layer locally**, so your
production handler stays untouched and production-safe.
---
## 2. Runtime substrate: Wrangler + Miniflare
- **Wrangler** is Cloudflare's Workers CLI. It is bundled as an
`optionalDependency` of this package, so users don't install it separately.
- `wrangler dev` runs your worker on **Miniflare**, a Workers-compatible runtime
that emulates the platform APIs **locally and offline**, including:
- **KV namespaces** (`env.EDGE_CONFIG`)
- the **Cache API** (`caches.default`)
- `fetch`, `Request`/`Response`, `ctx.waitUntil`, etc.
- Miniflare persists KV/Cache state **to disk** so it survives restarts. We pin
that location to `.wrangler-local/` (see §6).
No Cloudflare account, network, or deployed namespace is required for any of
this.
---
## 3. Component map
### Library source (`src/`)
| Module | Role |
|---|---|
| [src/kv/types.ts](src/kv/types.ts) | Minimal structural `EdgeKVNamespace` / `EdgeCache` interfaces — a subset of the real Workers types, so the library needs **no** `@cloudflare/workers-types` dependency. |
| [src/utils/match-rule.ts](src/utils/match-rule.ts) | Shared `source → destination` matcher (exact + trailing `/*` wildcard). Used by both redirects and rewrites. |
| [src/redirect/redirect-kv.ts](src/redirect/redirect-kv.ts) | `redirectFromKV` — reads the redirect table from KV, returns a redirect `Response` on match. |
| [src/rewrite/rewrite-kv.ts](src/rewrite/rewrite-kv.ts) | `rewriteFromKV` — reads the rewrite table from KV, returns a rewritten `Request` (path swapped, client URL unchanged). |
| [src/cache/priming.ts](src/cache/priming.ts) | `primeCache` / `primeCacheFromKV` (warm the Cache API) and `serveWithCache` (serve-or-populate with `X-Cache: HIT\|MISS`). |
| [src/launch/config.ts](src/launch/config.ts) | `LaunchConfig` types + `generateLaunchConfig` — the shape of `launch.json`. |
| [src/dev/rewrite-origin.ts](src/dev/rewrite-origin.ts) | `rewriteRequestToOrigin` — repoints a request at `BACKEND_URL` for local proxying. |
### CLI (`bin/`)
| Command (package bin) | Script | Role |
|---|---|---|
| `create-launch-edge` | [bin/launch-init.js](bin/launch-init.js) | Scaffolds `functions/`, `wrangler.toml`, and the **dev-worker shim**. Hosts the local wizard. |
| `launch-edge-local` | [bin/launch-edge-local.js](bin/launch-edge-local.js) | Shortcut for `create-launch-edge local` — the interactive preset wizard. |
| `launch-config` | [bin/launch-config.js](bin/launch-config.js) | Interactive editor for `launch.json` (redirects / rewrites / cache priming; CSV/JSON bulk import). |
| `launch-edge-quickstart` | [bin/launch-edge-quickstart.js](bin/launch-edge-quickstart.js) | **One command**: non-interactively scaffolds whatever's missing, seeds KV, prints a per-feature status report, then runs `launch-edge-test-local`. See §9. |
| `launch-edge-seed-local` | [bin/launch-edge-seed-local.js](bin/launch-edge-seed-local.js) | Loads `launch.json` into the **local KV** namespace. |
| `launch-edge-test-local` | [bin/launch-edge-test-local.js](bin/launch-edge-test-local.js) | Runs `wrangler dev` (pinned persist dir) and **auto-primes** the cache once ready. |
| `launch-help` | [bin/launch-help.js](bin/launch-help.js) | Prints usage/help. |
All scaffold file templates (the default handler, the KV-aware dev-worker shim, `wrangler.toml`, and the sample `launch.json`) live in one shared module, [bin/lib/scaffold-templates.js](bin/lib/scaffold-templates.js), imported by both `launch-init.js` and `launch-edge-quickstart.js` — so the auto-priming logic in the shim can't drift out of sync between the wizard path and the one-command path.
---
## 4. The two-layer worker model
This is the central design decision. There are **two** files in `functions/`:
1. **`[proxy].edge.js` — your real handler.** This is what deploys to Launch. It
contains your logic (auth, geo, JSON routes, `fetch`/passthrough to origin).
The toolkit **never bakes KV logic into this file** — so it behaves
identically in production, where there is no KV binding.
2. **`dev-worker.edge.js` — the local-only shim.** This is the `main` entry in
`wrangler.toml`. It is the Miniflare worker. It:
- reads the simulated Launch config from KV,
- applies redirects / rewrites / cache priming (the "platform layer"),
- and delegates everything else to your `[proxy].edge.js` as the "origin".
```mermaid
flowchart TD
Req[Incoming request] --> DW[dev-worker.edge.js]
DW -->|env.EDGE_CONFIG present?| FD{feature detect}
FD -->|no KV bound| PT[origin = your handler]
FD -->|KV bound| KV[apply KV config]
KV --> RD[redirectFromKV]
KV --> RW[rewriteFromKV]
KV --> CA[serveWithCache]
RD --> H["[proxy].edge.js"]
RW --> H
CA --> H
PT --> H
H --> ORIGIN[rewriteRequestToOrigin → BACKEND_URL app]
```
### Feature detection
The shim's KV block is guarded by `if (env.EDGE_CONFIG)`. If no KV namespace is
bound (e.g. a non-KV preset, or a stripped-down config), the shim degrades to a
plain rewrite-to-origin dev worker — the pre-KV behavior. This guard is what
keeps the same generated code safe across setups.
---
## 5. Config source of truth: `launch.json` → KV
`launch.json` at the project root is the **single source of truth**, the same
file you deploy. It is produced/edited by `launch-config` (or a wizard preset)
and has this shape (empty sections are stripped):
```json
{
"redirects": [
{ "source": "/legacy/about", "destination": "/about", "statusCode": 301 },
{ "source": "/old-shop/*", "destination": "/shop/*", "statusCode": 301 }
],
"rewrites": [
{ "source": "/home", "destination": "/" },
{ "source": "/docs/*", "destination": "/blog/*" }
],
"cache": { "cachePriming": { "urls": ["/", "/about"] } }
}
```
`launch-edge-seed-local` reads this file and writes **three keys** into the
`EDGE_CONFIG` KV namespace — only for the sections that are present:
| `launch.json` section | KV key | Reader |
|---|---|---|
| `.redirects` | `redirects` | `redirectFromKV` / `loadRedirects` |
| `.rewrites` | `rewrites` | `rewriteFromKV` / `loadRewrites` |
| `.cache.cachePriming` | `cache:priming` | `primeCacheFromKV` / `loadCachePrimingUrls` |
Seeding uses `wrangler kv key put --binding EDGE_CONFIG --local
--persist-to .wrangler-local --path ` (values passed by file to avoid
shell-escaping issues). Re-run it whenever you edit `launch.json`.
```mermaid
flowchart LR
LJ[launch.json] -->|launch-edge-seed-local| KVN[(Local KV
EDGE_CONFIG)]
KVN -->|redirects| RK[redirectFromKV]
KVN -->|rewrites| RWK[rewriteFromKV]
KVN -->|cache:priming| PK[primeCacheFromKV]
```
---
## 6. Local KV & Cache persistence
Both the seed step and the dev server must read/write the **same on-disk store**,
or the worker sees an empty KV. We pin both to `.wrangler-local/`:
- `launch-edge-seed-local` defaults `--persist-to .wrangler-local`.
- `launch-edge-test-local` defaults `--persist-to .wrangler-local` (a
user-supplied `--persist-to` is respected and passed through unchanged).
On disk Miniflare lays it out as:
```
.wrangler-local/
v3/
kv/edge_config_local_placeholder/… # seeded redirects / rewrites / cache:priming
cache/… # Cache API entries (priming + serveWithCache)
```
`.wrangler-local/` and `.wrangler/` are git-ignored. The `id` in the
`[[kv_namespaces]]` binding is a **placeholder** — it only names the local store;
no real namespace exists.
---
## 7. Request lifecycle
For a KV-enabled setup, `dev-worker.edge.js` processes each request in this
order:
1. **`GET /__prime`** → run cache priming from KV and return a JSON report.
(Convenience endpoint for manual warm-up; see §8.)
2. **First-request auto-prime** → on the very first request the worker serves,
kick off priming in the background via `ctx.waitUntil` (once per worker
lifetime).
3. **Redirects** → `redirectFromKV`. On a match, return `Response` (301/308/…)
immediately. Exact and wildcard sources supported.
4. **Rewrites** → `rewriteFromKV`. On a match, produce a new `Request` with the
path swapped (client URL unchanged); otherwise keep the original request.
5. **Serve through cache** → `serveWithCache` checks the Cache API; on a HIT it
returns the cached response (`X-Cache: HIT`), on a MISS it fetches from the
origin, stores a cacheable response, and returns it (`X-Cache: MISS`).
The "origin" for steps 2–5 is a function that runs **your** `[proxy].edge.js`
against the local app via `rewriteRequestToOrigin(req, BACKEND_URL)`.
```mermaid
sequenceDiagram
participant C as curl / browser
participant W as dev-worker.edge.js
participant KV as Local KV
participant Ca as Cache API
participant H as proxy.edge.js (your handler)
participant A as Local app (BACKEND_URL)
C->>W: GET /legacy/about
W->>KV: get "redirects"
KV-->>W: rules
W-->>C: 301 Location /about (redirect wins, stops here)
C->>W: GET /docs/intro
W->>KV: get "rewrites"
KV-->>W: rules → /blog/intro
W->>Ca: match(/docs/intro)
Ca-->>W: miss
W->>H: handler( rewrite→BACKEND_URL /blog/intro )
H->>A: fetch /blog/intro
A-->>H: 200 body
H-->>W: 200 body
W->>Ca: put(/docs/intro, body) (if cacheable)
W-->>C: 200 body X-Cache: MISS
```
### Matching semantics (`matchRule`)
- **Exact**: `source` equals the pathname.
- **Wildcard**: `source` ending in `/*` matches any pathname under that prefix.
- Destination with `*` → the captured tail is substituted
(`/old-shop/*` → `/shop/*` turns `/old-shop/a/b` into `/shop/a/b`).
- Destination without `*` → **static catch-all**, tail dropped
(`/drop/*` → `/gone` turns `/drop/x/y` into `/gone`).
- First matching rule (array order) wins.
---
## 8. Cache priming
Priming proactively fetches the URLs in `cache:priming` and stores cacheable
responses **before** real traffic arrives — so the first visitor gets a HIT
instead of paying the cold-origin cost. It runs three ways:
| Trigger | Where | When |
|---|---|---|
| **Startup (A)** | `launch-edge-test-local` | After Wrangler prints "Ready on …", the CLI fires one `GET /__prime`. Skip with `--no-prime`. |
| **First request (B)** | `dev-worker.edge.js` | Background prime (`ctx.waitUntil`) on the first served request, guarded by a once-flag. |
| **Manual** | `GET /__prime` | Any time, to re-warm. |
`primeCacheFromKV` fetches each URL through the same origin function used to
serve requests, keying the cache by the **public** URL (`keyBase = url.origin`)
so subsequent requests match. `primeCache` verifies retention with a follow-up
`cache.match` — so its `cached` flag reflects whether the Cache API **actually
kept** the entry, not merely that the fetch was 2xx.
> **Cacheability matters.** The Cache API only retains responses that are
> cacheable per HTTP semantics. A response with `Cache-Control: no-cache` (or an
> already-expired `max-age=0`) is dropped by `cache.put` — priming reports
> `cached: false` and requests stay `X-Cache: MISS`. This is correct behavior,
> and mirrors production: set proper `Cache-Control` on the routes you expect to
> cache.
---
## 9. End-to-end user flow
### One command (`launch-edge-quickstart`)
`launch-edge-quickstart` composes the three granular steps below into a single, non-interactive run:
```mermaid
flowchart TD
I["npm install @aryanbansal-launch/edge-utils"] --> Q["npx launch-edge-quickstart"]
Q --> SC["Scaffold: create only what's missing
(dev-worker.edge.js, wrangler.toml, sample launch.json)
never touches an existing handler or config"]
SC --> SE["Seed: launch.json → local KV"]
SE --> ST["Status report: per-feature curl commands"]
ST --> D["Run: wrangler dev + startup auto-prime"]
D --> T["curl / browser: redirects, rewrites, cache"]
T -->|edit launch.json, re-run| Q
```
```bash
npm install @aryanbansal-launch/edge-utils
npx launch-edge-quickstart # scaffold (only what's missing) + seed + status report + run
```
It shells out to the same `launch-edge-seed-local.js` and `launch-edge-test-local.js` scripts described below (via `spawnSync`/`spawn`), so behavior — persist-dir pinning, `--no-prime`, argument passthrough — is identical to running them by hand. The only things it adds are: non-interactive scaffolding with existing-file protection, and a status report that reads `launch.json` and prints exactly what's configured per feature with ready-to-copy `curl` commands.
### Step-by-step (the granular commands)
Use these instead of `launch-edge-quickstart` when you want to pick a specific preset via the wizard, or want scaffold/seed/serve as separate, inspectable steps:
```mermaid
flowchart TD
I["npm install @aryanbansal-launch/edge-utils"] --> W["npx launch-edge-local
(wizard: pick KV preset)"]
W --> F1["functions/dev-worker.edge.js (KV shim)"]
W --> F2["wrangler.toml (KV binding)"]
W --> F3["launch.json (sample config)"]
F3 --> S["npx launch-edge-seed-local
(launch.json → local KV)"]
S --> D["npx launch-edge-test-local
(wrangler dev + auto-prime)"]
D --> T["curl / browser: redirects, rewrites, cache"]
T -->|edit launch.json| S
```
Concretely:
```bash
npm install @aryanbansal-launch/edge-utils
npx launch-edge-local # scaffold: shim + wrangler.toml + sample launch.json
npx launch-edge-seed-local # load launch.json into local KV
npx launch-edge-test-local # wrangler dev (shared persist dir) + startup auto-prime
```
---
## 10. Production vs. local
| Concern | Production (Launch) | Local (this toolkit) |
|---|---|---|
| Config store | Platform-managed | Local KV (`EDGE_CONFIG`), seeded from `launch.json` |
| Where rules apply | Platform layer around your function | `dev-worker.edge.js` shim |
| Your handler | `[proxy].edge.js` | `[proxy].edge.js` (identical, untouched) |
| Cache priming trigger | Platform (post-deploy/purge) | Startup + first-request + manual `/__prime` |
| Runtime | Cloudflare Workers | Miniflare via `wrangler dev` |
**Fidelity is functional, not timing-accurate.** Local KV reads are immediate
and the Cache is in-process, so this does **not** reproduce real KV propagation
delay or edge cache tiering. It verifies config and logic, not consistency
timing.
---
## 11. Caveats & gotchas
- **Persist dir must match.** Seed and dev both default to `.wrangler-local`; if
you override one, override both, or the worker reads an empty KV.
- **Cacheability.** See §8 — no `Cache-Control`, no cache retention.
- **`caches` global.** Available at runtime in Miniflare; the generated
`dev-worker.edge.js` uses `caches.default`. TS tooling may need a declaration.
- **Wizard on an existing handler.** If `functions/[proxy].edge.js` already
exists, the wizard prompts before overwriting — decline to keep your handler
(the KV logic lives in the shim regardless).
- **Compression in tests.** `wrangler dev` may gzip responses; use
`curl --compressed` to read bodies, `curl -i` to inspect redirect headers.
---
## 12. Directory reference
```
launch-edge-utils/
├── bin/
│ ├── launch-init.js # scaffolder + wizard + file templates
│ ├── launch-edge-local.js # → launch-init local
│ ├── launch-config.js # launch.json editor
│ ├── launch-edge-seed-local.js # launch.json → local KV
│ ├── launch-edge-test-local.js # wrangler dev + auto-prime
│ └── launch-help.js
├── src/
│ ├── kv/types.ts # EdgeKVNamespace / EdgeCache interfaces
│ ├── utils/match-rule.ts # exact + wildcard matcher
│ ├── redirect/redirect-kv.ts # redirectFromKV
│ ├── rewrite/rewrite-kv.ts # rewriteFromKV
│ ├── cache/priming.ts # primeCache / primeCacheFromKV / serveWithCache
│ ├── dev/rewrite-origin.ts # rewriteRequestToOrigin
│ └── launch/config.ts # LaunchConfig / generateLaunchConfig
├── examples/local-dev/ # runnable reference project (see its README)
└── (scaffolded in a user project)
├── functions/[proxy].edge.js # your handler (production)
├── functions/dev-worker.edge.js # local KV shim (dev only)
├── wrangler.toml # KV binding + BACKEND_URL
├── launch.json # config source of truth
└── .wrangler-local/ # Miniflare KV + Cache state (git-ignored)
```
---
## Summary
The local setup mirrors Launch's production model by splitting responsibilities:
your **handler stays production-pure**, a **dev-only shim** plays the platform's
config layer, **`launch.json` seeded into a Miniflare KV** is the config source
of truth, and the **Cache API** (with automatic priming) reproduces edge
caching. The result is a faithful, offline way to see redirects, rewrites, and
cache priming behave before you deploy.