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 |
| Object store connector | Default (reqwest-based) | FetchConnector |
| Backend support | S3, Azure, GCS | 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
Only traits whose wasm implementations use !Send types need MaybeSend + MaybeSync: ProxyBackend, RouteHandler, Middleware, HttpExchange, and CredentialExchange. Other traits like BucketRegistry and CredentialRegistry use plain Send + Sync.
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: Createsobject_storeinstances with the default HTTP connector (reqwest) and uses reqwest forsend_raw()(multipart).
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: Createsobject_storeinstances withFetchConnectorinjected for HTTP transport.
FetchConnector
FetchConnector bridges object_store's HttpConnector trait to the Workers Fetch API. Since worker::Fetch::send() is !Send, each call is wrapped in spawn_local with a oneshot channel to bridge back to the Send context that object_store expects.
This is only used for LIST operations — presigned URL operations bypass object_store entirely.
WASM Limitations
- S3 only: Azure and GCS builders are gated behind cargo features that are disabled for the Workers runtime
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