Scramjet uses transport libraries to proxy HTTP requests and WebSocket connections. The primary transport system is bare-mux , which provides a pluggable architecture for different backend protocols.
Transport architecture
Scramjet’s transport layer consists of:
BareClient - Main client interface from @mercuryworkshop/bare-mux
Transport backends - Pluggable implementations (bare-server, epoxy, wisp, libcurl)
BareMuxConnection - Worker-to-client communication bridge
┌─────────────────┐
│ Scramjet Client │
└────────┬─────────┘
│ fetch()/WebSocket()
▼
┌─────────────────┐
│ BareClient │
└────────┬─────────┘
│ bare-mux protocol
▼
┌─────────────────┐
│ Transport │ ← bare-server, epoxy, wisp, etc.
└────────┬─────────┘
│
▼
Target Server
bare-mux integration
bare-mux is Scramjet’s default transport multiplexer, allowing runtime transport switching.
Installation
{
"dependencies" : {
"@mercuryworkshop/bare-mux" : "^2.1.7"
}
}
Service worker setup
The service worker creates a BareClient instance:
import BareClient from "@mercuryworkshop/bare-mux" ;
export class ScramjetServiceWorker extends EventTarget {
client : BareClient ;
constructor () {
super ();
this . client = new BareClient ();
}
async fetch ( event : FetchEvent ) {
// Use BareClient to proxy the request
const response = await this . client . fetch ( url , {
method: request . method ,
headers: requestHeaders ,
body: request . body ,
credentials: "omit" ,
redirect: "manual" ,
});
return response ;
}
}
BareClient provides a fetch()-like API that automatically routes requests through the configured transport.
Window/client setup
In the window context, Scramjet creates a separate BareClient:
import BareClient from "@mercuryworkshop/bare-mux" ;
export class ScramjetClient {
bare : BareClient ;
constructor ( public global : typeof globalThis ) {
if ( iswindow ) {
this . bare = new BareClient ();
} else {
// Workers receive BareMuxConnection via postMessage
this . bare = new BareClient (
new Promise (( resolve ) => {
addEventListener ( "message" , ({ data }) => {
if ( data . $scramjet$type === "baremuxinit" ) {
resolve ( data . port );
}
});
})
);
}
}
}
Worker transport bridge
Workers need a MessagePort to communicate with the parent’s BareClient:
import { BareMuxConnection } from "@mercuryworkshop/bare-mux" ;
client . Proxy ( "Worker" , {
construct ( ctx ) {
ctx . args [ 0 ] = rewriteUrl ( ctx . args [ 0 ], client . meta ) + "?dest=worker" ;
const worker = ctx . call ();
// Create BareMux connection
const conn = new BareMuxConnection ();
( async () => {
const port = await conn . getInnerPort ();
// Send port to worker
client . natives . call (
"Worker.prototype.postMessage" ,
worker ,
{
$scramjet$type: "baremuxinit" ,
port ,
},
[ port ] // Transfer port
);
})();
},
});
Why use BareMuxConnection?
Workers cannot directly access the transport because:
Isolation : Workers run in separate contexts without DOM access
Shared state : Multiple workers need to share the same transport configuration
Performance : Centralized transport reduces overhead
BareMuxConnection creates a MessageChannel that bridges the worker to the main context’s BareClient.
Transport backends
bare-server (default)
The standard Bare server protocol:
import BareClient from "@mercuryworkshop/bare-mux" ;
import { createBareServer } from "@nebula-services/bare-server-node" ;
// Server-side setup
const bareServer = createBareServer ( "/bare/" );
app . use (( req , res , next ) => {
if ( bareServer . shouldRoute ( req )) {
bareServer . routeRequest ( req , res );
} else {
next ();
}
});
Client configuration:
// Set transport to bare-server
await BareClient . SetTransport ( "/bare/" , "bare" );
epoxy-transport
Epoxy uses WebTransport for improved performance:
{
"devDependencies" : {
"@mercuryworkshop/epoxy-transport" : "^2.1.28"
}
}
Setup:
import { EpoxyClient } from "@mercuryworkshop/epoxy-transport" ;
// Initialize epoxy transport
await BareClient . SetTransport ( "https://epoxy.example.com/" , "epoxy" );
// Use BareClient as normal
const response = await bareClient . fetch ( url );
Epoxy requires browser support for WebTransport API (Chromium-based browsers). Fallback to bare-server for compatibility.
wisp protocol
Wisp provides WebSocket-based proxying:
{
"devDependencies" : {
"@mercuryworkshop/wisp-js" : "^0.3.3"
}
}
Configuration:
import { WispClient } from "@mercuryworkshop/wisp-js" ;
// Set wisp transport
await BareClient . SetTransport ( "wss://wisp.example.com/" , "wisp" );
Wisp is particularly useful for:
Environments where HTTP proxying is restricted
Bypassing certain network filters
Multiplexing connections over a single WebSocket
libcurl-transport
Native performance using libcurl:
{
"devDependencies" : {
"@mercuryworkshop/libcurl-transport" : "^1.5.0"
}
}
libcurl-transport requires native bindings and is primarily used in Electron or Node.js environments.
Setting the transport
Static configuration
Set transport during Scramjet initialization:
const { ScramjetController } = $scramjetLoadController ();
const { BareClient } = await import ( "@mercuryworkshop/bare-mux" );
const controller = new ScramjetController ({
prefix: "/service/" ,
files: {
wasm: "/scramjet.wasm.wasm" ,
all: "/scramjet.all.js" ,
sync: "/scramjet.sync.js" ,
},
});
// Set default transport
await BareClient . SetTransport ( "/bare/" , "bare" );
controller . init ( "/sw.js" );
Dynamic transport switching
Users can switch transports at runtime:
// Switch to epoxy
await BareClient . SetTransport ( "https://epoxy.example.com/" , "epoxy" );
// Switch to wisp
await BareClient . SetTransport ( "wss://wisp.example.com/" , "wisp" );
// Switch back to bare-server
await BareClient . SetTransport ( "/bare/" , "bare" );
Transport switching is seamless - existing connections continue using the old transport while new requests use the updated configuration.
WebSocket proxying
Scramjet uses bare-mux’s BareWebSocket class for WebSocket connections:
import { type BareWebSocket } from "@mercuryworkshop/bare-mux" ;
client . Proxy ( "WebSocket" , {
construct ( ctx ) {
const url = new URL ( ctx . args [ 0 ], client . meta . base );
const protocols = ctx . args [ 1 ];
// Create bare WebSocket
const ws = new client . bare . WebSocket ( url . href , protocols ) as BareWebSocket ;
// Proxy properties
Object . defineProperty ( ws , "url" , {
get : () => url . href ,
enumerable: true ,
});
ctx . return ( ws );
},
});
WebSocket protocol handling
Different transports handle WebSockets differently:
bare-server : Upgrades HTTP connection to WebSocket
wisp : Multiplexes over existing wisp WebSocket connection
epoxy : Uses WebTransport streams
Request/response flow
Standard fetch request
// 1. User code
fetch ( "https://example.com/api" );
// 2. Scramjet intercepts (client hooks)
fetch ( rewriteUrl ( "https://example.com/api" ));
// → fetch("/service/aHR0cHM6Ly9leGFtcGxlLmNvbS9hcGk=");
// 3. Service worker routes to handleFetch
const url = unrewriteUrl ( request . url );
// → "https://example.com/api"
// 4. BareClient proxies request
const response = await bareClient . fetch ( url , {
method: "GET" ,
headers: rewrittenHeaders ,
});
// 5. Transport executes request
// (bare-server makes actual HTTP request)
// 6. Response flows back through rewriters
const body = await rewriteBody ( response , meta , destination );
return new Response ( body , { headers: rewrittenHeaders });
BareClient handles header transformations:
import { rewriteHeaders } from "@rewriters/headers" ;
import type { BareHeaders } from "@mercuryworkshop/bare-mux" ;
export async function rewriteHeaders (
rawHeaders : BareHeaders ,
meta : URLMeta ,
client : BareClient
) {
const headers = {};
// Lowercase all headers
for ( const key in rawHeaders ) {
headers [ key . toLowerCase ()] = rawHeaders [ key ];
}
// Remove security headers
const SEC_HEADERS = new Set ([
"content-security-policy" ,
"x-frame-options" ,
"cross-origin-opener-policy" ,
// ...
]);
for ( const header of SEC_HEADERS ) {
delete headers [ header ];
}
// Rewrite URL headers
if ( headers [ "location" ]) {
headers [ "location" ] = rewriteUrl ( headers [ "location" ], meta );
}
return headers ;
}
Response types
BareClient returns a BareResponseFetch object:
import type { BareResponseFetch } from "@mercuryworkshop/bare-mux" ;
const response : BareResponseFetch = await bareClient . fetch ( url );
// Standard Response properties
response . status ; // 200
response . statusText ; // "OK"
response . headers ; // Headers object
response . body ; // ReadableStream
// Additional bare-mux properties
response . finalURL ; // Final URL after redirects
response . rawHeaders ; // Raw headers object
Debugging transports
Check current transport
import BareClient from "@mercuryworkshop/bare-mux" ;
const transport = await BareClient . GetTransport ();
console . log ( "Current transport:" , transport );
// { name: "bare", url: "/bare/" }
Monitor requests
client . addEventListener ( "request" , ( event ) => {
console . log ( "Request:" , event . url );
console . log ( "Headers:" , event . requestHeaders );
});
client . addEventListener ( "handleResponse" , ( event ) => {
console . log ( "Response:" , event . url );
console . log ( "Status:" , event . status );
console . log ( "Headers:" , event . responseHeaders );
});
Error handling
try {
const response = await bareClient . fetch ( url );
} catch ( err ) {
if ( err . message . includes ( "transport" )) {
console . error ( "Transport error - check backend connectivity" );
}
if ( err . message . includes ( "CORS" )) {
console . error ( "Transport backend has CORS issues" );
}
throw err ;
}
Advanced patterns
Custom transport implementation
You can implement custom transports by following the bare-mux protocol:
class CustomTransport {
async fetch ( url : URL , init : RequestInit ) {
// Custom logic here
const response = await myCustomFetch ( url , init );
return {
status: response . status ,
statusText: response . statusText ,
headers: response . headers ,
body: response . body ,
finalURL: response . url ,
rawHeaders: response . rawHeaders ,
};
}
createWebSocket ( url : URL , protocols ?: string []) {
return new MyCustomWebSocket ( url , protocols );
}
}
// Register custom transport
await BareClient . SetTransport ( "/custom/" , "custom" , CustomTransport );
Transport fallback chain
const transports = [
{ url: "/epoxy/" , type: "epoxy" },
{ url: "/wisp/" , type: "wisp" },
{ url: "/bare/" , type: "bare" },
];
for ( const { url , type } of transports ) {
try {
await BareClient . SetTransport ( url , type );
// Test transport
await bareClient . fetch ( "https://example.com/" );
console . log ( `Using ${ type } transport` );
break ;
} catch ( err ) {
console . warn ( ` ${ type } failed, trying next...` );
}
}
Conditional transport selection
function selectTransport () {
// Use epoxy for Chrome
if ( navigator . userAgent . includes ( "Chrome" )) {
return { url: "/epoxy/" , type: "epoxy" };
}
// Use wisp for restricted networks
if ( isRestrictedNetwork ()) {
return { url: "wss://wisp.example.com/" , type: "wisp" };
}
// Default to bare-server
return { url: "/bare/" , type: "bare" };
}
const transport = selectTransport ();
await BareClient . SetTransport ( transport . url , transport . type );
Common issues
Transport not initialized
// Error: BareClient transport not set
// Solution: Set transport before using BareClient
await BareClient . SetTransport ( "/bare/" , "bare" );
Worker transport errors
// Error: Worker cannot access BareClient
// Solution: Ensure BareMuxConnection is properly initialized
const conn = new BareMuxConnection ();
const port = await conn . getInnerPort ();
worker . postMessage ({ $scramjet$type: "baremuxinit" , port }, [ port ]);
CORS errors
// Error: CORS policy blocked request
// Solution: Ensure transport backend allows requests from your origin
// In bare-server:
app . use (( req , res , next ) => {
res . header ( "Access-Control-Allow-Origin" , "*" );
next ();
});