diff --git a/.claude/launch.json b/.claude/launch.json
new file mode 100644
index 0000000..2d3c45e
--- /dev/null
+++ b/.claude/launch.json
@@ -0,0 +1,12 @@
+{
+ "version": "0.0.1",
+ "configurations": [
+ {
+ "name": "docs",
+ "runtimeExecutable": "pnpm",
+ "runtimeArgs": ["docs:dev"],
+ "port": 5173,
+ "cwd": "docs"
+ }
+ ]
+}
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
new file mode 100644
index 0000000..20c2462
--- /dev/null
+++ b/.github/workflows/docs.yaml
@@ -0,0 +1,59 @@
+name: Deploy Docs
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - "docs/**"
+ - ".github/workflows/docs.yaml"
+
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+ cache-dependency-path: docs/pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+ working-directory: docs
+
+ - name: Build docs
+ run: pnpm docs:build
+ working-directory: docs
+
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs/.vitepress/dist
+
+ deploy:
+ name: Deploy
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/deploy-pages@v4
+ id: deployment
diff --git a/.gitignore b/.gitignore
index a55e53a..abaabb6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@ scripts/task_definition.json
target
.wrangler
.env*
+node_modules
+docs/.vitepress/cache
+docs/.vitepress/dist
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
new file mode 100644
index 0000000..bd1c24a
--- /dev/null
+++ b/docs/.vitepress/config.ts
@@ -0,0 +1,166 @@
+import { defineConfig } from "vitepress";
+import { withMermaid } from "vitepress-plugin-mermaid";
+
+const adminSidebar = [
+ {
+ text: "Getting Started",
+ items: [
+ { text: "Quick Start", link: "/getting-started/" },
+ {
+ text: "Local Development",
+ link: "/getting-started/local-development",
+ },
+ ],
+ },
+ {
+ text: "Configuration",
+ items: [
+ { text: "Overview", link: "/configuration/" },
+ { text: "Buckets", link: "/configuration/buckets" },
+ { text: "Roles", link: "/configuration/roles" },
+ { text: "Credentials", link: "/configuration/credentials" },
+ {
+ text: "Providers",
+ collapsed: false,
+ items: [
+ { text: "Overview", link: "/configuration/providers/" },
+ {
+ text: "Static File",
+ link: "/configuration/providers/static-file",
+ },
+ { text: "HTTP API", link: "/configuration/providers/http" },
+ {
+ text: "DynamoDB",
+ link: "/configuration/providers/dynamodb",
+ },
+ {
+ text: "PostgreSQL",
+ link: "/configuration/providers/postgres",
+ },
+ {
+ text: "Caching",
+ link: "/configuration/providers/cached",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ text: "Authentication",
+ items: [
+ { text: "Overview", link: "/auth/" },
+ {
+ text: "Client Auth (OIDC/STS)",
+ link: "/auth/proxy-auth",
+ },
+ {
+ text: "Backend Auth",
+ link: "/auth/backend-auth",
+ },
+ { text: "Sealed Session Tokens", link: "/auth/sealed-tokens" },
+ ],
+ },
+ {
+ text: "Deployment",
+ items: [
+ { text: "Overview", link: "/deployment/" },
+ { text: "Server Runtime", link: "/deployment/server" },
+ {
+ text: "Cloudflare Workers",
+ link: "/deployment/cloudflare-workers",
+ },
+ ],
+ },
+ {
+ text: "Architecture",
+ items: [
+ { text: "Overview", link: "/architecture/" },
+ { text: "Crate Layout", link: "/architecture/crate-layout" },
+ {
+ text: "Request Lifecycle",
+ link: "/architecture/request-lifecycle",
+ },
+ {
+ text: "Multi-Runtime Design",
+ link: "/architecture/multi-runtime",
+ },
+ ],
+ },
+ {
+ text: "Extending",
+ items: [
+ { text: "Overview", link: "/extending/" },
+ { text: "Custom Resolver", link: "/extending/custom-resolver" },
+ { text: "Custom Provider", link: "/extending/custom-provider" },
+ { text: "Custom Backend", link: "/extending/custom-backend" },
+ ],
+ },
+];
+
+export default withMermaid(
+ defineConfig({
+ title: "Source Data Proxy",
+ description: "Multi-runtime S3 gateway proxy in Rust",
+
+ themeConfig: {
+ nav: [
+ { text: "User Guide", link: "/guide/" },
+ { text: "Administration", link: "/getting-started/" },
+ { text: "Reference", link: "/reference/" },
+ ],
+
+ sidebar: {
+ "/guide/": [
+ {
+ text: "User Guide",
+ items: [
+ { text: "Overview", link: "/guide/" },
+ { text: "Endpoints", link: "/guide/endpoints" },
+ { text: "Authentication", link: "/guide/authentication" },
+ { text: "Client Usage", link: "/guide/client-usage" },
+ ],
+ },
+ ],
+
+ "/getting-started/": adminSidebar,
+ "/configuration/": adminSidebar,
+ "/auth/": adminSidebar,
+ "/deployment/": adminSidebar,
+ "/architecture/": adminSidebar,
+ "/extending/": adminSidebar,
+
+ "/reference/": [
+ {
+ text: "Reference",
+ items: [
+ { text: "Overview", link: "/reference/" },
+ {
+ text: "Supported Operations",
+ link: "/reference/operations",
+ },
+ { text: "Error Codes", link: "/reference/errors" },
+ { text: "Config Example", link: "/reference/config-example" },
+ ],
+ },
+ ],
+ },
+
+ socialLinks: [
+ {
+ icon: "github",
+ link: "https://github.com/source-cooperative/data.source.coop",
+ },
+ ],
+
+ search: {
+ provider: "local",
+ },
+
+ footer: {
+ message: "Released under the MIT / Apache-2.0 License.",
+ copyright:
+ 'A Radiant Earth project. Copyright © 2026 Source Cooperative.',
+ },
+ },
+ }),
+);
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
new file mode 100644
index 0000000..617deeb
--- /dev/null
+++ b/docs/.vitepress/theme/index.ts
@@ -0,0 +1,4 @@
+import DefaultTheme from "vitepress/theme";
+import "./style.css";
+
+export default DefaultTheme;
diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css
new file mode 100644
index 0000000..4bd08e0
--- /dev/null
+++ b/docs/.vitepress/theme/style.css
@@ -0,0 +1,220 @@
+/**
+ * Source Cooperative theme
+ *
+ * Matches docs.source.coop visual identity:
+ * - Font: IBM Plex Sans
+ * - Mono: Berkeley Mono (with fallbacks)
+ * - Light: warm off-white #efebea background, dark teal-gray #2c3233 text
+ * - Dark: inverted — #2c3233 background, #efebea text
+ *
+ * Reference: github.com/source-cooperative/docs.source.coop/blob/main/src/css/custom.css
+ */
+
+/* ------------------------------------------------------------------ */
+/* Typography */
+/* ------------------------------------------------------------------ */
+
+@import url("https://fonts.googleapis.com/css2?family=Cascadia+Mono:wght@400;600&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap");
+
+:root {
+ --vp-font-family-base: "IBM Plex Sans", system-ui, -apple-system, sans-serif;
+ --vp-font-family-mono: "IBM Plex Mono", SFMono-Regular, "SF Mono", Menlo,
+ Consolas, "Liberation Mono", monospace;
+ --vp-font-family-heading: "Cascadia Mono", var(--vp-font-family-mono);
+}
+
+/* ------------------------------------------------------------------ */
+/* Light theme colors */
+/* Warm off-white background, dark teal-gray primary */
+/* ------------------------------------------------------------------ */
+
+:root {
+ /* Primary — dark teal-gray (same as docs.source.coop) */
+ --vp-c-brand-1: #2c3233;
+ --vp-c-brand-2: #303738;
+ --vp-c-brand-3: #394142;
+ --vp-c-brand-soft: rgba(44, 50, 51, 0.08);
+
+ /* Page background — warm off-white */
+ --vp-c-bg: #efebea;
+ --vp-c-bg-alt: #e6e1df;
+ --vp-c-bg-elv: #ffffff;
+ --vp-c-bg-soft: #e6e1df;
+
+ /* Text */
+ --vp-c-text-1: #2c3233;
+ --vp-c-text-2: rgba(44, 50, 51, 0.78);
+ --vp-c-text-3: rgba(44, 50, 51, 0.56);
+
+ /* Borders & dividers */
+ --vp-c-divider: rgba(44, 50, 51, 0.12);
+ --vp-c-gutter: rgba(44, 50, 51, 0.06);
+ --vp-c-border: rgba(44, 50, 51, 0.15);
+
+ /* Tip callout — inherit brand */
+ --vp-c-tip-1: var(--vp-c-brand-1);
+ --vp-c-tip-2: var(--vp-c-brand-2);
+ --vp-c-tip-3: var(--vp-c-brand-3);
+ --vp-c-tip-soft: var(--vp-c-brand-soft);
+
+ /* Navbar / sidebar */
+ --vp-nav-bg-color: #ffffff;
+ --vp-sidebar-bg-color: #f7f4f3;
+
+ /* Code blocks */
+ --vp-code-block-bg: #f6f7f8;
+ --vp-code-bg: rgba(44, 50, 51, 0.06);
+}
+
+/* ------------------------------------------------------------------ */
+/* Dark theme colors */
+/* Inverted: teal-gray background, warm off-white primary */
+/* ------------------------------------------------------------------ */
+
+.dark {
+ --vp-c-brand-1: #efebea;
+ --vp-c-brand-2: #dbd1cf;
+ --vp-c-brand-3: #b29e99;
+ --vp-c-brand-soft: rgba(239, 235, 234, 0.1);
+
+ /* Page background */
+ --vp-c-bg: #2c3233;
+ --vp-c-bg-alt: #242a2b;
+ --vp-c-bg-elv: #343b3c;
+ --vp-c-bg-soft: #343b3c;
+
+ /* Text */
+ --vp-c-text-1: #efebea;
+ --vp-c-text-2: rgba(239, 235, 234, 0.72);
+ --vp-c-text-3: rgba(239, 235, 234, 0.48);
+
+ /* Borders & dividers */
+ --vp-c-divider: rgba(239, 235, 234, 0.12);
+ --vp-c-gutter: rgba(239, 235, 234, 0.06);
+ --vp-c-border: rgba(239, 235, 234, 0.15);
+
+ /* Navbar / sidebar */
+ --vp-nav-bg-color: #242a2b;
+ --vp-sidebar-bg-color: #272e2f;
+
+ /* Code blocks */
+ --vp-code-block-bg: rgba(255, 255, 255, 0.06);
+ --vp-code-bg: rgba(255, 255, 255, 0.1);
+}
+
+/* ------------------------------------------------------------------ */
+/* Buttons */
+/* ------------------------------------------------------------------ */
+
+:root {
+ --vp-button-brand-border: transparent;
+ --vp-button-brand-text: #efebea;
+ --vp-button-brand-bg: #2c3233;
+ --vp-button-brand-hover-border: transparent;
+ --vp-button-brand-hover-text: #efebea;
+ --vp-button-brand-hover-bg: #394142;
+ --vp-button-brand-active-border: transparent;
+ --vp-button-brand-active-text: #efebea;
+ --vp-button-brand-active-bg: #1f2324;
+}
+
+.dark {
+ --vp-button-brand-text: #2c3233;
+ --vp-button-brand-bg: #efebea;
+ --vp-button-brand-hover-text: #2c3233;
+ --vp-button-brand-hover-bg: #ffffff;
+ --vp-button-brand-active-text: #2c3233;
+ --vp-button-brand-active-bg: #dbd1cf;
+}
+
+/* ------------------------------------------------------------------ */
+/* Home hero */
+/* ------------------------------------------------------------------ */
+
+:root {
+ --vp-home-hero-name-color: #2c3233;
+ --vp-home-hero-name-background: none;
+ --vp-home-hero-image-background-image: none;
+ --vp-home-hero-image-filter: none;
+}
+
+.dark {
+ --vp-home-hero-name-color: #efebea;
+}
+
+/* ------------------------------------------------------------------ */
+/* Headings — Cascadia Mono for a distinctive technical feel */
+/* ------------------------------------------------------------------ */
+
+.vp-doc h1,
+.vp-doc h2,
+.vp-doc h3,
+.vp-doc h4,
+.vp-doc h5,
+.vp-doc h6 {
+ font-family: var(--vp-font-family-heading);
+ letter-spacing: -0.02em;
+}
+
+/* Hero heading on homepage */
+.VPHero .name,
+.VPHero .text {
+ font-family: var(--vp-font-family-heading) !important;
+ letter-spacing: -0.03em;
+}
+
+/* Feature card titles */
+.VPFeature .title {
+ font-family: var(--vp-font-family-heading);
+}
+
+/* Sidebar group headings */
+.VPSidebarItem.level-0 > .item > .text {
+ font-family: var(--vp-font-family-heading);
+}
+
+/* ------------------------------------------------------------------ */
+/* Content links — underlined, matching docs.source.coop */
+/* ------------------------------------------------------------------ */
+
+.vp-doc a {
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.vp-doc a:hover {
+ text-decoration: none;
+}
+
+/* ------------------------------------------------------------------ */
+/* Navbar — clean white surface (light) / dark surface (dark) */
+/* ------------------------------------------------------------------ */
+
+.VPNav {
+ background-color: var(--vp-nav-bg-color) !important;
+}
+
+.VPNavBar {
+ background-color: var(--vp-nav-bg-color) !important;
+ border-bottom: 1px solid var(--vp-c-divider) !important;
+}
+
+.VPNavBar .divider {
+ display: none;
+}
+
+/* ------------------------------------------------------------------ */
+/* Sidebar — subtle background */
+/* ------------------------------------------------------------------ */
+
+.VPSidebar {
+ background-color: var(--vp-sidebar-bg-color) !important;
+}
+
+/* ------------------------------------------------------------------ */
+/* Code font size — match docs.source.coop 95% */
+/* ------------------------------------------------------------------ */
+
+:root {
+ --vp-code-font-size: 95%;
+}
diff --git a/docs/architecture/crate-layout.md b/docs/architecture/crate-layout.md
new file mode 100644
index 0000000..3870f84
--- /dev/null
+++ b/docs/architecture/crate-layout.md
@@ -0,0 +1,114 @@
+# Crate Layout
+
+The project is organized as a Cargo workspace with libraries (traits and logic) and runtimes (executable targets).
+
+```
+crates/
+├── cli/ # source-coop CLI (OIDC login → STS credential exchange)
+├── libs/ # Libraries — not directly runnable
+│ ├── core/ (source-coop-core) # Runtime-agnostic: traits, S3 parsing, SigV4, config
+│ ├── sts/ (source-coop-sts) # OIDC/STS token exchange (AssumeRoleWithWebIdentity)
+│ ├── oidc-provider/ # Outbound OIDC provider (JWT signing, JWKS, exchange)
+│ └── source-coop/ # Source Cooperative resolver and API client
+└── runtimes/ # Runnable targets — one per deployment platform
+ ├── server/ (source-coop-server) # Tokio/Hyper for container deployments
+ └── cf-workers/ # Cloudflare Workers for edge deployments
+```
+
+## Crate Responsibilities
+
+### `source-coop-core`
+
+The runtime-agnostic core. Contains:
+- `ProxyHandler` — Two-phase request handler (`resolve_request()` → `HandlerAction`)
+- `RequestResolver` and `DefaultResolver` — Request parsing, SigV4 auth, authorization
+- `ConfigProvider` trait and implementations (static file, HTTP, DynamoDB, Postgres)
+- `ProxyBackend` trait — Runtime abstraction for store/signer/raw HTTP
+- S3 request parsing, XML response building, list prefix rewriting
+- SigV4 signature verification
+- Sealed session token encryption/decryption
+- Type definitions (`BucketConfig`, `RoleConfig`, `AccessScope`, etc.)
+
+**Feature flags:**
+- `config-http` — HTTP API config provider
+- `config-dynamodb` — DynamoDB config provider
+- `config-postgres` — PostgreSQL config provider
+- `azure` — Azure Blob Storage support
+- `gcp` — Google Cloud Storage support
+
+### `source-coop-sts`
+
+OIDC token exchange implementing `AssumeRoleWithWebIdentity`:
+- JWT decoding and validation (RS256)
+- JWKS fetching and caching
+- Trust policy evaluation (issuer, audience, subject conditions)
+- Temporary credential minting with scope template variables
+
+### `source-coop-oidc-provider`
+
+Outbound OIDC identity provider for backend authentication:
+- RSA JWT signing (`JwtSigner`)
+- JWKS endpoint serving
+- OpenID Connect discovery document
+- AWS credential exchange (`AwsOidcBackendAuth`)
+- Credential caching
+
+### `source-coop-server`
+
+The native server runtime:
+- Tokio/Hyper HTTP server
+- `ServerBackend` implementing `ProxyBackend` with reqwest
+- Streaming via hyper `Incoming` bodies and reqwest `bytes_stream()`
+- CLI argument parsing (`--config`, `--listen`, `--domain`, `--sts-config`)
+
+### `source-coop-cf-workers`
+
+The Cloudflare Workers WASM runtime:
+- `WorkerBackend` implementing `ProxyBackend` with `web_sys::fetch`
+- `FetchConnector` bridging `object_store` HTTP to Workers Fetch API
+- JS `ReadableStream` passthrough for zero-copy streaming
+- Config loading from env vars (`PROXY_CONFIG`)
+
+::: warning
+This crate is excluded from the workspace `default-members` because WASM types are `!Send` and won't compile on native targets. Always build with `--target wasm32-unknown-unknown`.
+:::
+
+### `source-coop` (lib)
+
+Source Cooperative-specific resolver and API client:
+- `SourceCoopResolver` — Custom namespace mapping (`/{account}/{repo}/{key}`)
+- External auth via Source Cooperative API
+
+### `cli`
+
+Command-line tool for OIDC authentication:
+- Browser-based OAuth2 Authorization Code + PKCE flow
+- `credential_process` integration with AWS SDKs
+- Credential caching in `~/.source-coop/credentials/`
+
+## Dependency Flow
+
+```mermaid
+flowchart TD
+ core["source-coop-core"]
+ sts["source-coop-sts"]
+ oidc["source-coop-oidc-provider"]
+ api["source-coop (lib)"]
+ server["source-coop-server"]
+ workers["source-coop-cf-workers"]
+ cli["source-coop CLI"]
+
+ server --> core
+ server --> sts
+ server --> oidc
+ workers --> core
+ workers --> sts
+ workers --> oidc
+ workers --> api
+ cli --> sts
+ sts --> core
+ oidc --> core
+ api --> core
+```
+
+Libraries define trait abstractions. Runtimes implement `ProxyBackend` with platform-native primitives and wire everything together.
diff --git a/docs/architecture/index.md b/docs/architecture/index.md
new file mode 100644
index 0000000..652ccb0
--- /dev/null
+++ b/docs/architecture/index.md
@@ -0,0 +1,57 @@
+# Architecture Overview
+
+The Source Data Proxy is an S3-compliant gateway that sits between clients and backend object stores. It provides authentication, authorization, and transparent proxying with zero-copy streaming.
+
+## High-Level Architecture
+
+```mermaid
+flowchart LR
+ Clients["S3 Clients
(aws-cli, boto3, SDKs)"]
+
+ subgraph Proxy["source-coop-proxy"]
+ Resolver["Request Resolver
(parse, auth, authorize)"]
+ Handler["Proxy Handler
(dispatch operations)"]
+ Backend["Proxy Backend
(runtime-specific I/O)"]
+ end
+
+ Config["Config Provider
(Static, HTTP, DynamoDB, Postgres)"]
+ OIDC["OIDC Providers
(Auth0, GitHub, Keycloak)"]
+ Stores["Object Stores
(S3, MinIO, R2, Azure, GCS)"]
+
+ Clients <--> Resolver
+ Resolver <--> Config
+ Resolver <--> OIDC
+ Handler <--> Backend
+ Backend <--> Stores
+```
+
+## Design Principles
+
+**Runtime-agnostic core** — The core proxy logic (`source-coop-core`) has zero runtime dependencies. No Tokio, no `worker-rs`. It compiles to both native and WASM targets.
+
+**Two-phase handler** — The proxy handler separates request resolution from execution. `resolve_request()` determines what to do; the runtime executes it. This keeps streaming logic in runtime-specific code where it belongs.
+
+**Presigned URLs for streaming** — GET, HEAD, PUT, and DELETE operations use presigned URLs. The runtime forwards the request directly to the backend — no buffering, no double-handling of bodies.
+
+**Pluggable traits** — Three trait boundaries enable customization:
+- `RequestResolver` — How requests are parsed, authenticated, and authorized
+- `ConfigProvider` — Where configuration comes from
+- `ProxyBackend` — How the runtime interacts with backends
+
+## Key Components
+
+| Component | Crate | Responsibility |
+|-----------|-------|---------------|
+| [Proxy Handler](./request-lifecycle) | `core` | Dispatch operations via presigned URLs, LIST, or multipart |
+| [Request Resolver](./request-lifecycle#request-resolution) | `core` | Parse S3 requests, authenticate, authorize |
+| [Config Providers](/configuration/providers/) | `core` | Load buckets, roles, credentials |
+| [STS Handler](/auth/proxy-auth#oidcsts-temporary-credentials) | `sts` | OIDC token exchange, credential minting |
+| [OIDC Provider](/auth/backend-auth#oidc-backend-auth) | `oidc-provider` | Self-signed JWT minting, backend credential exchange |
+| [Server Runtime](./multi-runtime#server-runtime) | `server` | Tokio/Hyper HTTP server |
+| [Workers Runtime](./multi-runtime#cloudflare-workers-runtime) | `cf-workers` | WASM-based Cloudflare Workers |
+
+## Further Reading
+
+- [Crate Layout](./crate-layout) — How the workspace is organized
+- [Request Lifecycle](./request-lifecycle) — How a request flows through the proxy
+- [Multi-Runtime Design](./multi-runtime) — How the same core runs on native and WASM
diff --git a/docs/architecture/multi-runtime.md b/docs/architecture/multi-runtime.md
new file mode 100644
index 0000000..94e6912
--- /dev/null
+++ b/docs/architecture/multi-runtime.md
@@ -0,0 +1,78 @@
+# 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 `source-coop-core`:
+
+- On native targets: `MaybeSend` resolves to `Send`, `MaybeSync` resolves to `Sync`
+- On `wasm32`: `MaybeSend` and `MaybeSync` are blanket traits that every type implements
+
+All core traits (`ProxyBackend`, `RequestResolver`, `ConfigProvider`) use `MaybeSend + MaybeSync` instead of `Send + Sync`, so they compile on both targets.
+
+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]`:
+
+```rust
+pub trait RequestResolver: Clone + MaybeSend + MaybeSync + 'static {
+ fn resolve(
+ &self,
+ method: &Method,
+ path: &str,
+ query: Option<&str>,
+ headers: &HeaderMap,
+ ) -> impl Future