HOST=0.0.0.0 makes the app reachable from another machine. Only the frontend moves: Trunk proxies /api from the server side, so the backend stays on the loopback and never needs exposing. Prints the reachable addresses from the routing table rather than `hostname -I`, which is empty on some systems, and skips the container bridges, which are not useful to anyone.
Runway
Passive infrastructure for life's coordination.
A CalDAV web client in Rust — Leptos/WASM frontend, Axum backend, speaking to any RFC-compliant CalDAV server (developed against Baikal).
This is a ground-up rewrite. Its predecessor lives in ../calendar; the reasons it was replaced,
and the feature-by-feature decisions that shaped this one, are recorded in
docs/legacy-audit.md — that document is the spec.
Layout
| Crate | Role |
|---|---|
runway-core |
RFC 5545 model, iCalendar round-trip, recurrence expansion. Pure, no I/O. |
runway-caldav |
CalDAV client: discovery, time-ranged queries, ETag-aware CRUD. |
runway-server |
Axum backend: CalDAV proxy, sessions, preferences, ICS feeds. |
runway-web |
Leptos CSR frontend. |
runway-cli |
Smoke tool for exercising a real CalDAV server from the terminal. |
runway-core is feature-gated (model / ical / recurrence) so the frontend gets the shared
types without pulling the parser and RRULE engine into the WASM bundle.
Development
The toolchain is pinned in rust-toolchain.toml and installs automatically on first use.
cargo check --workspace
cargo test --workspace
cargo clippy --workspace -- -D warnings
The CalDAV integration tests need a server and are skipped without one. This starts a throwaway Baikal in a container, installs it, runs them against it, and tears it down:
crates/runway-caldav/tests/baikal/run.sh
Running the backend needs an encryption key for stored CalDAV credentials. It must stay the same across restarts — a key invented at startup would silently make every saved credential unreadable — so the server refuses to start without one rather than generating a throwaway:
export RUNWAY_SECRET_KEY=$(cargo run -q -p runway-server -- genkey)
export RUNWAY_DATABASE_URL=sqlite:runway.db # default
export RUNWAY_BIND=0.0.0.0:3000 # default
export RUNWAY_INSECURE_COOKIES=1 # local HTTP only
cargo run -p runway-server
Frontend
npm install # Tailwind v4 and the browser tooling
cd crates/runway-web && trunk serve
Trunk serves on :8080 and proxies /api to the backend on :3000, so the
session cookie is same-origin in development exactly as in production — nothing
has to be weakened to make local work.
The whole stack, for looking at — a throwaway Baikal seeded with a week of events, the backend, and the frontend:
npx playwright install chromium # once
e2e/dev.sh up # → http://127.0.0.1:8080, testuser/testpassword
HOST=0.0.0.0 e2e/dev.sh up # ...and reachable from another machine
e2e/dev.sh shoot # screenshots + anything the console said
e2e/dev.sh down
Only the frontend binds to HOST; the backend stays on the loopback, because
Trunk proxies /api to it from the server side. HOST=0.0.0.0 is for looking
at the app from another machine on a network you trust — the dev stack runs with
RUNWAY_INSECURE_COOKIES=1 and a throwaway calendar server.
Driving a real browser is the loop v1 never had, and the reason its week grid
could only be checked by building, deploying and squinting. shoot.mjs also
asserts: that hiding a calendar removes its events, and that every preference
survives a reload.
Some tests can be pointed at a whole real calendar rather than the committed fixtures:
RUNWAY_TEST_FEED_FILE=/path/to/calendar.ics cargo test -p runway-server --test feeds
The CLI drives the same stack the app does, which makes it the quickest way to tell a display bug from a data one:
export RUNWAY_CALDAV_URL=https://example.com/dav.php/
export RUNWAY_CALDAV_USER=you
export RUNWAY_CALDAV_PASSWORD=... # from the environment, not a flag
cargo run -p runway-cli -- calendars
cargo run -p runway-cli -- list-events --from 2026-08-24 --to 2026-08-31
Principles
Carried over from the audit, and enforced by lints and CI rather than by good intentions:
- One representation of an event.
VEventis the model and the wire format. No parallel DTOs. - Use the library. Recurrence is
rrule, iCalendar isicalendar, XML isquick-xml. - Fix the parse, not the output. Never post-process data to paper over a bad parse.
- State lives where it's owned. No localStorage-as-global, no prop-drilling megacomponents.
- Test thoroughly. Real servers over mocks; the router under test is the router that ships.