# Spacedrive Server Architecture ## Overview The Spacedrive Server is a production-ready HTTP server that embeds the Spacedrive daemon and serves the web interface. It's designed for headless deployments, NAS systems, and container environments. ## Design Principles 1. **Embedded Daemon** - No separate process management needed 2. **Single Binary** - Web assets bundled via `include_dir` when built with `--features assets` 3. **Platform Abstraction** - Uses same `@sd/interface` as Tauri, with web-specific platform impl 4. **Security First** - HTTP Basic Auth for all endpoints (except health check) 5. **Container Native** - Docker-first design with distroless runtime image ## Components ### 1. HTTP Server (`apps/server/src/main.rs`) Built with Axum, provides: - **`GET /health`** - Health check (no auth required) - **`POST /rpc`** - JSON-RPC proxy to daemon Unix socket - **`GET /*`** - Static asset serving (SPA fallback to index.html) **Flow:** ``` Browser → HTTP Request → Axum Router → Basic Auth Middleware → Handler ↓ ┌─────────────────────────┴────────┐ ↓ ↓ Static Assets RPC Proxy (serve from ↓ ASSETS_DIR) Unix Socket → Daemon ``` ### 2. Embedded Daemon Unlike Tauri (which spawns `sd-daemon` as a child process), the server runs the daemon in-process: ```rust tokio::spawn(async move { sd_core::infra::daemon::bootstrap::start_default_server( socket_path, data_dir, enable_p2p, ).await }); ``` **Benefits:** - Single container image - Simplified lifecycle management - Shared memory space (more efficient) - Graceful shutdown via `tokio::select!` **Daemon lifecycle:** 1. Check if socket already exists (reuse existing daemon) 2. If not, spawn daemon in background task 3. Wait for socket file to appear (max 3s) 4. Return handle for graceful shutdown ### 3. Web Client (`apps/web/`) Minimal React app using `@sd/interface`: ```tsx // apps/web/src/main.tsx ``` **Platform implementation:** ```typescript // apps/web/src/platform.ts export const platform: Platform = { platform: "web", openLink(url) { window.open(url) }, confirm(msg, cb) { cb(window.confirm(msg)) }, // No native file pickers, daemon control, etc. }; ``` **Build process:** 1. Vite bundles React app → `apps/web/dist/` 2. `build.rs` runs `pnpm build` before compiling server 3. `include_dir!` macro embeds `dist/` into binary at compile time 4. Axum serves embedded files from memory ### 4. RPC Proxy Browsers can't connect to Unix sockets, so the server proxies: ``` Browser Server Daemon │ │ │ │ POST /rpc │ │ ├────────────────────>│ │ │ (JSON-RPC) │ Unix Socket Write │ │ ├────────────────────────>│ │ │ │ │ │ Unix Socket Read │ │ │<────────────────────────┤ │ 200 OK │ │ │<────────────────────┤ │ │ (JSON-RPC result) │ │ ``` **Implementation:** ```rust async fn daemon_rpc( State(state): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { let mut stream = UnixStream::connect(&state.socket_path).await?; stream.write_all(format!("{}\n", serde_json::to_string(&payload)?).as_bytes()).await?; let mut reader = BufReader::new(stream); let mut response = String::new(); reader.read_line(&mut response).await?; Ok(Json(serde_json::from_str(&response)?)) } ``` ## Comparison: Server vs Tauri vs CLI | Aspect | Server | Tauri | CLI | |--------|--------|-------|-----| | **Process Model** | Embedded daemon | Spawned daemon | Connects to daemon | | **UI** | Web (React in browser) | WebView (React) | Terminal (TUI) | | **Daemon Communication** | Unix socket (proxied) | Unix socket (direct) | Unix socket (direct) | | **Platform Abstraction** | `platform: "web"` | `platform: "tauri"` | N/A | | **Access Model** | Remote (HTTP) | Local only | Local only | | **Auth** | HTTP Basic Auth | Not needed | Not needed | | **Deployment** | Docker, systemd | App bundle | Binary | ## Authentication Flow ``` 1. Browser makes request without auth ↓ 2. basic_auth middleware checks state.auth ↓ 3. If empty → allow (auth disabled) If populated → require Basic Auth header ↓ 4. Extract credentials from Authorization header ↓ 5. Compare with state.auth HashMap ↓ 6. Match → proceed to handler No match → 401 Unauthorized ``` **Security considerations:** - Credentials stored in memory as `SecStr` (zeroed on drop) - Basic Auth over HTTPS recommended for production - Socket file has filesystem permissions (only accessible to server user) ## Docker Architecture ### Multi-stage Build ```dockerfile # Stage 1: Builder (Debian + Rust + Node) FROM debian:bookworm-slim AS builder RUN install Rust, Node, pnpm COPY workspace RUN pnpm build (web) RUN cargo build --release --features assets # Stage 2: Runtime (Distroless) FROM gcr.io/distroless/cc-debian12:nonroot COPY --from=builder /build/target/release/sd-server ENTRYPOINT ["/usr/bin/sd-server"] ``` **Benefits:** - **Small image** - Distroless base (~50MB vs ~1GB for full Debian) - **Secure** - No shell, no package manager, minimal attack surface - **Fast** - Cached layers for dependencies - **Reproducible** - Locked versions via `Cargo.lock` and `pnpm-lock.yaml` ### Volume Mounts ```yaml volumes: - spacedrive-data:/data # Persistent library data - /mnt/storage:/storage:ro # Optional: Read-only media access ``` **Data layout:** ``` /data/ ├── daemon/ │ └── daemon.sock # Unix socket for RPC ├── libraries/ │ └── *.sdlibrary/ # SQLite databases │ ├── library.db │ └── sidecars/ # Thumbnails, previews ├── logs/ │ ├── daemon.log │ └── indexing.log └── current_library_id.txt ``` ## Development vs Production ### Development Mode ```bash # Terminal 1: Web dev server (hot reload) cd apps/web pnpm dev # → http://localhost:3000 # Terminal 2: API server cargo run -p sd-server # → http://localhost:8080 # Vite proxies /rpc to 8080 ``` **Workflow:** 1. Edit React components → Vite hot reloads 2. Edit server code → `cargo run` rebuilds 3. No need to rebuild web assets during development ### Production Build ```bash # Build with bundled assets cargo build --release -p sd-server --features assets # Single binary contains: # - Axum HTTP server # - Embedded daemon # - Bundled web UI (React app) ``` **Deployment:** ```bash ./target/release/sd-server \ --data-dir /var/lib/spacedrive \ --port 8080 ``` ## Platform Abstraction Both Tauri and Web use `@sd/interface`, but with different platform implementations: ### Tauri Platform (`apps/tauri/src/platform.ts`) ```typescript { platform: "tauri", openDirectoryPickerDialog: async () => open({ directory: true }), revealFile: async (path) => invoke("reveal_file", { path }), getCurrentLibraryId: async () => invoke("get_current_library_id"), getDaemonStatus: async () => invoke("get_daemon_status"), // Full native capabilities } ``` ### Web Platform (`apps/web/src/platform.ts`) ```typescript { platform: "web", openLink: (url) => window.open(url), confirm: (msg, cb) => cb(window.confirm(msg)), // Minimal browser-only capabilities } ``` **Interface components adapt:** ```tsx function FilePickerButton() { const platform = usePlatform(); if (platform.platform === "tauri") { // Show native picker button return ; } else { // Web: no native picker, show manual path input return ; } } ``` ## Error Handling ### HTTP Errors ```rust async fn daemon_rpc(...) -> Result, (StatusCode, String)> { let stream = UnixStream::connect(&socket_path) .await .map_err(|e| (StatusCode::SERVICE_UNAVAILABLE, format!("Daemon not available: {}", e)))?; // ... } ``` **Responses:** - `503 Service Unavailable` - Daemon not running - `400 Bad Request` - Invalid JSON - `500 Internal Server Error` - RPC failed - `401 Unauthorized` - Auth failed ### Daemon Errors Daemon errors are passed through RPC response: ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "Library not found" } } ``` ## Performance Considerations 1. **Static Assets** - Served from memory (embedded via `include_dir`) 2. **Socket Pooling** - Each RPC request opens new socket (TODO: connection pool) 3. **Async I/O** - Tokio runtime handles concurrent requests 4. **Graceful Shutdown** - Waits for in-flight requests before terminating ## Future Enhancements 1. **WebSocket Support** - Real-time event streaming (vs polling) 2. **HTTPS** - TLS termination (currently expects reverse proxy) 3. **Connection Pool** - Reuse Unix sockets for RPC 4. **Multi-tenancy** - Separate libraries per user 5. **SSE Events** - Server-sent events for daemon notifications ## Security Model **Trust Boundaries:** ``` Internet ←[TLS]→ Reverse Proxy ←[HTTP+Auth]→ Server ←[Unix Socket]→ Daemon () () () () () ``` **Assumptions:** - Server runs on trusted network OR behind reverse proxy with TLS - Unix socket accessible only to server process (filesystem permissions) - HTTP Basic Auth sufficient for home/NAS use - For public internet: Use nginx/Caddy with Let's Encrypt ## Monitoring **Health Check:** ```bash curl http://localhost:8080/health # → "OK" ``` **Logs:** ```bash # Docker docker logs spacedrive -f # Systemd journalctl -u spacedrive -f # Native RUST_LOG=debug ./sd-server ``` **Metrics:** (TODO) - Request count/latency - Daemon socket errors - Active connections ## Related Documentation - [README.md](./README.md) - Setup and usage - [../../docs/core/architecture.md](../../docs/core/architecture.md) - Core VDFS design - [../tauri/DAEMON_SETUP.md](../tauri/DAEMON_SETUP.md) - Tauri daemon integration