Routes
Inventory and dashboard
Dashboard → Routes lists all registered public hostnames for the account, with resource ownership, status, namespace and per-hostname analytics. Open a hostname’s Routing tab to change its action, proxy upload limit, or restore a saved version. Static site deployments have their own Pages view.
GET /domain/routes returns { success: true, data: [...] }. Each route includes
hostname, owner_type, owner_id, namespace, status, slug and route_type.
Use owner_type to identify a Page, workspace, proxy or tunnel; route_type
describes the edge handler and can change when path rules are configured.
Disabled routes are included. Account credentials can filter with
?namespace=production; namespace credentials are pinned to their namespace.
Database failures return an error, not a successful empty list.
Hostname actions
Use one generic action for each registered hostname: proxy, static, or redirect. Your application chooses policies such as www → apex. Oblien manages hostname ownership, TLS, upload limits and proxy transport. Existing path rules use the same API and remain supported.
const current = await client.routes.get('example.com');
const result = await client.routes.set('example.com', {
action: {
kind: 'redirect',
to: 'https://www.example.com',
status: 308,
preservePath: true,
preserveQuery: true,
},
expectedVersion: current.active_version,
});
// result.publication.state is 'published' or 'pending'.PUT /domain/routes/example.com
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{"action":{"kind":"redirect","to":"https://www.example.com"},"expectedVersion":null}Other actions:
await client.routes.set('api.example.com', {
action: { kind: 'proxy', workspace: 'workspace-id', port: 3000 },
limits: { maxBodyBytes: 10 * 1024 * 1024 },
});
await client.routes.set('docs.example.com', {
action: { kind: 'static', page: 'my-docs' },
spa: false,
});Register the source hostname through its workspace, Page or proxy first. The hostname and its workspace/static target must belong to the same account and namespace. Namespace keys cannot cross that boundary. External proxy origins use the existing target-verification flow. Redirects require ownership of the source; they can point to an external destination.
action cannot be combined with routes or static. A save replaces the whole
configuration. The dashboard requires an explicit choice before replacing an
existing configuration that the hostname form cannot represent.
Redirects and TLS
301, 302, 307 and 308 are supported. New hostname actions default to 308, preserving
the method, path and query. Set preservePath or preserveQuery to false to replace
that part. A configured base path is followed by the request path; queries join
with &, and fragments remain last. Existing legacy forward routes retain 302.
For example, destination https://www.example.com/base?fixed=1#section and request
/guide?lang=en produce
https://www.example.com/base/guide?fixed=1&lang=en#section.
Apex and www are independent hostnames. Both need valid certificates for HTTPS. An HTTP redirect runs after TLS; it cannot repair an invalid source certificate. Direct self-hostname redirects and cycles through registered unconditional redirects are rejected. External or conditional application redirects can still create loops outside Oblien's registry.
Proxy upload limits
const { requestBody } = await client.routes.capabilities();
// requestBody.maxBodyBytes: account ceiling
// requestBody.allowedMaxBodyBytes: supported sizes within that ceilingREST: GET /domain/routes/capabilities (authenticated).
Current native sizes are 1, 5, 10, 25, 50 and 100 MiB. The default ceiling is
100 MiB. Send bytes in limits.maxBodyBytes; use the returned allowed values when
building a selector. This limits request bodies forwarded to an application proxy,
including aggregate chunked uploads. It does not set a static-file size or a CDN
upload quota; CDN uploads keep their signed token and file limits.
Oversized requests do not reach the application. The branded public response uses
X-Oblien-Error-Status: 413 with HTTP 200, as described in
public route failures. Unsupported sizes
return API 400; a value above the account ceiling returns API 403. Rollback checks
the current ceiling again. Operator default changes apply to future saves; existing
published routes require a resave to change their limit.
Existing path rules
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.
Changes are versioned and require no workload redeploy. This is a constrained
path-rule format; it does not interpret a complete vercel.json.
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
Replaces the saved rule table in one SQL transaction and queues its publication. Each successful save creates a new version; edge propagation is asynchronous.
// 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 its managed hostname and current custom apex, excluding
a separate www alias. Use /domain/routes/{hostname} for an independent redirect.
Body
| Field | Type | Description |
|---|---|---|
routes | Rule[] | Ordered rule table (full replace, no merge). |
action | proxy, static or redirect action | Alternative to a rule table. |
expectedVersion | number or null | Compare-and-save guard; null expects an unversioned route. |
limits | { maxBodyBytes } | Native application-proxy request-body limit. |
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, preservePath?, preserveQuery? } | 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 saved = await client.routes.get('app.example.com');
const prior = saved.versions.find(v => v.version !== saved.active_version);
if (prior) await client.routes.rollback('app.example.com', prior.version);Publication and recovery
The configuration, revision and publication intent commit together. If publication
is temporarily unavailable, the API returns 202, success: true, and
publication.state: "pending". The saved change is retained and retried
automatically; do not create another configuration just to retry delivery.
GET and mutation responses include:
{
"publication": {
"operationId": "publication-id",
"state": "published",
"attempts": 1,
"publishedAt": "2026-10-11T12:00:00.000Z"
}
}published means the registry accepted the change, not that every edge has already
installed it. Normal accepted publication returns 200. Poll GET while pending.
The previous route can remain active during an outage. SQL and the edge registry
are separate systems; propagation is eventual.
Recovery reads current ownership and targets, so an old retry cannot overwrite a new owner, moved workspace or deletion. Disabled routes remain disabled when edited. History belongs to the route registration; reclaiming a hostname does not grant access to its previous owner's revisions.
Use expectedVersion when saving from an editor. A stale version returns
409 route_version_conflict. Rollback creates a new version, preserves history,
and checks current origin ownership and upload limits. Legacy origins without a
retained workspace or verified URL return 409 route_origin_refresh_required and
must be configured explicitly again. GET includes route registration metadata
and staticPage when the static source still belongs to this namespace.
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 (
400). - Patterns compile from constrained tokens, with at most nine captures and bounded edge matching. There is no raw-regex API.
.., 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.
Responsibility boundary
Conditional method/header/query matching, has/missing, scripts and response
transformations are not supported. Unknown configuration fields and match
conditions are rejected rather than silently widening a route. Keep application
routing and authentication in the workload. Existing response-header and path
rules remain available. WebSocket and SSE transport use the shared proxy.
Normal route lookup and simple action selection use local caches; there are no per-request database or billing checks. Cold registry reads are coalesced.