Routes
Attach an ordered routing table to any deployment's domain, evaluated at the edge:
static assets at /, a same‑origin backend reverse‑proxied at configured paths
(/api/*), plus redirects, response headers, SPA fallback, and clean‑URL /
trailing‑slash control. It's deployment‑shaped, not Pages‑only:
- Static site — a Page serves
/(setstatic: { page }). - Server‑only — no static; a catch‑all proxy sends everything to a VM.
- Monorepo — one domain: static at
/+/api/*proxied to a VM. - Compose — one domain fronting several VM origins by path.
Maps 1:1 to the routing config in vercel.json. Changes are versioned and go
live in under a second with no redeploy.
Rules are evaluated before static files ("beforeFiles"): a matching rule wins even if a
file exists at that path (so /api/* proxies regardless). Don't add a catch‑all
/(.*) → /index.html rewrite — it shadows real assets. For SPA fallback set spa: true.
Set routes
Atomically replaces a hostname's full ordered rule table. Each call creates a new version.
// Monorepo: static frontend + API proxy on one domain
await client.routes.set('app.example.com', {
static: { page: 'my-frontend' }, // serves / from this Page (omit for server-only)
spa: true,
routes: [
{ match: { path: '/old', type: 'exact' },
action: { kind: 'redirect', status: 308, to: '/new' } },
{ match: { path: '/api/', type: 'prefix' },
action: { kind: 'proxy', workspace: 'ws_backend', port: 3000, stripPrefix: true } },
{ match: { path: '/', type: 'prefix' },
action: { kind: 'headers', set: [{ key: 'X-Frame-Options', value: 'DENY' }] } },
],
});PUT /domain/routes/{hostname}curl -X PUT "https://api.oblien.com/domain/routes/app.example.com" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"routes": [
{ "match": { "path": "/api/", "type": "prefix" },
"action": { "kind": "proxy", "origin": "https://backend", "stripPrefix": true } }
]
}'The hostname must be a domain you own (any routed deployment — Page, workspace/VM, edge‑proxy).
Pages also expose a convenience wrapper PUT /pages/{slug}/routes that defaults static to
the page and applies to all its hostnames.
Body
| Field | Type | Description |
|---|---|---|
routes | Rule[] | Ordered rule table (full replace, no merge). |
static | { page } | Static artifact backing / and rewrites. Omit for server‑only. |
spa | boolean | Serve index.html on a static miss. Default true. |
cleanUrls | boolean | Strip .html, serve extensionless. |
trailingSlash | "enforce" | "strip" | Trailing‑slash policy. |
A rule is { match, action }. match = { path, type: "exact"|"prefix"|"wildcard" }
(type inferred if omitted; wildcard supports *, (.*), :name, :name* with $1..$9).
| action | shape | notes |
|---|---|---|
proxy | { origin | workspace+port, stripPrefix?, path? } | Same‑origin reverse proxy (see below). |
rewrite | { to } | Serve a different asset, URL unchanged. $1..$9 supported. |
redirect | { status?, to } | 301/302/307/308 (default 308). $1..$9 supported. |
headers | { set: [{ key, value }] } | Add/override response headers (non‑terminal). |
Evaluation order
- Headers — every matching
headersrule applies (non‑terminal). - Terminal rules —
proxy/rewrite/redirectin array order, first match wins. - Filesystem — only if no rule matched: serve the file (when
staticis set); on miss →index.htmlifspa, else 404.cleanUrls/trailingSlashcanonicalize here.
Proxy origin
Provide either a verified external origin URL or an owned Oblien workspace + port.
An external origin is SSRF‑validated, ownership‑verified, and pinned to its IP; a workspace
is resolved to its internal IP:port (use a permanent / static‑network workspace so the pin
stays valid). Arbitrary/unverified origins are rejected.
stripPrefix: true— a/api/prefix match forwards/api/usersas/users.path— a capture‑templated upstream path (/api/:path* → path "/$1"), the general form of keep/strip. Only the path is templated; the origin host stays pinned.- Forwarding:
Host= original request host;X‑Forwarded‑For/Proto+X‑Real‑IPadded; WebSocket upgrade forwarded. - Captures (
$1..$9) substitute intorewrite.to,redirect.to, and proxypath.
Read / roll back
GET /domain/routes/{hostname} → { active_version, config, versions }
POST /domain/routes/{hostname}/rollback → { version } re-applies a prior versionconst { active_version } = await client.routes.get('app.example.com');
await client.routes.rollback('app.example.com', active_version - 1);Limits & validation
Rules are untrusted repo input — validated on write, re‑checked at the edge:
- ≤ 64 rules, ≤ 32 header rules (≤ 32 entries each), pattern ≤ 512, targets ≤ 2048,
header value ≤ 1024, total ≤ 32 KB. Over any limit → whole request rejected (
422). - Patterns compiled server‑side (no raw regex → no ReDoS).
.., CRLF, protocol‑relative / non‑http(s) redirects, and hop‑by‑hop /Hostheaders rejected.
Deployments with no rule table keep serving via their simple built‑in route — you opt into rules only when needed.
Not yet supported
Conditional matching (has/missing), method‑based matching, and CDN‑bundle static sources
are planned.