Provider-selectable redirect runtime for fetch-compatible edge platforms: Cloudflare Workers, Vercel Edge Functions, and Netlify Edge Functions. It enforces HTTPS, serves a favicon, and loads non-sensitive instance settings plus redirect rules through the selected Data Source. Choose the adapter that fits the deployment; the three providers do not need to run together.
Live previews:
- Cloudflare domains: https://i0c.cc, https://www.i0c.cc, https://api.i0c.cc
- Vercel deployment: https://vc.i0c.cc
- Netlify deployment: https://nf.i0c.cc
Deploy this package with apps/runtime as the project root.
If the platform detects multiple projects, choose apps/runtime.
Use these settings when the platform asks for project or build configuration:
| Platform | Project root | Build command | Output |
|---|---|---|---|
| Cloudflare Workers | apps/runtime |
pnpm build:cf |
dist/platforms/cloudflare.js |
| Vercel | apps/runtime |
pnpm build:vc |
.vercel/output |
| Netlify | apps/runtime |
pnpm build:nf |
dist |
Build from a full monorepo checkout. On Vercel, keep Include source files outside of the Root Directory in the Build Step enabled so the build can include the shared workspace packages.
The package-level pnpm build command generates and retains all three provider outputs in one pass. The provider-specific commands remain available for targeted builds and deployments.
After deploying:
- Save
config.jsonorredirects.jsonthrough the selected WebUI Repository when non-sensitive settings or rules change. The default Git setup uses thedatabranch. Built-in Sources pick up valid updates after their configured cache TTL without a rebuild. - Set the same
NAMI_SECRETon every Runtime provider and the WebUI. - Re-run the package build after updating shared redirect logic, then redeploy.
- Runtime host: src/entry.ts
- Installed Runtime plugins and platforms: ../../nami.runtime.config.ts
- Build assembly: ../../packages/runtime-build
Need a custom platform or Runtime feature? Add a workspace package with its Manifest and typed factory or ./installation entry, then add it to nami.runtime.config.ts. The Runtime host source and official catalog do not need plugin-specific changes. The external fixture builds a custom platform and Feature and verifies the Feature marker in the emitted artifact. The current contract proves source-workspace integration; the shared plugin packages are not yet published as a public npm SDK. Programmatic consumers can still import handleRedirectRequest from src/lib/handler.ts. Stable plugin manifests and adapter contracts live in ../../packages/plugin-api.
Each build injects only the selected Runtime adapter and uses the same root installation configuration to assemble its Data Source, Analytics Sink, and Features. Remote declarations control optional enablement, configuration, and Secret binding names. Installed packages and the selected Source's initial connection settings remain bootstrap configuration because they are required before config.json can be read. See ../docs/plugins/architecture.md for the package and failure boundaries.
Non-sensitive instance settings are versioned in the selected Repository's config.json. ../../packages/config owns its schema, validation, build-time Source selection, and safe fallback. The Runtime does not read legacy environment variables as overrides or fallbacks; values left in provider dashboards are ignored.
config.json owns:
runtime.canonicalOrigin: Canonical public Runtime origin used by shared consumers such as the WebUI QR code.runtime.robotsPolicy: Set toallowto publish an openrobots.txtand sitemap; set todisallowto block crawling and omit the sitemap.runtime.configCacheTtlSeconds: Cache lifetime forconfig.json.runtime.redirectsCacheTtlSeconds: Cache lifetime forredirects.json.analytics.ingestEndpoint: HTTPS WebUI collector endpoint.analytics.sourceId: Lowercase base hostname and stable statistics namespace shared by all providers.plugins: Namespaced, non-sensitive plugin settings and references to environment-variable secret bindings.
GitHub Raw remains available and keeps the independent config.json and redirects.json cache behavior. Select it with data.source.provider: "github" in ../../packages/config/src/defaults.ts.
The checked-in HTTP Snapshot Source reads both documents atomically from the WebUI:
source: {
provider: "http",
snapshotUrl: "https://u.example.com/api/runtime/snapshot",
requestTimeoutMs: 5_000,
maximumFetchAttempts: 2,
failureBackoffSeconds: 30,
}It deduplicates concurrent loads, uses ETags, bounds timeouts and transient retries, and retains the last host-valid in-memory or platform-cached snapshot when a refresh fails. Invalid snapshot envelopes, data documents, or required-plugin declarations never replace the active version. Use this Source with the PostgreSQL Repository so Runtime deployments receive database-backed saves without database credentials. Source selection is build-time bootstrap configuration and requires rebuilding the Runtime.
When GitHub Raw is selected, programmatic consumers can override its URLs or inject a complete data source through HandlerOptions.
Analytics delivery is disabled unless the versioned endpoint and source ID are valid and this secret is set:
NAMI_SECRET: Shared instance secret used to sign analytics delivery. Use the same value on the WebUI and every Runtime provider.
Copy .env.example for the local placeholder. No other built-in Runtime setting is read from the environment.
Matched redirect and proxy events are sent at full rate. Unmatched and system outcomes are sampled at 10% so arbitrary bot and probe traffic can be analyzed without sending every 404. Cloudflare, Vercel, and Netlify use their platform background-execution mechanism; collector failures are logged and never change the redirect response. Delivery is best effort and currently has no retry queue. Each request is signed with HMAC-SHA256 in X-Analytics-Signature; the signed timestamp is sent in X-Analytics-Timestamp.
The event records the actual entry hostname and adapter provider separately. Entry hostnames must be the configured source hostname or one of its subdomains; other hosts become unknown. Browser referrer hostnames, signed campaign IDs, and verified internal short-link sources remain separate attribution dimensions. Controlled short-link hops use a short-lived signed _nami_via token that is removed before rule processing.
Classification locally derives bounded traffic, bot, confidence, resource, device, match, outcome, and probe categories. This makes robots that request paths outside redirects.json visible in sampled Runtime analysis. Events never send IP addresses, full User-Agent strings, full referrer URLs, query strings, destination URLs, or raw unmatched paths. Matched events contain only the configured rule path and stable analytics ID. Existing rules without an analyticsId receive a deterministic legacy identifier at runtime. Explicit object rules saved through the WebUI persist a UUID for future aggregation; string shortcuts continue using their legacy identifier until converted to object form.
See ../docs/reference/analytics.md for counting semantics, attribution tokens, sampling, privacy limits, schema-update order, and acceptance scenarios.
Custom adapters that enable analytics should also pass provider, optional country, and the platform's waitUntil through HandlerOptions.
Run the plugin contracts, Runtime tests, and independent provider builds from the repository root:
pnpm plugins:check
pnpm runtime:check
pnpm runtime:test
pnpm runtime:build:cf
pnpm runtime:build:vc
pnpm runtime:build:nfYou can also deploy the WebUI panel to edit redirects.json online.
Provide a Slots object in redirects.json to define routing rules. The table below lists the available fields for each route:
| Field | Type | Default | Description |
|---|---|---|---|
analyticsId |
UUID string | generated or derived | Stable analytics identity. Keep it unchanged when editing the path or destination. |
description |
string | omitted | Optional management note of up to 500 characters. Runtime ignores it when matching and responding. |
type |
string | prefix |
Route mode: prefix for prefix redirects, exact for exact matches, proxy for reverse proxying. |
target |
string | "" |
Destination URL. Use exactly one of target, to, or url. |
to / url |
string | "" |
Alias fields. Use exactly one of target, to, or url. |
appendPath |
boolean | true |
Whether to append the remaining path when using prefix or proxy mode. Not applicable to exact. |
proxyOptions |
object | omitted | Optional request headers, response headers, redirect, cookie, timeout, and request-body overrides for proxy rules. |
status |
number | 302 |
HTTP status code from 200 through 599 for non-proxy responses. Do not set for proxy. |
priority |
number | by order | Determines rule precedence for the same path. Smaller numbers are matched first. |
- Keys must start with
/and can use colon parameters such as:idor the*wildcard. Captures can be referenced in the target with$1,:id, and so on. - When multiple path patterns match, literal segments take precedence over colon parameters, parameters take precedence over
*, and deeper patterns win when shared segments have equal specificity. - The
proxytype forwards the request to the destination and returns the upstream response. Other types respond with aLocationredirect. - Proxy requests preserve application headers such as
Cookie,Authorization,Origin, andReferer. The Runtime removes hop-by-hop headers, replacesX-Forwarded-HostandX-Forwarded-Proto, and drops credentials when an upstream redirect changes origin. - Upstream response headers, including security headers and separate
Set-Cookiefields, are preserved. CookieDomainattributes are removed so cookies bind to the public proxy hostname. proxyOptions.requestHeadersandproxyOptions.responseHeadersuse string values to set headers andnullto remove them. Runtime-managed protocol headers cannot be overridden. Configured request headers are not reapplied after a redirect changes origin.- Proxy redirects are followed up to five times by default.
proxyOptions.redirectscan change the limit or pass the upstream redirect through. Optional timeout and request-body limits use seconds and megabytes; provider limits still apply. - To configure multiple rules for the same path, provide an array. Array order controls the default priority, or you can specify
priorityexplicitly.
Add the schema reference below to unlock autocomplete and validation in supporting editors. The schema lives on main, so it still applies if the JSON sits in a data branch:
{
"Slots": {
"/": "https://example.com",
"/docs/:page": [
{
"type": "exact",
"target": "https://kb.example.com/:page",
"status": 302,
"priority": 1
},
{
"type": "prefix",
"target": "https://docs.example.com/:page",
"appendPath": false,
"status": 301,
"priority": 5
}
],
"/promo": {
"target": "https://example.com/campaign",
"status": 308
},
"/api": [
{
"type": "exact",
"target": "https://status.example.com/healthz",
"status": 200,
"priority": 1
},
{
"type": "proxy",
"target": "https://api.example.com",
"appendPath": true,
"priority": 10
},
{
"type": "proxy",
"target": "https://backup-api.example.com",
"appendPath": true,
"priority": 20
}
],
"/media/*": {
"type": "proxy",
"target": "https://cdn.example.com/$1",
"proxyOptions": {
"requestHeaders": {
"Referer": "https://www.example.com/"
},
"redirects": {
"maxHops": 3
},
"timeoutSeconds": 10,
"maxRequestBodyMegabytes": 5
}
},
"/admin": {
"type": "prefix",
"target": "https://console.example.com",
"appendPath": true,
"status": 307
}
}
}English · 简体中文
{ "$schema": "https://raw.githubusercontent.com/Cedarflake/Cedarflake-Nami/main/packages/config/redirects.schema.json", "Slots": { // ... } }