Runtime Compatibility
Juice uses only WinterCG standard APIs at runtime. This page documents exactly which APIs are used, which runtimes support them natively, and what happens when they are not available.
Compatibility Matrix
Every cell is a runtime feature that Juice depends on. Green means native support. Yellow means Juice auto-polyfills. Red means not supported.
| API | Cloudflare Workers | Bun 1.1+ | Deno 1.40+ | Node.js 22+ |
|---|---|---|---|---|
Request / Response | Native | Native | Native | Native |
URL / URLSearchParams | Native | Native | Native | Native |
URLPattern | Native | Native | Native | Native (22+), polyfilled for older |
ReadableStream / TransformStream | Native | Native | Native | Native |
AbortController | Native | Native | Native | Native |
crypto.randomUUID() | Native | Native | Native | Native |
TextEncoder / TextDecoder | Native | Native | Native | Native |
AsyncLocalStorage | Native | Native | Native | Native |
FormData | Native | Native | Native | Native |
URLPattern Polyfill
Juice uses URLPattern for all route matching. On runtimes where it is not natively available (Node.js before version 22, older Bun versions), Juice automatically imports the urlpattern-polyfill package at startup:
// This happens automatically inside Juice's matcher:
if (!('URLPattern' in globalThis)) {
await import('urlpattern-polyfill');
}If the polyfill is not installed and URLPattern is not available, the router throws a clear error at startup with installation instructions. The polyfill is included as a production dependency of @cmj/juice, so you do not need to install it separately.
AsyncLocalStorage: Primary and Fallback
Juice's context system (setContext / getContext) and the cache() function use AsyncLocalStorage (ALS) for request-scoped isolation. ALS is available on all four supported runtimes, but Juice does not assume it exists. Here is the exact detection logic:
// context.ts — runs once at module load
let _als = null;
try {
const { AsyncLocalStorage } = await import('node:async_hooks');
_als = new AsyncLocalStorage();
} catch {
// ALS not available — fall back to WeakMap
}What the fallback means
When ALS is available (the common case), every request gets its own isolated storage. Concurrent requests cannot interfere with each other. The cache()function deduplicates correctly across concurrent requests because each request has its own ALS store.
When ALS is not available, Juice falls back to a WeakMap keyed by the Request object. This still provides per-request isolation: each request has its own context map. However, there is a subtle difference:
- ALS path: context is implicitly available to any function in the async call stack. You can call
getContext(req, key)from deeply nested helper functions without passingreqthrough every layer. - WeakMap path: you must have a reference to the original
Requestobject to read context. In practice, this is not a problem because Juice passesrequestas a prop to every page component.
The cache() function works correctly in both paths. In ALS mode, it stores the deduplication map in the ALS store. In WeakMap mode, it uses the same Request-keyed WeakMap. Either way, two calls to the same cached function within one request return the same promise.
When would ALS be unavailable?
In practice, all four supported runtimes provide ALS. The fallback exists for:
- Edge runtimes that restrict
node:async_hooksimports - Custom runtimes or embedded V8 environments without Node.js compatibility
- Testing environments where the import is mocked or stripped
If you are deploying to Cloudflare Workers, Bun, Deno, or Node.js 18+, ALS is available and the fallback never activates.
What Juice Does NOT Use
Juice's runtime has zero Node.js-specific imports (the ALS import is optional and wrapped in try-catch). Here is what is explicitly excluded:
node:fs— no filesystem access at runtime (all file I/O happens at build time)node:http/node:net— uses standardRequest/Responseinsteadnode:stream— uses webReadableStreamandTransformStreamnode:crypto— uses webcrypto.randomUUID()node:buffer— usesTextEncoder/TextDecodernode:child_process— no subprocess spawningprocess.env— environment variables are runtime-specific (see each environment page)eval()/new Function()— safe for restricted environments like Workers
The build toolchain (Vite plugin, CLI) uses Node.js APIs freely — but none of that code ships to production. The runtime is the only thing that runs on your server.
Tested Runtimes
The test suite runs on Node.js with polyfills via Vitest. Runtime-specific behavior is verified through deployment testing on each target platform. If you encounter a runtime-specific issue, please report it with the runtime name and version.
| Runtime | Minimum Version | Notes |
|---|---|---|
| Cloudflare Workers | 2024-01-01 compat date | Primary target. Requires nodejs_compat flag for ALS. |
| Bun | 1.1+ | Native URLPattern and ALS. |
| Deno | 1.40+ | Native URLPattern and ALS via node:async_hooks compat. |
| Node.js | 22+ | Native URLPattern. Node 18-21 work with the polyfill. |