Multi-Runtime Design
The proxy runs on two runtimes — a native Tokio/Hyper server for container deployments and Cloudflare Workers for edge deployments. The same core logic compiles to both targets through careful abstraction of platform-specific concerns.
Runtime Comparison
| Server Runtime | CF Workers Runtime | |
|---|---|---|
| Platform | Linux/macOS containers | Cloudflare Workers (V8) |
| Target | x86_64 / aarch64 | wasm32-unknown-unknown |
| HTTP client | reqwest | web_sys::fetch |
| Streaming | hyper Incoming / reqwest bytes_stream() | JS ReadableStream passthrough |
| Raw signed HTTP | reqwest | web_sys::fetch |
| Backend support | S3, Azure, GCS | S3, Azure, GCS via cargo features (example ships S3 only) |
| Config loading | TOML file | Env var (JSON or JS object) |
| Threading | Multi-threaded (Send + Sync required) | Single-threaded (!Send types allowed) |
How It Works
MaybeSend / MaybeSync
The core challenge is that Tokio requires Send + Sync for task spawning, while WASM runtimes are single-threaded and use !Send types (like JsValue and ReadableStream).
The solution is conditional trait aliases defined in multistore:
- On native targets:
MaybeSendresolves toSend,MaybeSyncresolves toSync - On
wasm32:MaybeSendandMaybeSyncare blanket traits that every type implements
Traits whose wasm implementations use !Send types are bounded by MaybeSend + MaybeSync: ProxyBackend, RouteHandler, Middleware, HttpExchange, and CredentialExchange. The registry traits BucketRegistry and CredentialRegistry use the same conditional bounds (Clone + MaybeSend + MaybeSync + 'static), so they resolve to Send + Sync on native targets and relax to !Send on wasm.
The Signer trait from object_store requires real Send + Sync, which works because UnsignedUrlSigner only holds String fields, and object_store's built-in store types are Send + Sync.
RPITIT Async Methods
Core traits use return-position impl Trait in trait (RPITIT) for async methods instead of #[async_trait]:
pub trait ProxyBackend: Clone + MaybeSend + MaybeSync + 'static {
fn send_raw(
&self,
method: http::Method,
url: String,
headers: HeaderMap,
body: Bytes,
) -> impl Future<Output = Result<RawResponse, ProxyError>> + MaybeSend;
}This avoids #[async_trait]'s Box<dyn Future + Send> requirement, which won't compile on WASM targets.
Server Runtime
The server runtime (examples/server/) uses Tokio and Hyper:
- Forward actions: reqwest sends the presigned URL request. For GET, the response body is streamed via
bytes_stream(). For PUT, the client's hyperIncomingbody is streamed directly to reqwest. ServerBackend: Uses reqwest forsend_raw()(multipart, batch delete, and LIST) and builds presigned URLs offline via theobject_storesigner.
Cloudflare Workers Runtime
The CF Workers runtime (examples/cf-workers/) uses worker-rs, wasm-bindgen, and web_sys:
- Forward actions: JS
ReadableStreambodies pass through without touching Rust. The Workers Fetch API handles streaming natively. WorkerBackend: Usesweb_sys::fetchforsend_raw()(multipart, batch delete, and LIST) and builds presigned URLs offline via theobject_storesigner. Noobject_storeHTTP client is used, so LIST responses never touchobject_store::Path.
WASM Limitations
- Backend features: The
multistore-cf-workerscrate supports S3, Azure, and GCS — theStoreBuilder::AzureandStoreBuilder::Gcsbranches work on the Workers runtime, gated behind theazureandgcpcargo features. Those features are off by default, so the shipped example (examples/cf-workers) enables S3 only Instant::now()panics on WASM: TheUnsignedUrlSigneravoids theInstanceCredentialProvider→TokenCache→Instant::now()code path that panics on WASM- No
default-members: The CF Workers crate is excluded from the workspace default members. Always build with:bashcargo check -p multistore-cf-workers --target wasm32-unknown-unknown