apps/server/ARCHITECTURE.md
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.
include_dir when built with --features assets@sd/interface as Tauri, with web-specific platform implapps/server/src/main.rs)Built with Axum, provides:
GET /health - Health check (no auth required)POST /rpc - JSON-RPC proxy to daemon Unix socketGET /* - 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
Unlike Tauri (which spawns sd-daemon as a child process), the server runs the daemon in-process:
tokio::spawn(async move {
sd_core::infra::daemon::bootstrap::start_default_server(
socket_path,
data_dir,
enable_p2p,
).await
});
Benefits:
tokio::select!Daemon lifecycle:
apps/web/)Minimal React app using @sd/interface:
// apps/web/src/main.tsx
<PlatformProvider platform={webPlatform}>
<Explorer />
</PlatformProvider>
Platform implementation:
// 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:
apps/web/dist/build.rs runs pnpm build before compiling serverinclude_dir! macro embeds dist/ into binary at compile timeBrowsers 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:
async fn daemon_rpc(
State(state): State<AppState>,
Json(payload): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, (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)?))
}
| 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 |
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:
SecStr (zeroed on drop)# 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:
Cargo.lock and pnpm-lock.yamlvolumes:
- 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
# 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:
cargo run rebuilds# 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:
./target/release/sd-server \
--data-dir /var/lib/spacedrive \
--port 8080
Both Tauri and Web use @sd/interface, but with different platform implementations:
apps/tauri/src/platform.ts){
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
}
apps/web/src/platform.ts){
platform: "web",
openLink: (url) => window.open(url),
confirm: (msg, cb) => cb(window.confirm(msg)),
// Minimal browser-only capabilities
}
Interface components adapt:
function FilePickerButton() {
const platform = usePlatform();
if (platform.platform === "tauri") {
// Show native picker button
return <button onClick={platform.openDirectoryPickerDialog}>Pick</button>;
} else {
// Web: no native picker, show manual path input
return <input type="text" placeholder="Enter path..." />;
}
}
async fn daemon_rpc(...) -> Result<Json<Value>, (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 running400 Bad Request - Invalid JSON500 Internal Server Error - RPC failed401 Unauthorized - Auth failedDaemon errors are passed through RPC response:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"message": "Library not found"
}
}
include_dir)Trust Boundaries:
Internet ←[TLS]→ Reverse Proxy ←[HTTP+Auth]→ Server ←[Unix Socket]→ Daemon
() () () () ()
Assumptions:
Health Check:
curl http://localhost:8080/health
# → "OK"
Logs:
# Docker
docker logs spacedrive -f
# Systemd
journalctl -u spacedrive -f
# Native
RUST_LOG=debug ./sd-server
Metrics: (TODO)