The parse is the whole story. v1's importer had two open TODOs -- no RECURRENCE-ID and no VTIMEZONE -- so a correct feed came back looking full of duplicates, because a series master and its modified occurrences share a UID and a SUMMARY and read as a flat list they look like one event repeated. Rather than fix that, v1 added ~500 lines that stripped punctuation from titles, grouped by the result, scored the collisions for "completeness" and threw the losers away. Nothing here compares titles, because nothing looks like a duplicate in the first place. The regression test is two genuinely different meetings called "Stand-up" and "Stand up" at the same moment: both survive. Against the real 1 MB Outlook feed, 103 UIDs expand to 644 occurrences over twenty years with every UID accounted for and no series producing two occurrences at one instant. Freshness has three answers, cheapest first: If-None-Match, then If-Modified-Since, then a hash of the body -- and the hash is not academic, because a published Outlook calendar sends neither validator, so without it every poll would reparse a megabyte to discover nothing changed. v1 had `let etag = None; // TODO`. An unreachable feed serves yesterday's copy rather than blanking the week, and says so. A URL that returns a login page with a 200 is refused at subscribe time, when the person still has the link to hand, and the failed subscription is removed rather than left as a broken row. webcal:// is rewritten, because that is the scheme Outlook and Google hand people.
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
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.