Collaborative volumetric image viewer. Multiple peers open the same OME-Zarr dataset, follow each other's viewport, and share annotations in real time. Server in Rust (Axum + Tokio), client in TypeScript + WebGPU + WASM.
docker run --rm -p 127.0.0.1:9876:9876 \
-e LUCIDA_AUTH=disabled -e LUCIDA_INSECURE=1 \
ghcr.io/aelefebv/lucida:latestVisit http://localhost:9876. The 127.0.0.1: prefix on -p keeps the host's port forward bound to loopback so only your machine can reach it; the container itself still binds 0.0.0.0 internally (the Dockerfile defaults it that way), and LUCIDA_INSECURE=1 acknowledges that auth is off (see ADR-0018).
docker run --rm -p 9876:9876 \
-e LUCIDA_AUTH=disabled -e LUCIDA_INSECURE=1 \
ghcr.io/aelefebv/lucida:latestDrops the 127.0.0.1: prefix so the host port-forward listens on every interface — anyone on the same LAN can reach http://your-machine:9876. Be aware of the auth-off posture: browsers default to the admin dev@local identity, the profile menu can switch a browser to a different local dev identity for manual role testing, and admin endpoints (/admin/clear-proxy-cache) are unprotected. If you want real per-user authentication, use the auth-enabled scenario below.
For any production-shape deployment — multi-user identity, proper admin gating, internet-reachable hostname — sign-in is required. The click-by-click Google Cloud Console setup (provision an OAuth client, configure the redirect URI, supply the credentials to the container) lives in extras/deploy/RUNBOOK.md §2 alongside the Kubernetes manifests in extras/deploy/k8s/ and the single-host docker-compose alternative in extras/deploy/docker-compose.yml. The conceptual model (env-var contract, persistence layout, OAuth provider extensibility, per-cloud identity wiring) lives in wiki/systems/subsystems/deployment.md.
Prerequisites: rust + cargo, pnpm, wasm-pack, node — your package manager equivalent.
One command brings everything up to date and runs both servers:
./scripts/dev.shIt installs the web deps if they're missing, rebuilds the lucida-core wasm pkg only when a lucida-* Rust source actually changed (content-hashed, so an mtime bump from git checkout doesn't trigger a needless rebuild), builds lucida-server, then starts the relay server (binds 127.0.0.1:9876, auth auto-disabled on loopback) and the Vite SPA dev server (which proxies /auth /api /admin /ws to :9876), streaming both logs. Ctrl-C stops both cleanly. Pass --wasm to force a wasm rebuild, or --help for details.
Then visit http://localhost:5173.
Prefer to run the two-terminal loop by hand?
One-time setup:
(cd lucida-web && pnpm install)Terminal 1 — relay server (binds 127.0.0.1:9876, auth auto-disabled on loopback):
cargo run -p lucida-serverTerminal 2 — SPA dev server (Vite proxies /auth /api /admin /ws to :9876):
(pnpm install --force && cd lucida-web && pnpm run build:wasm && pnpm run dev -- --force)pnpm install --force refreshes the local file:../lucida-core/pkg copy in node_modules, and pnpm run dev -- --force makes Vite discard any stale optimized dependency cache.
Visit http://localhost:5173.
The product CLI command is lucida. From a source checkout, run the same binary by replacing lucida with cargo run -p lucida-cli --, for example cargo run -p lucida-cli -- --server http://127.0.0.1:9876 status.
To install the CLI from a checkout for repeated local use:
cargo install --locked --path lucida-cli
lucida --server http://127.0.0.1:9876 statusFor an auth-disabled local server:
lucida --server http://127.0.0.1:9876 status
lucida --server http://127.0.0.1:9876 workspace list
lucida --server http://127.0.0.1:9876 workspace create "local analysis"
lucida --server http://127.0.0.1:9876 workspace use "local analysis"
lucida --server http://127.0.0.1:9876 workspace open --no-browser
lucida --server http://127.0.0.1:9876 workspace pin
lucida --server http://127.0.0.1:9876 workspace share showFor protected deployments, authenticate first:
lucida --server https://lucida.example.org auth login
lucida auth whoamiTo open a dataset and verify it in the browser, keep a browser on the workspace URL printed by workspace open, then run:
lucida dataset browse /var/lib/lucida/data
lucida dataset open /var/lib/lucida/data/sample.ome.zarr
lucida dataset list
lucida viewer state
lucida viewer screenshot current-view.pngThe already-open browser workspace should update when dataset open, layout, saved-view, or other shared workspace commands land. View, camera, layer, and channel commands update the selected durable headless viewer profile by default, and can also broadcast ephemeral presence while connected. viewer screenshot/viewer overview use the web renderer through Chrome/Chromium and wait for a nonblank canvas before writing the PNG.
Live peer following is intentionally ephemeral. To inspect or capture what an
already-open browser peer is looking at, run lucida peer list to find the
client id, then use lucida viewer state --from-peer <client-id>,
lucida viewer screenshot --from-peer <client-id> peer-view.png, or
lucida viewer adopt --from-peer <client-id> to copy that peer's current view
into the durable headless viewer profile.
Python scripts use the same server/client model:
from lucida import LucidaClient
client = LucidaClient("http://127.0.0.1:9876")
workspace = client.workspaces.use("local analysis")
workspace.datasets.open("/var/lib/lucida/data/sample.ome.zarr")
print(workspace.datasets.list())LucidaClient reads explicit constructor tokens, LUCIDA_TOKEN, macOS Keychain credentials created by lucida auth login, and the CLI-compatible config file. Default workspaces and config-file token fallback are scoped to the normalized server URL.
From a source checkout, run Python examples through the package environment:
uv run --project lucida-py python your_script.pyFor a repeatable local smoke pass against a running server, set a server-visible dataset path and run:
export LUCIDA_SMOKE_SERVER=http://127.0.0.1:9876
export LUCIDA_SMOKE_DATASET=/var/lib/lucida/data/sample.ome.zarr
scripts/smoke_lucida_cli.sh
uv run --project lucida-py python scripts/smoke_python_client.pyThe CLI smoke script isolates LUCIDA_CONFIG_PATH in a temp directory, creates a throwaway workspace, opens the dataset, checks dataset health, verifies structured diagnostics for missing/malformed dataset opens, mutates view/layer/channel state, runs debug/plan diagnostics, and validates screenshot/overview PNGs. Set LUCIDA_SMOKE_CAPTURE=0 to skip browser-rendered captures when Chrome/Chromium is unavailable.
For a broader local fixture pass against Austin's test datasets, run a server
whose --data-dir can see /Users/austin/local_data/lucida_test_zarrs, then:
uv run --project lucida-py python scripts/smoke_dataset_reliability.py \
--server "$LUCIDA_SMOKE_SERVER"Add to any of the docker run recipes above.
Mount a local data directory so /api/browse can list OME-Zarr files on your filesystem (otherwise browsing is restricted to gs:// / s3:// / http(s):// URLs):
-v /path/on/host:/var/lib/lucida/data \
-e LUCIDA_DATA_DIR=/var/lib/lucida/dataPersist bookmarks/sessions across restarts with a named volume covering the whole /var/lib/lucida tree (lucida.db + proxy cache). Without this, docker rm wipes everything; matters most for the LAN-shared case where multiple people accumulate state:
-v lucida-data:/var/lib/lucidaLucida discovers Google Cloud credentials, in order: object_store-native GOOGLE_SERVICE_ACCOUNT* env vars, then GOOGLE_APPLICATION_CREDENTIALS (forwarded explicitly), then the well-known ADC file at $HOME/.config/gcloud/application_default_credentials.json, then the GCE metadata server. See wiki/gotchas/gcs-credentials.md for the full story (and how to avoid the off-cluster ~13s metadata-server hang).
Bare binary on a dev laptop with gcloud auth application-default login already done — zero env config; the well-known ADC file at $HOME/.config/gcloud/application_default_credentials.json is read automatically:
cargo run -p lucida-serverdocker run with the host's ADC file (or any service-account JSON) bind-mounted in:
docker run --rm -p 127.0.0.1:9876:9876 \
-e LUCIDA_AUTH=disabled -e LUCIDA_INSECURE=1 \
-e GOOGLE_APPLICATION_CREDENTIALS=/gcp/adc.json \
-v "$HOME/.config/gcloud/application_default_credentials.json:/gcp/adc.json:ro" \
ghcr.io/aelefebv/lucida:latestGKE with Workload Identity — annotate the KSA with the GSA email and lucida picks credentials up via the metadata server with no env config. Full walkthrough in extras/deploy/RUNBOOK.md §5.
- Rust changes in any
lucida-*crate → the SPA needs a fresh WASM build;./scripts/dev.shrebuilds it automatically on restart, or rerun(cd lucida-web && pnpm run build:wasm)by hand - TypeScript changes in
lucida-web/→ Vite hot-reloads automatically - Python binding changes in
lucida-py/→(cd lucida-py && maturin develop)
Tests:
cargo test --workspace
(cd lucida-web && pnpm test)Type-check the SPA: (cd lucida-web && pnpm exec tsc --noEmit -p tsconfig.app.json) — see wiki/gotchas/ts-typecheck-trap for why the project flag is load-bearing.
The wiki under wiki/ is the primary reference — start at wiki/index.md (or wiki/CLAUDE.md for navigation conventions).
For why something is shaped the way it is, look in wiki/decisions/ (numbered ADRs). For what bites you when you don't expect it, look in wiki/gotchas/.
MIT — see LICENSE.