1tube 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,23 @@
1
1
  ![NPM Version](https://img.shields.io/npm/v/1tube)
2
+ ![NuGet Version](https://img.shields.io/nuget/v/OneTube?v=2&icon=nuget)
2
3
 
3
4
  # 1tube
4
5
 
5
- Self-hosted Supabase Edge Functions gateway. Runs your Deno edge functions locally or behind a .NET host with zero-copy YARP proxying — no cold starts, no version lock, no Supabase compute dependency.
6
+ Self-hosted Supabase Edge Functions gateway. Runs your Deno edge functions
7
+ locally or behind a .NET host with zero-copy YARP proxying — no cold starts, no
8
+ version lock, no Supabase compute dependency.
6
9
 
7
10
  ## How it works
8
11
 
9
- 1tube discovers edge function modules from a `supabase/functions/` directory and hosts them in a single Deno HTTP server. Each function's `serve()` call registers a handler in a global registry instead of starting a separate `Deno.serve()`. The gateway then routes requests, handles JWT auth, CORS, rate limiting, and structured logging.
12
+ 1tube discovers edge function modules from a `supabase/functions/` directory and
13
+ hosts them in a single Deno HTTP server. Each function's `serve()` call
14
+ registers a handler in a global registry instead of starting a separate
15
+ `Deno.serve()`. The gateway then routes requests, handles JWT auth, CORS, rate
16
+ limiting, and structured logging.
10
17
 
11
- Individual edge functions require **zero changes** — only the shared `_shared/handler.ts` wrapper needs a small shim (4 lines) to detect the 1tube registry.
18
+ Individual edge functions require **zero changes** — only the shared
19
+ `_shared/handler.ts` wrapper needs a small shim (4 lines) to detect the 1tube
20
+ registry.
12
21
 
13
22
  ## Quick start (local dev)
14
23
 
@@ -23,51 +32,77 @@ cp .env.example .env
23
32
  bun run dev -- --functions ../sciobot-next/supabase/functions
24
33
  ```
25
34
 
26
- The gateway starts on `http://localhost:3100`. Functions are available at `http://localhost:3100/functions/v1/<name>`.
35
+ The gateway starts on `http://localhost:3100`. Functions are available at
36
+ `http://localhost:3100/functions/v1/<name>`.
27
37
 
28
- In dev mode, 1tube also watches the functions directory and reloads handlers in-process on filesystem changes (including newly created function folders).
38
+ In dev mode, 1tube also watches the functions directory and reloads handlers
39
+ in-process on filesystem changes (including newly created function folders).
40
+
41
+ ## npm CLI
42
+
43
+ The published npm package exposes a `1tube` binary. It is a tiny Node shim that
44
+ launches this repo's Deno CLI, so machines using it need Deno on `PATH`.
45
+
46
+ ```bash
47
+ # Show CLI help
48
+ npx 1tube
49
+
50
+ # Build and sign a firmware payload in one command
51
+ npx 1tube package --functions supabase/functions --out fw.1tube --sign-key "$1TUBE_PACKAGE_SIGN_KEY"
52
+
53
+ # Keep/update the intermediate dist/ artifact as well
54
+ npx 1tube package --functions supabase/functions --in dist --out fw.1tube --sign-key "$1TUBE_PACKAGE_SIGN_KEY"
55
+ ```
56
+
57
+ This replaces local-source invocations such as
58
+ `deno run -A ../1tube/src/cli.ts ...` in downstream CI pipelines.
29
59
 
30
60
  ### Deno npm lifecycle scripts
31
61
 
32
- Deno does not run npm `postinstall` / build scripts unless you allow them. `deno.lock` may still list transitive packages such as `protobufjs`; the `check` script uses `--allow-scripts=npm:protobufjs` so `bun run check` / `npm publish` (via `prepublishOnly`) stays warning-free. That script is only needed for dependency install — **1tube’s runtime graph is just Hono + JSR std**.
62
+ Deno does not run npm `postinstall` / build scripts unless you allow them.
63
+ `deno.lock` may still list transitive packages such as `protobufjs`; the `check`
64
+ script uses `--allow-scripts=npm:protobufjs` so `bun run check` / `npm publish`
65
+ (via `prepublishOnly`) stays warning-free. That script is only needed for
66
+ dependency install — **1tube’s runtime graph is just Hono + JSR std**.
33
67
 
34
68
  ## Endpoints
35
69
 
36
- | Path | Description |
37
- |---|---|
38
- | `GET /` | Liveness probe (`{"status":"ok"}`) — intentionally minimal so unauthenticated callers don't see the function map. |
39
- | `POST /functions/v1/:name` | Invoke an edge function |
40
- | `GET /health` | Auth-gated health (`Authorization: Bearer $INTERNAL_KEY`); without auth returns the same minimal `{"status":"ok"}`. |
41
- | `GET /metrics` | Auth-gated Prometheus exposition (same scheme). |
70
+ | Path | Description |
71
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
72
+ | `GET /` | Liveness probe (`{"status":"ok"}`) — intentionally minimal so unauthenticated callers don't see the function map. |
73
+ | `POST /functions/v1/:name` | Invoke an edge function |
74
+ | `GET /health` | Auth-gated health (`Authorization: Bearer $INTERNAL_KEY`); without auth returns the same minimal `{"status":"ok"}`. |
75
+ | `GET /metrics` | Auth-gated Prometheus exposition (same scheme). |
42
76
 
43
77
  ## Configuration
44
78
 
45
- All knobs default to safe-but-backwards-compatible values. The TS gateway (`src/server.ts`) reads:
46
-
47
- | Env / flag | Default | Notes |
48
- |---|---|---|
49
- | `--port` / `PORT` | `3100` | Listen port |
50
- | `--host` / `1TUBE_HOST` | `127.0.0.1` | Loopback by default — pass `--host 0.0.0.0` to expose. |
51
- | `--functions` / `FUNCTIONS_PATH` | `./supabase/functions` | Functions root |
52
- | `--timeout` / `FUNCTION_TIMEOUT_MS` | `150000` | Per-request wall-clock cap (also overridable per function) |
53
- | `--dev` / `1TUBE_DEV` | off | Applies the well-known local Supabase JWT/secrets. **Refuses to start in prod when JWT_SECRET / SUPABASE_SERVICE_ROLE_KEY are missing or are the public dev defaults.** |
54
- | `--hmr` / `1TUBE_HMR` | off | File-watch + per-function reload (dev only). |
55
- | `1TUBE_BODY_LIMIT_MB` | `30` | Hono `bodyLimit`; matches Supabase. Returns 413 before the handler runs. |
56
- | `1TUBE_BODY_READ_MS` | `30000` | Slow-loris guard. Max idle gap (ms) between body chunks before the request is aborted with **408**. NOT a total body-read deadline — large but fast uploads pass through. Set `0` to disable. |
57
- | `1TUBE_CORS_ORIGIN` | `*` (dev only) | Comma-separated allowlist or `*`. In prod, leaving unset disables CORS. |
58
- | `1TUBE_TRUSTED_PROXIES` | empty | Comma-separated list of remote IPs whose `X-Forwarded-For` is honored. Anything else uses the raw socket address — XFF spoofing no longer mints fresh rate-limit buckets. |
59
- | `1TUBE_SHUTDOWN_GRACE_MS` | `10000` | SIGINT/SIGTERM drain budget. |
60
- | `INTERNAL_KEY` | unset | Required to read detailed `/health` and `/metrics`. Header-only: `Authorization: Bearer $INTERNAL_KEY`. |
61
- | `--backend` / `1TUBE_BACKEND` | `deno` | Function execution engine. `deno` (default) imports each function as a module in this process — same as 1tube has always done. `workerd` bundles each function and serves it from a Cloudflare-style workerd subprocess for hard isolation. See [Workerd backend](#workerd-backend) below. |
62
- | `--workerd-bin` / `1TUBE_WORKERD_BIN` | `workerd` (PATH) | Path to the workerd binary. Only consulted with `--backend workerd`. |
63
- | `--workerd-env` / `1TUBE_WORKERD_ENV` | unset (= forward all) | Comma-separated allowlist of env var names to forward to bundled functions under `--backend workerd`. Each name reaches the bundle via `Deno.env.get(name)` and the worker's `env` binding. Values stay in the gateway's process env — they are NEVER written into `config.capnp`. **When unset (or `*`), every env var the gateway can see is forwarded** — bundled functions inherit their parent's environment like a regular `deno run` child. Pass an explicit list to narrow the surface for shared/multi-tenant workerds. |
64
- | `--kill-stale-workerd` / `1TUBE_KILL_STALE_WORKERD` | off | When the boot-time port preflight finds one of workerd's socket ports already busy, automatically run `taskkill /F /IM workerd.exe` (Windows) or `pkill -9 -x workerd` (Unix), wait briefly, and re-probe. Only ever targets processes named `workerd` — non-workerd processes holding the port still surface the normal preflight error. Off by default; recommended for dev where leftover workerds from prior runs are the common cause of flaky boots. |
65
- | `--inspector` / `--inspector-addr=ADDR` / `1TUBE_INSPECTOR` | off | Launch workerd with the V8 inspector bound to `ADDR` (default `127.0.0.1:9229` when `--inspector` has no value). Lets Chrome DevTools / Node-style debuggers attach for breakpoints inside bundled functions. **Local dev only** — opens an unauthenticated debug port. See [V8 inspector / debugger access](#v8-inspector--debugger-access) below. |
66
- | `1TUBE_SHUTDOWN_GRACE_MS` | `10000` | Total wall-clock budget (ms) for shutdown, split between (1) draining gateway in-flight requests and (2) tearing down the workerd subprocess. See [Graceful shutdown](#graceful-shutdown) below. |
67
- | `1TUBE_DEFAULT_RPM` | `120` | Override the gateway-wide default rate limit (requests per minute, per IP, per function). Per-function `1tube.json#rpm` still wins. |
68
- | `1TUBE_DISABLE_RATE_LIMIT` | unset | Set to `1` to bypass rate limiting entirely. **Load-test / dev only** — production deployments must keep the limiter on. The gateway prints a clear warning at boot when this is enabled. |
69
- ```
70
-
79
+ All knobs default to safe-but-backwards-compatible values. The TS gateway
80
+ (`src/server.ts`) reads:
81
+
82
+ | Env / flag | Default | Notes |
83
+ | ----------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `--port` / `PORT` | `3100` | Listen port |
85
+ | `--host` / `1TUBE_HOST` | `127.0.0.1` | Loopback by default — pass `--host 0.0.0.0` to expose. |
86
+ | `--functions` / `FUNCTIONS_PATH` | `./supabase/functions` | Functions root |
87
+ | `--timeout` / `FUNCTION_TIMEOUT_MS` | `150000` | Per-request wall-clock cap (also overridable per function) |
88
+ | `--dev` / `1TUBE_DEV` | off | Applies the well-known local Supabase JWT/secrets. **Refuses to start in prod when JWT_SECRET / SUPABASE_SERVICE_ROLE_KEY are missing or are the public dev defaults.** |
89
+ | `--hmr` / `1TUBE_HMR` | off | File-watch + per-function reload (dev only). |
90
+ | `1TUBE_BODY_LIMIT_MB` | `30` | Hono `bodyLimit`; matches Supabase. Returns 413 before the handler runs. |
91
+ | `1TUBE_BODY_READ_MS` | `30000` | Slow-loris guard. Max idle gap (ms) between body chunks before the request is aborted with **408**. NOT a total body-read deadline — large but fast uploads pass through. Set `0` to disable. |
92
+ | `1TUBE_CORS_ORIGIN` | `*` (dev only) | Comma-separated allowlist or `*`. In prod, leaving unset disables CORS. |
93
+ | `1TUBE_TRUSTED_PROXIES` | empty | Comma-separated list of remote IPs whose `X-Forwarded-For` is honored. Anything else uses the raw socket address — XFF spoofing no longer mints fresh rate-limit buckets. |
94
+ | `1TUBE_SHUTDOWN_GRACE_MS` | `10000` | SIGINT/SIGTERM drain budget. |
95
+ | `INTERNAL_KEY` | unset | Required to read detailed `/health` and `/metrics`. Header-only: `Authorization: Bearer $INTERNAL_KEY`. |
96
+ | `--backend` / `1TUBE_BACKEND` | `deno` | Function execution engine. `deno` (default) imports each function as a module in this process — same as 1tube has always done. `workerd` bundles each function and serves it from a Cloudflare-style workerd subprocess for hard isolation. See [Workerd backend](#workerd-backend) below. |
97
+ | `--workerd-bin` / `1TUBE_WORKERD_BIN` | `workerd` (PATH) | Path to the workerd binary. Only consulted with `--backend workerd`. |
98
+ | `--workerd-env` / `1TUBE_WORKERD_ENV` | unset (= forward all) | Comma-separated allowlist of env var names to forward to bundled functions under `--backend workerd`. Each name reaches the bundle via `Deno.env.get(name)` and the worker's `env` binding. Values stay in the gateway's process env — they are NEVER written into `config.capnp`. **When unset (or `*`), every env var the gateway can see is forwarded** — bundled functions inherit their parent's environment like a regular `deno run` child. Pass an explicit list to narrow the surface for shared/multi-tenant workerds. |
99
+ | `--kill-stale-workerd` / `1TUBE_KILL_STALE_WORKERD` | off | When the boot-time port preflight finds one of workerd's socket ports already busy, automatically run `taskkill /F /IM workerd.exe` (Windows) or `pkill -9 -x workerd` (Unix), wait briefly, and re-probe. Only ever targets processes named `workerd` — non-workerd processes holding the port still surface the normal preflight error. Off by default; recommended for dev where leftover workerds from prior runs are the common cause of flaky boots. |
100
+ | `--inspector` / `--inspector-addr=ADDR` / `1TUBE_INSPECTOR` | off | Launch workerd with the V8 inspector bound to `ADDR` (default `127.0.0.1:9229` when `--inspector` has no value). Lets Chrome DevTools / Node-style debuggers attach for breakpoints inside bundled functions. **Local dev only** — opens an unauthenticated debug port. See [V8 inspector / debugger access](#v8-inspector--debugger-access) below. |
101
+ | `1TUBE_SHUTDOWN_GRACE_MS` | `10000` | Total wall-clock budget (ms) for shutdown, split between (1) draining gateway in-flight requests and (2) tearing down the workerd subprocess. See [Graceful shutdown](#graceful-shutdown) below. |
102
+ | `1TUBE_DEFAULT_RPM` | `120` | Override the gateway-wide default rate limit (requests per minute, per IP, per function). Per-function `1tube.json#rpm` still wins. |
103
+ | `1TUBE_DISABLE_RATE_LIMIT` | unset | Set to `1` to bypass rate limiting entirely. **Load-test / dev only** — production deployments must keep the limiter on. The gateway prints a clear warning at boot when this is enabled. |
104
+
105
+ ````
71
106
  ## Workerd backend
72
107
 
73
108
  1tube can optionally run each function inside a [workerd](https://github.com/cloudflare/workerd) subprocess instead of importing it directly into the gateway. This gives:
@@ -90,7 +125,7 @@ deno run --allow-all src/server.ts \
90
125
  --functions ./playground \
91
126
  --port 3100 \
92
127
  --dev
93
- ```
128
+ ````
94
129
 
95
130
  You should see something like:
96
131
 
@@ -119,17 +154,29 @@ curl -i http://127.0.0.1:3100/functions/v1/no-such-fn
119
154
  # → HTTP/1.1 404 Not Found
120
155
  ```
121
156
 
122
- The full e2e test (`tests/workerd-e2e.test.ts`) automates exactly the same checks against a real workerd subprocess; run `deno test --allow-all tests/workerd-e2e.test.ts` for one-shot verification.
157
+ The full e2e test (`tests/workerd-e2e.test.ts`) automates exactly the same
158
+ checks against a real workerd subprocess; run
159
+ `deno test --allow-all tests/workerd-e2e.test.ts` for one-shot verification.
123
160
 
124
161
  ### Compatibility date
125
162
 
126
- Workerd applies a `compatibilityDate` to enable behaviour-change opt-ins. 1tube defaults to today's date, then **clamps it down** to whatever the installed workerd binary actually accepts — derived from the binary's version string (`1.YYYYMMDD.N`), since workerd refuses dates later than its build date. You only see a clamp warning if you explicitly pinned a date the binary can't honour.
163
+ Workerd applies a `compatibilityDate` to enable behaviour-change opt-ins. 1tube
164
+ defaults to today's date, then **clamps it down** to whatever the installed
165
+ workerd binary actually accepts — derived from the binary's version string
166
+ (`1.YYYYMMDD.N`), since workerd refuses dates later than its build date. You
167
+ only see a clamp warning if you explicitly pinned a date the binary can't
168
+ honour.
127
169
 
128
170
  ### Forwarding env vars (secrets, API keys)
129
171
 
130
- **Default: every env var the gateway sees is forwarded.** Bundled functions inherit their parent's environment the same way a `deno run ./functions/foo/index.ts` child would — so `Deno.env.get("OPENAI_API_KEY")` Just Works without any extra wiring. This is the developer-friendly default; use it for solo projects and trusted code.
172
+ **Default: every env var the gateway sees is forwarded.** Bundled functions
173
+ inherit their parent's environment the same way a
174
+ `deno run ./functions/foo/index.ts` child would — so
175
+ `Deno.env.get("OPENAI_API_KEY")` Just Works without any extra wiring. This is
176
+ the developer-friendly default; use it for solo projects and trusted code.
131
177
 
132
- Pass an explicit allowlist when you care about isolation (shared multi-tenant workerds, reproducible build artifacts, etc.):
178
+ Pass an explicit allowlist when you care about isolation (shared multi-tenant
179
+ workerds, reproducible build artifacts, etc.):
133
180
 
134
181
  ```bash
135
182
  # CLI flag — narrows to exactly these names
@@ -145,17 +192,28 @@ deno run --allow-all src/server.ts \
145
192
  --workerd-env=*
146
193
  ```
147
194
 
148
- Each listed name becomes a `(name = X, fromEnvironment = X)` binding in the generated `config.capnp`. Workerd reads the **value** from its own process env at boot; the on-disk capnp file contains nothing but the var names. This means:
195
+ Each listed name becomes a `(name = X, fromEnvironment = X)` binding in the
196
+ generated `config.capnp`. Workerd reads the **value** from its own process env
197
+ at boot; the on-disk capnp file contains nothing but the var names. This means:
149
198
 
150
- - Operators can rotate secrets by restarting the gateway with new env values; no cache rebuild needed.
199
+ - Operators can rotate secrets by restarting the gateway with new env values; no
200
+ cache rebuild needed.
151
201
  - A leaked `config.capnp` cannot leak secrets — it never had them.
152
- - Vars listed but absent from the gateway env get a single grouped warning at boot, then are simply not exposed (workerd would otherwise refuse to start).
202
+ - Vars listed but absent from the gateway env get a single grouped warning at
203
+ boot, then are simply not exposed (workerd would otherwise refuse to start).
153
204
 
154
- > **A note on the default.** Earlier versions of 1tube defaulted to *deny-all* and required explicit opt-in. That was friction for the 90%-case (single-app deploys where the gateway and the functions trust each other) and surprised people whose `.env` already worked everywhere else. The deny-all stance is still one keystroke away — `--workerd-env=A,B,C` — and the per-function `permissions.env` manifest knob still narrows further under `1TUBE_ENFORCE_MANIFEST=1`.
205
+ > **A note on the default.** Earlier versions of 1tube defaulted to _deny-all_
206
+ > and required explicit opt-in. That was friction for the 90%-case (single-app
207
+ > deploys where the gateway and the functions trust each other) and surprised
208
+ > people whose `.env` already worked everywhere else. The deny-all stance is
209
+ > still one keystroke away — `--workerd-env=A,B,C` — and the per-function
210
+ > `permissions.env` manifest knob still narrows further under
211
+ > `1TUBE_ENFORCE_MANIFEST=1`.
155
212
 
156
213
  ### Bundle sizes at boot
157
214
 
158
- Workerd boot prints a sorted table so unintended npm bloat is visible immediately:
215
+ Workerd boot prints a sorted table so unintended npm bloat is visible
216
+ immediately:
159
217
 
160
218
  ```
161
219
  [1tube] workerd bundle sizes (sorted, largest first):
@@ -166,22 +224,46 @@ Workerd boot prints a sorted table so unintended npm bloat is visible immediatel
166
224
  total 492.6KB (1131ms across 7 fns)
167
225
  ```
168
226
 
169
- If a bundle is much bigger than expected (50MB+), look for an unintended deep import — `nodejs_compat` shims most of `node:*` automatically and you rarely need to import a "browser polyfill" version of an npm package.
227
+ If a bundle is much bigger than expected (50MB+), look for an unintended deep
228
+ import — `nodejs_compat` shims most of `node:*` automatically and you rarely
229
+ need to import a "browser polyfill" version of an npm package.
170
230
 
171
231
  ### Compatibility date
172
232
 
173
- Workerd applies a `compatibilityDate` to enable behaviour-change opt-ins. 1tube defaults to today's date, then **clamps it down** to whatever the installed workerd binary actually accepts — derived from the binary's version string (`1.YYYYMMDD.N`), since workerd refuses dates later than its build date. You only see a clamp warning if you explicitly pinned a date the binary can't honour.
233
+ Workerd applies a `compatibilityDate` to enable behaviour-change opt-ins. 1tube
234
+ defaults to today's date, then **clamps it down** to whatever the installed
235
+ workerd binary actually accepts — derived from the binary's version string
236
+ (`1.YYYYMMDD.N`), since workerd refuses dates later than its build date. You
237
+ only see a clamp warning if you explicitly pinned a date the binary can't
238
+ honour.
174
239
 
175
240
  ### Per-function manifests on workerd
176
241
 
177
- Each `<function>/1tube.json` is loaded at workerd boot and applied the same way the Deno backend uses it:
178
-
179
- - **`timeoutMs`** — per-function dispatch timeout. The gateway aborts the request and returns 504 if the bundled handler doesn't respond within this window. Falls back to `--timeout` when unset.
180
- - **`rpm`** — per-function rate limit. The gateway-wide rate limiter consults the workerd manifest the same way it consults Deno-side ones; manifest values override `1TUBE_DEFAULT_RPM`.
181
- - **`recycle`** — circuit breaker. After `errorWindow` requests with at least `errorRate` of them returning 5xx (or aborting), the breaker opens for `cooldownMs`. While open, requests short-circuit with `503 Retry-After: <seconds>` and a `{"reason": "circuit_breaker_open"}` body — workerd is never touched. The breaker half-opens automatically once the cool-down elapses.
182
- - **`permissions.env`** — per-function env scoping. Default behaviour: every function sees the full `--workerd-env` allowlist (manifest entries are documentation only). Set `1TUBE_ENFORCE_MANIFEST=1` to switch to strict mode, where each function only sees the *intersection* of the gateway allowlist and its declared `permissions.env`. Use `["*"]` to opt back into the full surface under enforcement.
183
-
184
- Functions without a `1tube.json` get the defaults from `defaultManifest()` — wide-open env, gateway-default timeout/rpm, errorRate=0.5 over a 20-request window with a 30s cool-down.
242
+ Each `<function>/1tube.json` is loaded at workerd boot and applied the same way
243
+ the Deno backend uses it:
244
+
245
+ - **`timeoutMs`** — per-function dispatch timeout. The gateway aborts the
246
+ request and returns 504 if the bundled handler doesn't respond within this
247
+ window. Falls back to `--timeout` when unset.
248
+ - **`rpm`** — per-function rate limit. The gateway-wide rate limiter consults
249
+ the workerd manifest the same way it consults Deno-side ones; manifest values
250
+ override `1TUBE_DEFAULT_RPM`.
251
+ - **`recycle`** — circuit breaker. After `errorWindow` requests with at least
252
+ `errorRate` of them returning 5xx (or aborting), the breaker opens for
253
+ `cooldownMs`. While open, requests short-circuit with
254
+ `503 Retry-After: <seconds>` and a `{"reason": "circuit_breaker_open"}` body —
255
+ workerd is never touched. The breaker half-opens automatically once the
256
+ cool-down elapses.
257
+ - **`permissions.env`** — per-function env scoping. Default behaviour: every
258
+ function sees the full `--workerd-env` allowlist (manifest entries are
259
+ documentation only). Set `1TUBE_ENFORCE_MANIFEST=1` to switch to strict mode,
260
+ where each function only sees the _intersection_ of the gateway allowlist and
261
+ its declared `permissions.env`. Use `["*"]` to opt back into the full surface
262
+ under enforcement.
263
+
264
+ Functions without a `1tube.json` get the defaults from `defaultManifest()` —
265
+ wide-open env, gateway-default timeout/rpm, errorRate=0.5 over a 20-request
266
+ window with a 30s cool-down.
185
267
 
186
268
  #### Example — tight breaker
187
269
 
@@ -200,22 +282,40 @@ Three consecutive 500s trip the breaker; the gateway logs:
200
282
  [1tube] Circuit breaker OPEN for "boom" (workerd) — refusing requests for 5000ms (errorRate >= 1).
201
283
  ```
202
284
 
203
- The 4th request returns 503 with `Retry-After: 5` without ever reaching the workerd subprocess. Verified end-to-end in `tests/workerd-e2e.test.ts`.
285
+ The 4th request returns 503 with `Retry-After: 5` without ever reaching the
286
+ workerd subprocess. Verified end-to-end in `tests/workerd-e2e.test.ts`.
204
287
 
205
288
  ### HMR on workerd
206
289
 
207
- `--hmr` (or `1TUBE_HMR=1`) enables live reload on the workerd backend. The gateway watches the functions directory and on each save:
208
-
209
- 1. Classifies the changed path. A change inside a function dir (e.g. `playground/hello/index.ts` or `playground/hello/1tube.json`) only re-bundles that one function; a change to anything starting with `_` (`_shared/`, `_internal/`, etc.) triggers a full re-bundle since shared code may be imported by any function.
210
- 2. Coalesces a 200ms burst of save events (editor format-on-save typically writes the file 2–3 times within ~50ms).
211
- 3. Re-bundles the affected function(s) using the long-lived esbuild worker. Bundles for unchanged functions stay on disk.
212
- 4. Generates a fresh `config.gen-{0|1}.capnp` on shifted ports (alternating between `basePort` and `basePort+500`) so the new workerd never collides with the still-serving old one.
213
- 5. Spawns the new workerd, waits for TCP readiness, then atomically swaps the gateway's route table.
214
- 6. Stops the old workerd. In-flight requests against the old process keep their captured origin URL and complete naturally; only NEW requests go to the new workerd. There is no request-drop window.
215
-
216
- A typical incremental swap completes in **200–800ms** on a warm cache (single-function rebundle + workerd cold start). Manifests (`1tube.json`) are re-read on every reload and propagated into the registry / supervisor before the new workerd starts serving, so an `rpm` or `timeoutMs` edit applies to the very first post-reload request.
217
-
218
- Failures are non-fatal: a syntax error in your source code aborts the swap, the old workerd keeps serving, and the next save retries. The reloader logs:
290
+ `--hmr` (or `1TUBE_HMR=1`) enables live reload on the workerd backend. The
291
+ gateway watches the functions directory and on each save:
292
+
293
+ 1. Classifies the changed path. A change inside a function dir (e.g.
294
+ `playground/hello/index.ts` or `playground/hello/1tube.json`) only re-bundles
295
+ that one function; a change to anything starting with `_` (`_shared/`,
296
+ `_internal/`, etc.) triggers a full re-bundle since shared code may be
297
+ imported by any function.
298
+ 2. Coalesces a 200ms burst of save events (editor format-on-save typically
299
+ writes the file 2–3 times within ~50ms).
300
+ 3. Re-bundles the affected function(s) using the long-lived esbuild worker.
301
+ Bundles for unchanged functions stay on disk.
302
+ 4. Generates a fresh `config.gen-{0|1}.capnp` on shifted ports (alternating
303
+ between `basePort` and `basePort+500`) so the new workerd never collides with
304
+ the still-serving old one.
305
+ 5. Spawns the new workerd, waits for TCP readiness, then atomically swaps the
306
+ gateway's route table.
307
+ 6. Stops the old workerd. In-flight requests against the old process keep their
308
+ captured origin URL and complete naturally; only NEW requests go to the new
309
+ workerd. There is no request-drop window.
310
+
311
+ A typical incremental swap completes in **200–800ms** on a warm cache
312
+ (single-function rebundle + workerd cold start). Manifests (`1tube.json`) are
313
+ re-read on every reload and propagated into the registry / supervisor before the
314
+ new workerd starts serving, so an `rpm` or `timeoutMs` edit applies to the very
315
+ first post-reload request.
316
+
317
+ Failures are non-fatal: a syntax error in your source code aborts the swap, the
318
+ old workerd keeps serving, and the next save retries. The reloader logs:
219
319
 
220
320
  ```
221
321
  [1tube] HMR 1 function(s) changed: hello
@@ -228,27 +328,45 @@ or, on failure:
228
328
  [1tube] HMR reload FAILED — keeping previous workerd alive. Error: …
229
329
  ```
230
330
 
231
- End-to-end coverage in `tests/workerd-e2e.test.ts` (`HMR rebundles + swaps without dropping requests`).
331
+ End-to-end coverage in `tests/workerd-e2e.test.ts`
332
+ (`HMR rebundles + swaps without dropping requests`).
232
333
 
233
334
  ### Memory enforcement & crash recovery
234
335
 
235
- **The honest version up front**: workerd's open-source binary does **not** support per-isolate memory limits in its capnp schema — that's a Cloudflare-platform feature, not something the OSS runtime exposes. So 1tube cannot kill an individual misbehaving function; it can only recycle the whole workerd process. Two mechanisms keep things stable:
336
+ **The honest version up front**: workerd's open-source binary does **not**
337
+ support per-isolate memory limits in its capnp schema — that's a
338
+ Cloudflare-platform feature, not something the OSS runtime exposes. So 1tube
339
+ cannot kill an individual misbehaving function; it can only recycle the whole
340
+ workerd process. Two mechanisms keep things stable:
236
341
 
237
- 1. **Memory watchdog** — periodically reads the workerd PID's resident set size (RSS) and recycles the entire process when it stays over budget for `N` consecutive samples.
238
- 2. **Crash auto-recovery** — if workerd dies unexpectedly (OOM, SIGKILL, segfault), the gateway re-bundles and respawns it automatically.
342
+ 1. **Memory watchdog** — periodically reads the workerd PID's resident set size
343
+ (RSS) and recycles the entire process when it stays over budget for `N`
344
+ consecutive samples.
345
+ 2. **Crash auto-recovery** — if workerd dies unexpectedly (OOM, SIGKILL,
346
+ segfault), the gateway re-bundles and respawns it automatically.
239
347
 
240
- Both reuse the M4 dual-process swap, so recovery is non-disruptive: in-flight requests against the old workerd complete on their existing socket, the gateway atomically points new requests at the new process, and only then is the dead/over-budget process torn down.
348
+ Both reuse the M4 dual-process swap, so recovery is non-disruptive: in-flight
349
+ requests against the old workerd complete on their existing socket, the gateway
350
+ atomically points new requests at the new process, and only then is the
351
+ dead/over-budget process torn down.
241
352
 
242
353
  #### Watchdog
243
354
 
244
- Opt-in via `1TUBE_WORKERD_MAX_RSS_MB` (an absolute cap in MB). When unset, the gateway derives a recommended budget from `sum(manifest.memoryMB) × 1.5 + 64MB` and uses that automatically. If no manifest declares `memoryMB` either, the watchdog stays off — no surprises for existing deployments.
355
+ Opt-in via `1TUBE_WORKERD_MAX_RSS_MB` (an absolute cap in MB). When unset, the
356
+ gateway derives a recommended budget from `sum(manifest.memoryMB) × 1.5 + 64MB`
357
+ and uses that automatically. If no manifest declares `memoryMB` either, the
358
+ watchdog stays off — no surprises for existing deployments.
245
359
 
246
- | Env var | Default | Notes |
247
- |---|---|---|
248
- | `1TUBE_WORKERD_MAX_RSS_MB` | unset | Hard RSS cap in MB. |
249
- | `1TUBE_WORKERD_RSS_INTERVAL_MS` | `5000` | Poll interval. |
360
+ | Env var | Default | Notes |
361
+ | ------------------------------- | ------- | ------------------- |
362
+ | `1TUBE_WORKERD_MAX_RSS_MB` | unset | Hard RSS cap in MB. |
363
+ | `1TUBE_WORKERD_RSS_INTERVAL_MS` | `5000` | Poll interval. |
250
364
 
251
- Hysteresis is fixed at **3 consecutive over-budget samples** before tripping (a single GC pause spike won't recycle), and a **10-second cooldown** runs after each recycle to let the new process settle. Watchdog stats (last RSS, configured budget, total recycles, current generation) are surfaced on the authenticated `/health` endpoint under a `workerd` block:
365
+ Hysteresis is fixed at **3 consecutive over-budget samples** before tripping (a
366
+ single GC pause spike won't recycle), and a **10-second cooldown** runs after
367
+ each recycle to let the new process settle. Watchdog stats (last RSS, configured
368
+ budget, total recycles, current generation) are surfaced on the authenticated
369
+ `/health` endpoint under a `workerd` block:
252
370
 
253
371
  ```json
254
372
  {
@@ -263,11 +381,16 @@ Hysteresis is fixed at **3 consecutive over-budget samples** before tripping (a
263
381
  }
264
382
  ```
265
383
 
266
- RSS reading is cross-platform: Linux reads `/proc/{pid}/status`, macOS shells out to `ps`, Windows to `tasklist`. The numbers approximate (working set vs resident vs incl. shared pages differ by platform) — treat the budget as a rough cap, not a precise quota.
384
+ RSS reading is cross-platform: Linux reads `/proc/{pid}/status`, macOS shells
385
+ out to `ps`, Windows to `tasklist`. The numbers approximate (working set vs
386
+ resident vs incl. shared pages differ by platform) — treat the budget as a rough
387
+ cap, not a precise quota.
267
388
 
268
389
  #### Crash recovery
269
390
 
270
- `WorkerdProcess.onExit` is wired to trigger an automatic recycle. A bounded retry budget — **5 crashes in 30 seconds** — protects against hot-looping on a fundamentally broken bundle:
391
+ `WorkerdProcess.onExit` is wired to trigger an automatic recycle. A bounded
392
+ retry budget — **5 crashes in 30 seconds** — protects against hot-looping on a
393
+ fundamentally broken bundle:
271
394
 
272
395
  ```
273
396
  [1tube] workerd crashed (gen=2, code=137, crashes=1/5 in 30s) — auto-recycling...
@@ -276,11 +399,17 @@ RSS reading is cross-platform: Linux reads `/proc/{pid}/status`, macOS shells ou
276
399
  [1tube] workerd crashed (gen=6, code=137, crashes=6/5 in 30s) — GIVING UP, manual restart required.
277
400
  ```
278
401
 
279
- After the budget is exhausted the gateway returns `502 workerd backend error` for every request until the operator restarts it (or, in dev, an HMR save which clears the counter and resumes recovery). End-to-end coverage in `tests/workerd-e2e.test.ts` (`gateway auto-recovers when workerd is killed externally`).
402
+ After the budget is exhausted the gateway returns `502 workerd backend error`
403
+ for every request until the operator restarts it (or, in dev, an HMR save which
404
+ clears the counter and resumes recovery). End-to-end coverage in
405
+ `tests/workerd-e2e.test.ts`
406
+ (`gateway auto-recovers when workerd is killed externally`).
280
407
 
281
408
  ### Observability on workerd
282
409
 
283
- Both `/health` (JSON) and `/metrics` (Prometheus exposition) report workerd-specific state when the backend is active. Both endpoints require `Authorization: Bearer $INTERNAL_KEY`.
410
+ Both `/health` (JSON) and `/metrics` (Prometheus exposition) report
411
+ workerd-specific state when the backend is active. Both endpoints require
412
+ `Authorization: Bearer $INTERNAL_KEY`.
284
413
 
285
414
  `/health` adds a `workerd` block:
286
415
 
@@ -298,56 +427,82 @@ Both `/health` (JSON) and `/metrics` (Prometheus exposition) report workerd-spec
298
427
  }
299
428
  ```
300
429
 
301
- `/metrics` exposes the same data as scrapeable gauges, plus per-function circuit-breaker view that mirrors `supervisor.allStats()`:
302
-
303
- | Gauge | Meaning |
304
- |---|---|
305
- | `onetube_workerd_up` | `1` if workerd is currently running, `0` between processes (mid-recycle, post-crash gap). |
306
- | `onetube_workerd_pid` | PID of the live workerd, `0` when not running. |
307
- | `onetube_workerd_generation` | Increments on every reload (HMR / watchdog / crash recovery). |
308
- | `onetube_workerd_recycles_total` | Number of memory-watchdog-triggered recycles. |
309
- | `onetube_workerd_rss_bytes` | Most recent RSS sample. |
310
- | `onetube_workerd_budget_bytes` | RSS budget enforced by the watchdog (when set). |
311
- | `onetube_workerd_last_reload_duration_ms` | Wall-clock duration of the most recent successful reload. |
312
- | `onetube_workerd_bundle_bytes{function="..."}` | esbuild output size per function. |
313
- | `onetube_function_breaker_open{function="..."}` | `1` while the supervisor's breaker is open for this function. |
314
- | `onetube_function_error_rate{function="..."}` | Rolling error rate inside the supervisor's window (0..1). |
315
- | `onetube_function_recycle_recommended{function="..."}` | `1` once `recycle.maxRequests` has been reached. |
316
-
317
- The breaker gauges work on the Deno backend too — the supervisor is shared between paths.
430
+ `/metrics` exposes the same data as scrapeable gauges, plus per-function
431
+ circuit-breaker view that mirrors `supervisor.allStats()`:
432
+
433
+ | Gauge | Meaning |
434
+ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
435
+ | `onetube_workerd_up` | `1` if workerd is currently running, `0` between processes (mid-recycle, post-crash gap). |
436
+ | `onetube_workerd_pid` | PID of the live workerd, `0` when not running. |
437
+ | `onetube_workerd_generation` | Increments on every reload (HMR / watchdog / crash recovery). |
438
+ | `onetube_workerd_recycles_total` | Number of memory-watchdog-triggered recycles. |
439
+ | `onetube_workerd_rss_bytes` | Most recent RSS sample. |
440
+ | `onetube_workerd_budget_bytes` | RSS budget enforced by the watchdog (when set). |
441
+ | `onetube_workerd_last_reload_duration_ms` | Wall-clock duration of the most recent successful reload. |
442
+ | `onetube_workerd_bundle_bytes{function="..."}` | esbuild output size per function. |
443
+ | `onetube_function_breaker_open{function="..."}` | `1` while the supervisor's breaker is open for this function. |
444
+ | `onetube_function_error_rate{function="..."}` | Rolling error rate inside the supervisor's window (0..1). |
445
+ | `onetube_function_recycle_recommended{function="..."}` | `1` once `recycle.maxRequests` has been reached. |
446
+
447
+ The breaker gauges work on the Deno backend too — the supervisor is shared
448
+ between paths.
318
449
 
319
450
  ### V8 inspector / debugger access
320
451
 
321
- Pass `--inspector` (or `--inspector-addr=host:port`, `1TUBE_INSPECTOR=1`) to launch workerd with `--inspector-addr=...`. Default address is `127.0.0.1:9229`:
452
+ Pass `--inspector` (or `--inspector-addr=host:port`, `1TUBE_INSPECTOR=1`) to
453
+ launch workerd with `--inspector-addr=...`. Default address is `127.0.0.1:9229`:
322
454
 
323
455
  ```bash
324
456
  deno run --allow-all src/server.ts --backend workerd --inspector
325
457
  # → [1tube] workerd V8 inspector enabled at 127.0.0.1:9229 (open chrome://inspect or attach via DevTools)
326
458
  ```
327
459
 
328
- Then open `chrome://inspect` in a Chromium browser; the workerd target appears under "Remote Target" once a function is invoked. Set breakpoints, step through bundled code, inspect locals — everything you'd expect from V8.
460
+ Then open `chrome://inspect` in a Chromium browser; the workerd target appears
461
+ under "Remote Target" once a function is invoked. Set breakpoints, step through
462
+ bundled code, inspect locals — everything you'd expect from V8.
329
463
 
330
464
  Caveats:
331
465
 
332
- - **Unauthenticated debug port.** `--inspector` opens an unauthenticated V8 protocol port. The gateway warns loudly when the bind address is non-loopback. Don't enable in production.
333
- - **HMR port shift.** When `--hmr` is also enabled, every reload spawns a *new* workerd on the next port slot. The inspector port shifts by one (`9229` → `9230` → `9229` → …) to avoid colliding with the still-listening predecessor. The boot log calls out the actual port for each generation.
334
- - **Workerd-only.** The flag is a no-op on `--backend deno`; debug Deno-side functions with the standard `--inspect` Deno flag instead.
466
+ - **Unauthenticated debug port.** `--inspector` opens an unauthenticated V8
467
+ protocol port. The gateway warns loudly when the bind address is non-loopback.
468
+ Don't enable in production.
469
+ - **HMR port shift.** When `--hmr` is also enabled, every reload spawns a _new_
470
+ workerd on the next port slot. The inspector port shifts by one (`9229` →
471
+ `9230` → `9229` → …) to avoid colliding with the still-listening predecessor.
472
+ The boot log calls out the actual port for each generation.
473
+ - **Workerd-only.** The flag is a no-op on `--backend deno`; debug Deno-side
474
+ functions with the standard `--inspect` Deno flag instead.
335
475
 
336
476
  ### Graceful shutdown
337
477
 
338
- On `SIGINT` / `SIGTERM` the gateway runs a three-phase shutdown bounded by `1TUBE_SHUTDOWN_GRACE_MS` (default `10000`):
478
+ On `SIGINT` / `SIGTERM` the gateway runs a three-phase shutdown bounded by
479
+ `1TUBE_SHUTDOWN_GRACE_MS` (default `10000`):
339
480
 
340
- 1. **Stop watchers** — memory watchdog, hot reloader. Prevents a poll racing the shutdown from triggering a doomed recycle.
341
- 2. **Drain the listener** — `Deno.serve`'s abort signal stops accepting new TCP connections. Already-accepted requests keep running against the still-live workerd until they complete or grace expires.
342
- 3. **Stop workerd** — only after the drain has finished (or grace expired). Workerd's own SIGTERM-then-SIGKILL ladder gets the larger of `(remaining_grace, 1000ms)` so its log pumps can flush a final line.
481
+ 1. **Stop watchers** — memory watchdog, hot reloader. Prevents a poll racing the
482
+ shutdown from triggering a doomed recycle.
483
+ 2. **Drain the listener** — `Deno.serve`'s abort signal stops accepting new TCP
484
+ connections. Already-accepted requests keep running against the still-live
485
+ workerd until they complete or grace expires.
486
+ 3. **Stop workerd** — only after the drain has finished (or grace expired).
487
+ Workerd's own SIGTERM-then-SIGKILL ladder gets the larger of
488
+ `(remaining_grace, 1000ms)` so its log pumps can flush a final line.
343
489
 
344
- Previous releases fired-and-forgot `workerd.stop()` in parallel with the drain, which killed workerd while requests were still forwarding through it (502s on the way out). The current ordering ensures in-flight requests keep their original socket until they complete naturally — same property HMR's dual-process swap relies on.
490
+ Previous releases fired-and-forgot `workerd.stop()` in parallel with the drain,
491
+ which killed workerd while requests were still forwarding through it (502s on
492
+ the way out). The current ordering ensures in-flight requests keep their
493
+ original socket until they complete naturally — same property HMR's dual-process
494
+ swap relies on.
345
495
 
346
- If grace is too tight (rare), bump `1TUBE_SHUTDOWN_GRACE_MS=30000`. The boot log records the actual drain + workerd-stop durations so you can size it from real numbers.
496
+ If grace is too tight (rare), bump `1TUBE_SHUTDOWN_GRACE_MS=30000`. The boot log
497
+ records the actual drain + workerd-stop durations so you can size it from real
498
+ numbers.
347
499
 
348
500
  ### Prebuilt artifacts (`1tube build` + `--prebuilt`)
349
501
 
350
- For prod deploys you usually don't want esbuild on the box. `1tube build` produces a sealed artifact directory that `1tube serve --prebuilt` can boot with **zero bundling on the critical path** — just load, sha-256-verify, hand to workerd.
502
+ For prod deploys you usually don't want esbuild on the box. `1tube build`
503
+ produces a sealed artifact directory that `1tube serve --prebuilt` can boot with
504
+ **zero bundling on the critical path** — just load, sha-256-verify, hand to
505
+ workerd.
351
506
 
352
507
  ```bash
353
508
  # Build in CI (or locally)
@@ -360,46 +515,67 @@ deno run --allow-all src/cli.ts --backend workerd --prebuilt ./dist
360
515
 
361
516
  Build flags worth knowing:
362
517
 
363
- | Flag | Default | Purpose |
364
- |---|---|---|
365
- | `--functions <path>` | `./supabase/functions` | Source dir to bundle. |
366
- | `--out <path>` | `./dist` | Output artifact directory. |
367
- | `--only A,B,C` | all | Build a subset (smoke tests, partial deploys). |
368
- | `--sourcemap MODE` | `linked` | `none` / `linked` / `inline`. |
369
- | `--minify` | off | Minify output. |
370
- | `--compat-date YYYY-MM-DD` | (workerd's default) | Bake a workerd compatibility date into the manifest. |
371
- | `--compat-flag FLAG` | `nodejs_compat` | Repeatable; appends a workerd compatibility flag. |
372
- | `--workerd-env A,B,C` | empty | Bake env-var allowlist into the manifest. Values are still read from process env at serve time — only names are persisted. |
518
+ | Flag | Default | Purpose |
519
+ | -------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- |
520
+ | `--functions <path>` | `./supabase/functions` | Source dir to bundle. |
521
+ | `--out <path>` | `./dist` | Output artifact directory. |
522
+ | `--only A,B,C` | all | Build a subset (smoke tests, partial deploys). |
523
+ | `--sourcemap MODE` | `linked` | `none` / `linked` / `inline`. |
524
+ | `--minify` | off | Minify output. |
525
+ | `--compat-date YYYY-MM-DD` | (workerd's default) | Bake a workerd compatibility date into the manifest. |
526
+ | `--compat-flag FLAG` | `nodejs_compat` | Repeatable; appends a workerd compatibility flag. |
527
+ | `--workerd-env A,B,C` | empty | Bake env-var allowlist into the manifest. Values are still read from process env at serve time — only names are persisted. |
373
528
 
374
529
  `dist/manifest.json` carries:
375
530
 
376
- - Schema version (`schema: 1`) — serve refuses newer schemas instead of mis-parsing.
531
+ - Schema version (`schema: 1`) — serve refuses newer schemas instead of
532
+ mis-parsing.
377
533
  - 1tube version + ISO build timestamp (diagnostic).
378
- - Per-function `bundleFile` + `bundleBytes` + `bundleSha256` (lower-case hex). The serve loader recomputes the digest at boot and **fails fast** if a bundle has been modified or corrupted.
379
- - Per-function `1tube.json` parsed in-line, so a prebuilt deploy doesn't need the source tree at all.
534
+ - Per-function `bundleFile` + `bundleBytes` + `bundleSha256` (lower-case hex).
535
+ The serve loader recomputes the digest at boot and **fails fast** if a bundle
536
+ has been modified or corrupted.
537
+ - Per-function `1tube.json` parsed in-line, so a prebuilt deploy doesn't need
538
+ the source tree at all.
380
539
 
381
540
  Prebuilt-mode behaviour at serve time:
382
541
 
383
- - `--functions` is ignored (warned about). `1TUBE_PREBUILT=path` works as an env var equivalent.
384
- - `--prebuilt` implies `--backend workerd`; coerced with a warning if you forget.
385
- - `--hmr` is rejected with a one-line warning — sealed artifacts can't reload. Re-run `1tube build` and restart.
386
- - `backend.reload()` is rejected. Crash-recovery (re-spawning workerd against the same on-disk bundles) still works — it doesn't need to re-bundle.
387
- - The artifact directory must be **writable at boot** because workerd's generated `config.gen-*.capnp` is written next to the bundles. If your prod filesystem is read-only, `cp -r dist /tmp/dist-rw && --prebuilt /tmp/dist-rw`. (Capnp generation is microseconds; we don't bake it at build time so `--port`, `--host`, and `--workerd-env` overrides keep working at serve.)
542
+ - `--functions` is ignored (warned about). `1TUBE_PREBUILT=path` works as an env
543
+ var equivalent.
544
+ - `--prebuilt` implies `--backend workerd`; coerced with a warning if you
545
+ forget.
546
+ - `--hmr` is rejected with a one-line warning — sealed artifacts can't reload.
547
+ Re-run `1tube build` and restart.
548
+ - `backend.reload()` is rejected. Crash-recovery (re-spawning workerd against
549
+ the same on-disk bundles) still works — it doesn't need to re-bundle.
550
+ - The artifact directory must be **writable at boot** because workerd's
551
+ generated `config.gen-*.capnp` is written next to the bundles. If your prod
552
+ filesystem is read-only, `cp -r dist /tmp/dist-rw && --prebuilt /tmp/dist-rw`.
553
+ (Capnp generation is microseconds; we don't bake it at build time so `--port`,
554
+ `--host`, and `--workerd-env` overrides keep working at serve.)
388
555
 
389
556
  What you gain by separating build and serve:
390
557
 
391
- - Reproducible deploys — the artifact is byte-identical for fixed inputs (esbuild output is deterministic; manifest sorts by function name).
392
- - Tamper-evident — sha-256 verification at boot catches both corruption and silent edits.
558
+ - Reproducible deploys — the artifact is byte-identical for fixed inputs
559
+ (esbuild output is deterministic; manifest sorts by function name).
560
+ - Tamper-evident — sha-256 verification at boot catches both corruption and
561
+ silent edits.
393
562
  - No esbuild / `@deno/esbuild-plugin` / npm fetch on the prod box.
394
563
  - Faster boot — esbuild (the slow part) runs once in CI, not once per restart.
395
564
 
396
565
  ### Cache layout
397
566
 
398
- Bundles, sourcemaps, and the generated `config.gen-{0,1}.capnp` files live in `node_modules/.cache/1tube-workerd/` (or `.1tube-cache/workerd/` if there's no `node_modules/`). The directory is auto-gitignored. Safe to delete; it gets rebuilt on next boot.
567
+ Bundles, sourcemaps, and the generated `config.gen-{0,1}.capnp` files live in
568
+ `node_modules/.cache/1tube-workerd/` (or `.1tube-cache/workerd/` if there's no
569
+ `node_modules/`). The directory is auto-gitignored. Safe to delete; it gets
570
+ rebuilt on next boot.
399
571
 
400
572
  ### Benchmarking
401
573
 
402
- A self-contained bench script under `scripts/bench.ts` boots a gateway with each backend, hammers `/functions/v1/hello` (GET) and `/functions/v1/echo` (POST with a 256-byte body), and reports `RPS / p50 / p95 / p99` per (backend, route) pair. It auto-disables rate limiting via `1TUBE_DISABLE_RATE_LIMIT=1` so per-function manifest caps don't skew the numbers.
574
+ A self-contained bench script under `scripts/bench.ts` boots a gateway with each
575
+ backend, hammers `/functions/v1/hello` (GET) and `/functions/v1/echo` (POST with
576
+ a 256-byte body), and reports `RPS / p50 / p95 / p99` per (backend, route) pair.
577
+ It auto-disables rate limiting via `1TUBE_DISABLE_RATE_LIMIT=1` so per-function
578
+ manifest caps don't skew the numbers.
403
579
 
404
580
  ```bash
405
581
  # Default sweep (5000 reqs at 64 concurrency, both backends, ~30s).
@@ -414,14 +590,17 @@ deno run --allow-all scripts/bench.ts -n 500 -c 16 --warmup 50
414
590
 
415
591
  Order-of-magnitude numbers on a recent laptop, loopback, both backends warm:
416
592
 
417
- | Backend | Route | RPS | p50 | p95 | p99 |
418
- |---------|-------|----:|----:|----:|----:|
419
- | Deno | GET hello | 3180 | 8.8ms | 16.7ms | 26.0ms |
420
- | Deno | POST echo | 1819 | 16.4ms | 27.2ms | 36.3ms |
421
- | Workerd | GET hello | 1377 | 22.6ms | 32.9ms | 38.0ms |
422
- | Workerd | POST echo | 1034 | 29.7ms | 47.2ms | 57.2ms |
593
+ | Backend | Route | RPS | p50 | p95 | p99 |
594
+ | ------- | --------- | ---: | -----: | -----: | -----: |
595
+ | Deno | GET hello | 3180 | 8.8ms | 16.7ms | 26.0ms |
596
+ | Deno | POST echo | 1819 | 16.4ms | 27.2ms | 36.3ms |
597
+ | Workerd | GET hello | 1377 | 22.6ms | 32.9ms | 38.0ms |
598
+ | Workerd | POST echo | 1034 | 29.7ms | 47.2ms | 57.2ms |
423
599
 
424
- Workerd costs roughly 2× the latency of in-process Deno calls because every request takes one extra HTTP hop (gateway → workerd loopback socket). For LLM/streaming workloads where the upstream call dwarfs proxy overhead, that delta is invisible.
600
+ Workerd costs roughly 2× the latency of in-process Deno calls because every
601
+ request takes one extra HTTP hop (gateway → workerd loopback socket). For
602
+ LLM/streaming workloads where the upstream call dwarfs proxy overhead, that
603
+ delta is invisible.
425
604
 
426
605
  ## .NET integration
427
606
 
@@ -453,6 +632,10 @@ services.AddOneTube(options =>
453
632
  app.MapOneTube(port: 3100);
454
633
  ```
455
634
 
456
- The OneTube package ships the gateway TypeScript sources alongside the DLL (under `OneTubeGateway/` in the host's output directory), so you do **not** need a 1tube checkout on the production host — only the `deno` and `workerd` binaries you point at via `DenoBinary` / `WorkerdBinary`.
635
+ The OneTube package ships the gateway TypeScript sources alongside the DLL
636
+ (under `OneTubeGateway/` in the host's output directory), so you do **not** need
637
+ a 1tube checkout on the production host — only the `deno` and `workerd` binaries
638
+ you point at via `DenoBinary` / `WorkerdBinary`.
457
639
 
458
- This spawns the Deno gateway as a managed child process with health monitoring and auto-restart, and forwards `/functions/v1/*` via YARP zero-copy proxying.
640
+ This spawns the Deno gateway as a managed child process with health monitoring
641
+ and auto-restart, and forwards `/functions/v1/*` via YARP zero-copy proxying.