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.
Overview
Section titled “Overview”@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
publicRoutesper HTTP method (auth bypass)changeOriginfor rewritingHost/Originsecureto 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)Configuration
Section titled “Configuration”manifest.yaml
Section titled “manifest.yaml”name: "@buntime/plugin-proxy"base: "/redirects"enabled: trueinjectBase: true
dependencies: - "@buntime/plugin-turso"
entrypoint: dist/client/index.htmlpluginEntry: dist/plugin.js
menus: - icon: lucide:network path: /redirects title: Redirects
rules: [] # static rules (optional)Configuration options
Section titled “Configuration options”| Option | Type | Default | Description |
|---|---|---|---|
rules | ProxyRule[] | [] | Static rules (read-only at runtime) |
Everything else (base path, dependencies, menu) follows the standard plugin system schema — see Plugin System.
Storage direction
Section titled “Storage direction”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-proxyowns itsproxy_rulestable and dynamic-rule repository.- The plugin manifest depends on
@buntime/plugin-tursofor durable SQL access, not on@buntime/plugin-keyval. - Do not route proxy storage through
plugin-keyvalasproxy -> 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 CONCURRENTfor concurrent writes. bun:sqliteis 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.
Rule model (ProxyRule)
Section titled “Rule model (ProxyRule)”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | auto | generated | Unique identifier. Static: static-{index}. Dynamic: UUID |
name | string | yes | — | Human-readable name |
pattern | string | yes | — | JS regex tested against the request pathname |
target | string | yes | — | Destination URL (supports ${ENV_VAR} and partial composition) |
rewrite | string | no | — | Rewrite template with $1, $2. Omitted = forwards original pathname |
changeOrigin | boolean | no | false | Rewrites Host and Origin to the target host |
secure | boolean | no | true | TLS certificate verification on the target |
ws | boolean | no | true | Enables WebSocket proxying for this rule |
headers | Record<string, string> | no | {} | Extra headers added to the proxied request (overwrite same-name originals) |
publicRoutes | Record<string, string[]> | no | {} | Paths that bypass authentication, indexed by HTTP method |
Pattern matching
Section titled “Pattern matching”JS regex against the pathname (not the full URL). Capture groups are positional
($1, $2, …):
| Pattern | Matches | Use case |
|---|---|---|
^/api(/.*)?$ | /api, /api/users, /api/users/123 | API gateway |
^/ws(/.*)?$ | /ws, /ws/chat | WebSocket endpoint |
^/v(\d+)/(.*)$ | /v1/users, /v2/data | Versioned API ($1=version) |
^/(\w+)/api(/.*)?$ | /tenant1/api/data | Multi-tenant routing |
^/static/(.*)$ | /static/js/app.js | Asset proxy |
^/(.*)$ | everything | Catch-all |
Evaluation order: static first, dynamic after. Within each group, declaration order — first match wins. Place specific rules before catch-alls.
Rewrite (capture groups)
Section titled “Rewrite (capture groups)”| Pattern | Rewrite | /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.
${ENV_VAR} substitution
Section titled “${ENV_VAR} substitution”Targets accept ${ENV_VAR} resolved at request time. Supports partial composition:
target: "${BACKEND_URL}" # entire variabletarget: "https://${API_HOST}:${API_PORT}" # compositionMissing 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 and secure
Section titled “changeOrigin and secure”changeOrigin: truerewritesHostandOriginto the target host. Use when the target validatesHost, does virtual hosting, or has strict CORS requirements onOrigin.secure: falsedisables TLS verification — useful only in dev against self-signed certificates. Do not use in production.
Custom headers
Section titled “Custom headers”Added to the proxied request on top of the originals. Override on collision:
headers: X-Forwarded-By: buntime Authorization: "Bearer ${API_TOKEN}"WebSocket proxying
Section titled “WebSocket proxying”Enabled by default (ws: true). When an upgrade arrives:
onRequestdetectsUpgrade: websocketand attempts to match against rules withws: true.- On match, uses the
serverreference captured inonServerStartto callserver.upgrade(), attaching{ rule, url }indata. - The
websocket.openhandler opens aWebSocketto the target and relays messages in both directions. - Closing either side closes the other;
onShutdowntears 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── BuntimeClient ──send(msg)──► Buntime ──relay(msg)──► TargetClient ◄──relay(msg)── Buntime ◄──send(msg)── TargetClient ──close──► Buntime ──close──► TargetSpecific configuration:
rules: - name: "Realtime Chat" pattern: "^/ws/chat(/.*)?$" target: "ws://chat-service:8080" rewrite: "/chat$1" ws: true changeOrigin: truePath 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)
API Reference
Section titled “API Reference”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).
| Method | Endpoint | Description |
|---|---|---|
GET | /admin/rules | List all rules (static + dynamic) |
GET | /admin/rules/:id | Get a single rule by id |
POST | /admin/rules | Create dynamic rule in proxy_rules through plugin-turso |
PUT | /admin/rules/reorder | Reorder dynamic rules ({ ids: string[] }) |
PUT | /admin/rules/:id | Update dynamic rule (static → error) |
PATCH | /admin/rules/:id/toggle | Enable/disable a dynamic rule |
DELETE | /admin/rules/:id | Remove dynamic rule (static → 403 error) |
GET /admin/rules
Section titled “GET /admin/rules”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 }]POST /admin/rules
Section titled “POST /admin/rules”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.
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"] } }'PUT /admin/rules/:id
Section titled “PUT /admin/rules/:id”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.
DELETE /admin/rules/:id
Section titled “DELETE /admin/rules/:id”Removes a dynamic rule. Static rules return 403 with
{"error":"Cannot delete static rules"}.
TypeScript types
Section titled “TypeScript types”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;Public routes
Section titled “Public routes”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"] }}| Key | Meaning |
|---|---|
ALL | Matches any HTTP method |
GET | Matches GET only |
POST | Matches POST only |
PUT | Matches PUT only |
DELETE | Matches 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 forwardUse sparingly — each entry in publicRoutes expands the attack surface.
Service sharing
Section titled “Service sharing”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}Lifecycle hooks
Section titled “Lifecycle hooks”| Hook | Role |
|---|---|
onInit | Gets plugin-turso, initializes proxy_rules on demand, loads dynamic rules, and merges them with static ones |
onServerStart | Captures Bun server reference (required for server.upgrade) |
onRequest | Matches request against rules; on match, forwards and short-circuits |
onShutdown | Closes WebSocket connections and cleans up |
websocket | open/message/close handlers for bidirectional relay |
Dependencies
Section titled “Dependencies”| Plugin | Required | Role |
|---|---|---|
@buntime/plugin-turso | Yes | Durable 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:
| Aspect | Static (manifest) | Dynamic (API) |
|---|---|---|
| Defined in | manifest.yaml | REST API |
| Persistence | Always available | proxy_rules through plugin-turso |
| Modifiable? | No | Yes (CRUD) |
| Match order | First | After static rules |
readonly | true | false |
| Use case | Core routing, infra | Ad-hoc services, A/B, hot reload |
File structure
Section titled “File structure”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 outputGuides
Section titled “Guides”Microservices (static rules)
Section titled “Microservices (static rules)”name: "@buntime/plugin-proxy"enabled: truerules: - 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"]HTTP + WebSocket combined
Section titled “HTTP + WebSocket combined”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: trueLegacy → new API migration
Section titled “Legacy → new API migration”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):
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”).
Quick validation
Section titled “Quick validation”# List and filtercurl -s http://localhost:8000/redirects/admin/rules | jq '.[] | {name, pattern, target}'
# Check ${ENV_VAR} resolutioncurl -s http://localhost:8000/redirects/admin/rules | jq '.[].target'
# Test HTTP matchcurl -v http://localhost:8000/api/users
# Test WebSocketwscat -c ws://localhost:8000/ws/chatTroubleshooting
Section titled “Troubleshooting”| Symptom | Investigation |
|---|---|
| Rule does not match request | Validate regex against the pathname; check order (static → dynamic, more specific first); confirm enabled: true in manifest |
| Connection to target fails | curl <target> directly; check resolved ${ENV_VAR} (curl /redirects/admin/rules | jq '.[].target'); for dev HTTPS self-signed use secure: false |
| Dynamic rules disappear after restart | Confirm plugin-turso is active and configured with durable local/sync storage |
400 Cannot modify/delete static rule | The rule is from manifest.yaml — edit the manifest and redeploy |
| WebSocket does not upgrade | ws: 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 public | Add 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 auth | Send Authorization: Bearer <token> and Origin: <base-url> (CSRF protection) |
${ENV_VAR} appears literally in target | Variable not set in the pod environment; populate via Helm values / .env before loading the rule |
Logs in K8s deploy
Section titled “Logs in K8s deploy”kubectl -n zomme logs -f $POD | grep -i proxySecurity considerations
Section titled “Security considerations”- 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: truechangesHost— some targets checkHostas a tenancy/security mechanism; align with the target team.secure: falsein dev only. In production, generate valid certificates (cert-manager) or use service mesh mTLS.
See also Security.