Pular para o conteúdo

Proxy

Este conteúdo não está disponível em sua língua ainda.

Dynamic HTTP and WebSocket reverse proxy, with static rules in the manifest and dynamic rules persisted through @buntime/plugin-turso, so proxy can run independently from KeyVal. Default mount base /redirects, enabled by default.

@buntime/plugin-proxy intercepts requests in the onRequest hook and attempts to match the URL against an ordered set of proxy rules. On the first match, it rewrites the path, resolves ${ENV_VAR} in the target, adjusts headers (changeOrigin, custom headers), and forwards the request — short-circuiting the rest of the pipeline. WebSocket upgrades go through the same matching but use server.upgrade() to establish the upstream connection and bidirectional message relay.

Main features:

  • Pattern matching via JavaScript regex with capture groups ($1, $2, …)
  • Path rewriting referencing capture groups
  • Transparent WebSocket proxying (upgrade + relay)
  • Dynamic rules via REST API, persisted in proxy-owned Turso tables
  • Static rules in manifest.yaml (read-only at runtime)
  • Per-rule custom headers
  • publicRoutes per HTTP method (auth bypass)
  • changeOrigin for rewriting Host/Origin
  • secure to control TLS verification
  • ${ENV_VAR} substitution in targets

API mode: persistent — onRequest and Hono routes live in plugin.ts and run in the main thread (required for server.upgrade() in WebSocket).

Request matching flow:

Request
├─ WebSocket upgrade? ──yes──► match against rules with ws=true
│ └─► server.upgrade() + upstream WebSocket
│ └─► bidirectional relay
└─ no ─► iterate rules (static → dynamic, first match wins)
├─ regex matches pathname? ──no──► continue iterating
│ └─ no match ─► continue pipeline (next middleware/plugin)
└─ yes ─► apply rewrite ($1, $2)
└─► resolve ${ENV_VAR}
└─► apply changeOrigin + custom headers
└─► forward to target
└─► return proxied response (short-circuit)
name: "@buntime/plugin-proxy"
base: "/redirects"
enabled: true
injectBase: true
dependencies:
- "@buntime/plugin-turso"
entrypoint: dist/client/index.html
pluginEntry: dist/plugin.js
menus:
- icon: lucide:network
path: /redirects
title: Redirects
rules: [] # static rules (optional)
OptionTypeDefaultDescription
rulesProxyRule[][]Static rules (read-only at runtime)

Everything else (base path, dependencies, menu) follows the standard plugin system schema — see Plugin System.

The plugin declares a dependency on @buntime/plugin-turso and stores dynamic rules in proxy-owned proxy_rules storage.

The storage architecture is:

  • @buntime/plugin-proxy owns its proxy_rules table and dynamic-rule repository.
  • The plugin manifest depends on @buntime/plugin-turso for durable SQL access, not on @buntime/plugin-keyval.
  • Do not route proxy storage through plugin-keyval as proxy -> keyval -> turso; KeyVal should be validated by its own tests and smoke flows, not by becoming mandatory proxy infrastructure.
  • Turso Database is the durable driver for production because it supports MVCC and BEGIN CONCURRENT for concurrent writes.
  • bun:sqlite is not the default durable driver for this state because SQLite WAL still allows only one writer at a time.

See Turso and Storage for the cross-plugin decision.

FieldTypeRequiredDefaultDescription
idstringautogeneratedUnique identifier. Static: static-{index}. Dynamic: UUID
namestringyesHuman-readable name
patternstringyesJS regex tested against the request pathname
targetstringyesDestination URL (supports ${ENV_VAR} and partial composition)
rewritestringnoRewrite template with $1, $2. Omitted = forwards original pathname
changeOriginbooleannofalseRewrites Host and Origin to the target host
securebooleannotrueTLS certificate verification on the target
wsbooleannotrueEnables WebSocket proxying for this rule
headersRecord<string, string>no{}Extra headers added to the proxied request (overwrite same-name originals)
publicRoutesRecord<string, string[]>no{}Paths that bypass authentication, indexed by HTTP method

JS regex against the pathname (not the full URL). Capture groups are positional ($1, $2, …):

PatternMatchesUse case
^/api(/.*)?$/api, /api/users, /api/users/123API gateway
^/ws(/.*)?$/ws, /ws/chatWebSocket endpoint
^/v(\d+)/(.*)$/v1/users, /v2/dataVersioned API ($1=version)
^/(\w+)/api(/.*)?$/tenant1/api/dataMulti-tenant routing
^/static/(.*)$/static/js/app.jsAsset proxy
^/(.*)$everythingCatch-all

Evaluation order: static first, dynamic after. Within each group, declaration order — first match wins. Place specific rules before catch-alls.

PatternRewrite/api/users
^/api(/.*)?$/api$1/api/users (preserves prefix)
^/backend(/.*)?$$1/backend/users/users
^/api(/.*)?$/v2/api$1/api/users/v2/api/users
^/v(\d+)/api(/.*)?$/version/$1$2/v1/api/data/version/1/data
^/api(/.*)?$(omitted)original path forwarded as-is

Optional capture groups with an empty match resolve to an empty string: ^/api(/.*)?$ + rewrite: "/v2$1"/api produces /v2, /api/users produces /v2/users.

Targets accept ${ENV_VAR} resolved at request time. Supports partial composition:

target: "${BACKEND_URL}" # entire variable
target: "https://${API_HOST}:${API_PORT}" # composition

Missing variables keep the placeholder in the URL — this almost always results in a connection error. Make sure .env/deploy secrets are populated before loading rules that depend on them.

  • changeOrigin: true rewrites Host and Origin to the target host. Use when the target validates Host, does virtual hosting, or has strict CORS requirements on Origin.
  • secure: false disables TLS verification — useful only in dev against self-signed certificates. Do not use in production.

Added to the proxied request on top of the originals. Override on collision:

headers:
X-Forwarded-By: buntime
Authorization: "Bearer ${API_TOKEN}"

Enabled by default (ws: true). When an upgrade arrives:

  1. onRequest detects Upgrade: websocket and attempts to match against rules with ws: true.
  2. On match, uses the server reference captured in onServerStart to call server.upgrade(), attaching { rule, url } in data.
  3. The websocket.open handler opens a WebSocket to the target and relays messages in both directions.
  4. Closing either side closes the other; onShutdown tears down all active connections.

WebSocket relay sequence:

Client ──GET /ws (Upgrade)──► Buntime (Proxy)
│ match rule + server.upgrade()
└──WebSocket connect──► Target
◄──101 Switching Protocols──┘
Client ◄──101 Switching Protocols── Buntime
Client ──send(msg)──► Buntime ──relay(msg)──► Target
Client ◄──relay(msg)── Buntime ◄──send(msg)── Target
Client ──close──► Buntime ──close──► Target

Specific configuration:

rules:
- name: "Realtime Chat"
pattern: "^/ws/chat(/.*)?$"
target: "ws://chat-service:8080"
rewrite: "/chat$1"
ws: true
changeOrigin: true

Path rewrite and changeOrigin operate on the upgrade the same way as in HTTP. Custom headers are forwarded in the upgrade (not in individual messages). TLS terminates at the Buntime server — clients can use wss:// even when the target is an internal ws://.

Limitations:

  • No message transformation (pure relay)
  • No load balancing (1 target per rule)
  • No compression negotiation controlled by the plugin
  • Requires main thread (does not run in workers)

All routes are under /{base}/admin/* — default /redirects/admin/* (the admin API is mounted with .basePath("/admin"), not /api; /api is left free so a proxy rule can claim it). The manifest lists /admin/** in publicRoutes so auth does not intercept; the gate is the plugin’s own createApiKeyMiddleware — send the runtime root key or an admin/editor store key via X-API-Key or Authorization: Bearer. These routes sit outside the runtime’s /_/api CSRF surface, so no Origin is required (harmless to send).

MethodEndpointDescription
GET/admin/rulesList all rules (static + dynamic)
GET/admin/rules/:idGet a single rule by id
POST/admin/rulesCreate dynamic rule in proxy_rules through plugin-turso
PUT/admin/rules/reorderReorder dynamic rules ({ ids: string[] })
PUT/admin/rules/:idUpdate dynamic rule (static → error)
PATCH/admin/rules/:id/toggleEnable/disable a dynamic rule
DELETE/admin/rules/:idRemove dynamic rule (static → 403 error)

200 OK response with array of rules. Static rules appear with readonly: true, dynamic with readonly: false.

[
{
"id": "static-0",
"name": "API Gateway",
"pattern": "^/api(/.*)?$",
"target": "https://api.internal:3000",
"rewrite": "/api$1",
"changeOrigin": true,
"secure": true,
"ws": true,
"headers": {},
"publicRoutes": {},
"readonly": true
}
]

Body is a ProxyRule without id. 201 Created response returns the created rule with a generated UUID id (plus order, enabled, readonly: false). Body parameters follow the Rule model table.

Terminal window
curl -X POST http://localhost:8000/redirects/admin/rules \
-H "Content-Type: application/json" \
-d '{
"name": "Backend API",
"pattern": "^/api(/.*)?$",
"target": "https://api.example.com",
"rewrite": "/api$1",
"changeOrigin": true,
"publicRoutes": {
"GET": ["/api/health"],
"POST": ["/api/webhook"]
}
}'

Updates a dynamic rule — body with fields to change. Attempting to edit a static rule returns an error. PUT /admin/rules/reorder ({ ids }) reorders dynamic rules; PATCH /admin/rules/:id/toggle flips a rule’s enabled flag.

Removes a dynamic rule. Static rules return 403 with {"error":"Cannot delete static rules"}.

export interface ProxyConfig { rules?: ProxyRule[]; }
export interface ProxyRule {
id?: string;
name: string;
pattern: string;
target: string;
rewrite?: string;
changeOrigin?: boolean;
secure?: boolean;
ws?: boolean;
headers?: Record<string, string>;
publicRoutes?: Record<string, string[]>;
}
export interface ProxyRuleResponse extends ProxyRule {
id: string;
readonly: boolean;
}
export type ProxyRoutesType = typeof api;

publicRoutes declares, per rule, paths that should bypass authentication. The plugin exposes the isPublic(pathname, method) method through provides(); the auth layer consults it before requiring a token.

{
"publicRoutes": {
"ALL": ["/api/health"],
"GET": ["/api/config/**"],
"POST": ["/api/webhook"]
}
}
KeyMeaning
ALLMatches any HTTP method
GETMatches GET only
POSTMatches POST only
PUTMatches PUT only
DELETEMatches DELETE only

Paths support glob: * (one segment) and ** (multiple segments). Example flow:

GET /api/health
→ proxy matches against the rule
→ auth calls proxy.isPublic("/api/health", "GET")
→ publicRoutes.ALL contains "/api/health" → true
→ auth skips authentication and proxy executes the forward

Use sparingly — each entry in publicRoutes expands the attack surface.

Via provides(), the plugin exposes:

{
isPublic: (pathname: string, method: string) => boolean
}

A consumer reaches it through the plugin context:

const proxy = ctx.getPlugin("@buntime/plugin-proxy");
if (proxy?.isPublic(request.pathname, request.method)) {
return next(); // auth bypass
}
HookRole
onInitGets plugin-turso, initializes proxy_rules on demand, loads dynamic rules, and merges them with static ones
onServerStartCaptures Bun server reference (required for server.upgrade)
onRequestMatches request against rules; on match, forwards and short-circuits
onShutdownCloses WebSocket connections and cleans up
websocketopen/message/close handlers for bidirectional relay
PluginRequiredRole
@buntime/plugin-tursoYesDurable SQL provider for proxy_rules

Without plugin-turso, proxy still supports static manifest rules but dynamic rule API mutations return 400 Dynamic rules not enabled.

Static vs dynamic:

AspectStatic (manifest)Dynamic (API)
Defined inmanifest.yamlREST API
PersistenceAlways availableproxy_rules through plugin-turso
Modifiable?NoYes (CRUD)
Match orderFirstAfter static rules
readonlytruefalse
Use caseCore routing, infraAd-hoc services, A/B, hot reload
plugins/plugin-proxy/
├── manifest.yaml # Configuration + static rules
├── plugin.ts # Hooks (onRequest, websocket, provides)
├── index.ts # Worker entrypoint (UI SPA)
├── server/
│ ├── api.ts # Hono routes (CRUD)
│ └── services.ts # Matching logic and rule management
├── client/ # React SPA + TanStack Router
│ └── hooks/
│ └── use-proxy-rules.ts
└── dist/ # Build output
name: "@buntime/plugin-proxy"
enabled: true
rules:
- name: "Auth Service"
pattern: "^/auth(/.*)?$"
target: "${AUTH_URL}"
rewrite: "$1"
changeOrigin: true
publicRoutes:
POST: ["/auth/login", "/auth/register"]
- name: "Users Service"
pattern: "^/users(/.*)?$"
target: "${USERS_URL}"
rewrite: "$1"
changeOrigin: true
- name: "Payments Service"
pattern: "^/payments(/.*)?$"
target: "${PAYMENTS_URL}"
rewrite: "$1"
changeOrigin: true
publicRoutes:
POST: ["/payments/webhook"]
rules:
- name: "API"
pattern: "^/api(/.*)?$"
target: "https://backend:3000"
rewrite: "/api$1"
changeOrigin: true
ws: false # HTTP only
- name: "Realtime"
pattern: "^/ws(/.*)?$"
target: "ws://realtime:8080"
rewrite: "$1"
ws: true

Place the new rule before the legacy one (first match wins):

rules:
- name: "New API"
pattern: "^/v2/api(/.*)?$"
target: "https://new-api:3000"
rewrite: "$1"
changeOrigin: true
- name: "Legacy API"
pattern: "^/api(/.*)?$"
target: "https://legacy-api:3000"
rewrite: "/api$1"
changeOrigin: true
headers: { X-Legacy: "true" }

Multi-service setup via REST (K8s/Rancher deploy)

Section titled “Multi-service setup via REST (K8s/Rancher deploy)”

Provision dynamic rules after the pod is ready — useful when the set of services changes without rebuilding the image. In deployments with auth enabled, always send Authorization and Origin (CSRF):

Terminal window
TOKEN="your-jwt-token"
BASE_URL="https://buntime.home"
HDRS=(-H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -H "Origin: $BASE_URL")
curl -X POST $BASE_URL/redirects/admin/rules "${HDRS[@]}" -d '{
"name": "Front Manager API",
"pattern": "^/api(/.*)?$",
"target": "https://backend.example.com",
"rewrite": "/api$1",
"changeOrigin": true,
"publicRoutes": { "GET": ["/api/config/**"] }
}'
curl -X POST $BASE_URL/redirects/admin/rules "${HDRS[@]}" -d '{
"name": "Edge Runtime",
"pattern": "^/a(/.*)?$",
"target": "https://edge.example.com",
"rewrite": "/a$1",
"changeOrigin": true,
"publicRoutes": { "GET": ["/a/translate-api/**"] }
}'
curl -s $BASE_URL/redirects/admin/rules -H "Authorization: Bearer $TOKEN" \
| jq '.[] | {name, pattern}'

For a reusable TypeScript CRUD SDK, see plugins/plugin-proxy/docs/api-reference.md (section “Client SDK Example”).

Terminal window
# List and filter
curl -s http://localhost:8000/redirects/admin/rules | jq '.[] | {name, pattern, target}'
# Check ${ENV_VAR} resolution
curl -s http://localhost:8000/redirects/admin/rules | jq '.[].target'
# Test HTTP match
curl -v http://localhost:8000/api/users
# Test WebSocket
wscat -c ws://localhost:8000/ws/chat
SymptomInvestigation
Rule does not match requestValidate regex against the pathname; check order (static → dynamic, more specific first); confirm enabled: true in manifest
Connection to target failscurl <target> directly; check resolved ${ENV_VAR} (curl /redirects/admin/rules | jq '.[].target'); for dev HTTPS self-signed use secure: false
Dynamic rules disappear after restartConfirm plugin-turso is active and configured with durable local/sync storage
400 Cannot modify/delete static ruleThe rule is from manifest.yaml — edit the manifest and redeploy
WebSocket does not upgradews: true on the rule; pattern matches the upgrade path; deploy must be on the main thread (not a worker)
Auth blocks a route that should be publicAdd path to publicRoutes under the correct method; remember that glob ** is multi-segment and * is a single segment
POST/PUT/DELETE rejected in deploy with authSend Authorization: Bearer <token> and Origin: <base-url> (CSRF protection)
${ENV_VAR} appears literally in targetVariable not set in the pod environment; populate via Helm values / .env before loading the rule
Terminal window
kubectl -n zomme logs -f $POD | grep -i proxy
  • Proxy only to trusted targets — a catch-all rule (^/(.*)$) with an external target is an SSRF vector.
  • Minimize publicRoutes. Each glob expands the attack surface.
  • For internal services, prefer cluster-internal DNS (do not expose external hosts unnecessarily).
  • changeOrigin: true changes Host — some targets check Host as a tenancy/security mechanism; align with the target team.
  • secure: false in dev only. In production, generate valid certificates (cert-manager) or use service mesh mTLS.

See also Security.