Hermes Research · executes Themis-PM #8 · Track C / C7

Canonical Calendar:
the open-source & self-hosted landscape

A 5-lane study of how to power a Supabase-backed canonical calendar and a state-of-the-art planning app — tying together the live cal.boas.dev booking pipeline and meet.boas.dev.

Author: hermes Date: 2026-06-09 Method: /research-orchestrate (5 Sonnet lanes) Build owner: metis Commit: 31c8680

The Verdict

C7 resolves to Option C — by the operator's own constraint

“A Supabase calendar DB where we log all events” means Supabase is the SSOT. That disqualifies Option A (Cal.com-centric) and Option B (CalDAV-server-centric) as canonical — each relocates the SSOT into an opaque store. They aren't beaten on merit; they're ruled out by the requirement, and their tooling is re-used as edges around the Supabase core.

The decision was effectively pre-made. The research confirms it's also the correct call: only a Supabase-canonical model gives an agent fleet one SQL-queryable calendar (joins to crm_deals, meetings, meeting_tasks, agent identity) with RLS-scoped visibility and Realtime.

TL;DR — Seven Findings

  1. The A/B/C decision collapses to Option C. Supabase = SSOT requirement disqualifies A and B as canonical; both become edges.
  2. Cal.com cannot be the calendar. One event type (Booking), no arbitrary events, recurrence as a JSON blob (not RFC-5545 RRULE), cannot serve CalDAV. It's a booking front-end — exactly as wired today.
  3. No CalDAV server is a shareable Postgres store. Radicale (files), Baïkal/Nextcloud/SOGo (own opaque schema), DAViCal (Postgres but DAViCal-owned), Xandikos (Git), EteSync (E2EE). Run one as a read-edge, never as the SSOT.
  4. The Supabase events model is well-trodden, not bespoke risk. Clean RFC-5545 schema: UTC + IANA tzid + generated tstzrange, EXCLUDE USING gist double-booking, store-the-rule recurrence.
  5. External sync is feasible self-hosted. Thin TS worker (tsdav + ical.js + googleapis + graph-client), hybrid push+poll. iCloud is poll-only. Nylas/Cronofy are SaaS-lock fallback only.
  6. The planning app's value is ritual + UX speed, not AI. Akiflow/Sunsama win on daily-ritual flow + command-bar + drag time-blocking — all thin builds. Defer AI auto-scheduling.
  7. Rule #18 (adopt-before-build) satisfied. Every layer except the SSOT table + planning UX is adopted. The justified net-new build: one queryable fleet calendar SSOT + the planner over it — unbuyable.

The A/B/C Decision, Resolved

OptionWhat it meansVerdict
A — Cal.com-centric Cal.com's Postgres is the calendar ✗ Disqualified. Booking-only schema; no arbitrary events; not a CalDAV server; commercial/closed drift (v6.4 split, Apr 2026).
B — CalDAV-server-centric Nextcloud / Baïkal / Radicale / DAViCal is canonical ✗ Disqualified as SSOT. Server-owned, opaque storage; external writes corrupt it. ✓ Re-used as a native-client read-edge.
C — Supabase-canonical + edges Supabase events table is the SSOT; all else syncs to it ✓ Selected — the only model consistent with “Supabase DB where we log all events.”

Target Architecture — Supabase Core + Adopted Edges

Booking edge · LOCKED

Cal.com → n8n

cal.boas.dev webhook (HMAC) → n8n upsert. Track A, green. Booking front-end only.

Meet edge · Track E

meet.boas.dev/<slug>

Per-booking link written to events.meeting_url.

Native-client edge · C4/C5

Radicale / Baïkal on VPS

One-way from Supabase + ICS/webcal feed. Phones/desktop subscribe. NOT canonical.

Single source of truth

SUPABASE

events · event_attendees · sync_state
RFC-5545 model · RLS · Realtime
tstzrange EXCLUDE

the justified build

External sync edge · C8

TS sync worker

Google / MS Graph / iCloud. Push+poll, echo-loop guard, UID mapping. iCloud poll-only.

Interaction layer

Planning app

Next.js/React · react-big-calendar · ritual UX. AI auto-scheduling deferred.

Fleet joins

crm_deals · meetings · tasks

One SQL-queryable calendar for the agent fleet — the reason C wins.

Everything outside the green box is adopted. The box — schema + planning UX — is the build.

Component Picks

Build · canonical store

Supabase events table

Generalize the live meetings table. Wall-clock + IANA tzid + generated dtstart_utc; EXCLUDE USING gist double-booking; store-the-rule recurrence via pg_rrule/rrule.js; event_kind→TRANSP taxonomy; RLS by agent. Full DDL in lane T2 §7.

Adopt · booking

Cal.com (unchanged)

Booking-only — confirmed it cannot be canonical. Keep the live webhook→n8n→Supabase spine; write the canonical events row from it.

Adopt + thin glue · C4/C5

Radicale or Baïkal

Lightweight CalDAV on the Hostinger VPS, fed one-way from Supabase + ICS feed. Baïkal for web admin/sharing; Radicale for minimum ops. Avoid SOGo/Nextcloud (6–8 / 2–4 GB).

Adopt libs + build worker · C8

TS sync worker

tsdav + ical.js + googleapis + graph-client on the VPS. Push+poll, last-write-wins, sync-id + iCal-UID dedup. Nango optional for OAuth.

Build · interaction layer

Planning app

Next.js/React on Supabase. react-big-calendar (MIT) → FullCalendar v7 when resource lanes needed. MVP = ritual UX + drag time-blocking + scheduling links. Morgen = reference UX.

Adopt · recurrence

pg_rrule / rrule.js

Expand on read; never materialize (520+ rows/decade per weekly event). Note: pg_rrule needs COUNT/UNTIL — set a far-future UNTIL for open-ended series.

Build Sequence — maps to Track C

#WorkTrack CAdopt vs Build
1events + event_attendees + sync_state schema (generalize meetings)C3Build (schema), patterns adopted
2Event-kind taxonomy + TRANSP mappingC6Build (CHECK constraints)
3Cal.com webhook → write canonical events rowA↔C1Adopt (live pipeline)
4Track E meet-link bridge → events.meeting_urlE1Build (thin glue)
5Supabase→CalDAV one-way adapter + run Radicale/BaïkalC4Adopt server + build adapter
6ICS/webcal read-only feed publisherC5Build (thin)
7Planning app MVP (react-big-calendar + ritual UX)app layerAdopt lib + build UX
8External sync worker (Google/MS first, iCloud poll)C8Adopt libs + build worker

Steps 1–4 are the spine and unblock the planning app; 5–6 give native-client reach; 8 (two-way external sync) is heaviest, last.

Evidence — CalDAV Servers (lane T1)

ServerPostgres-backableOps burdenLast releaseEdge role
RadicaleNo (filesystem)Very lowv3.7.4 · 2026-05Minimum-ops read-edge
BaïkalUncertain (PDO)Lowv0.11.1 · 2025-11Read-edge + web admin
DAViCalYes (PG-only, owned)Moderatev1.1.9.3 · 2026Closest, but slowing
NextcloudYes, opaqueMod–HighHub 9 · 2026Only if already running
SOGoYes, opaqueVery high (6–8 GB)v5.12.9 · 2026-05Overkill
XandikosNo (Git)Lowv0.4.2 · 2026-05RFC-6578 complete, niche
EteSyncNo (E2EE)Mod–Highv0.35.1 · 2025-08Incompatible w/ C

Evidence — Planning Apps & UI Libraries (lane T4)

Killer-feature split

AppCore betAI auto-scheduleRitualCalDAV-friendly
MotionFull AI auto-scheduling + reflowpartial
ReclaimSchedule defense / habits✓ lighter
AkiflowCommand-bar speed + daily ritual✓ best
SunsamaMindful guided planning2026 partial✓ guided
MorgenUnified + CalDAV-native + LinuxAI assist✓ reference

Key insight: the most-loved apps win on ritual design + UX speed, not AI. A fast command-bar + daily ritual + drag time-blocking delivers ~80% of value at ~20% of the AI-tier cost.

UI calendar libraries

LibraryFrameworksRecurrenceLicensePick
react-big-calendarReactmanualMITStart here (MVP)
FullCalendar v7React/Vue/AngularpluginMIT core / $480/dev premiumUpgrade for Timeline/Resource
Schedule-Xall majorpluginfree core / paid premiumIf framework-agnostic matters
Toast UIReact/Vue (stale)unknownMITAvoid (stale)
Mobiscrollall majoryes$995+/yr commercialAvoid (no OSS leverage)

Confidence & Caveats

High confidence (multi-lane agreement): Cal.com = booking-edge not store (T2+T3+T5); no CalDAV server as shareable SSOT (T1); store-don't-materialize recurrence (T2+T5); iCloud poll-only (T5); ritual > AI for planning value (T4).

Surfaced fork (resolved, not silently picked): Lane T3 labels Nextcloud/Baïkal “viable canonical store”; lane T1 shows no CalDAV server is Postgres-transparent. Reconciled: those verdicts mean canonical if you let the CalDAV server own the SSOT — which violates the Supabase-SSOT requirement. Hence CalDAV servers are edges, not the store. The lanes don't actually conflict once the constraint is applied.

Watch items: a possible 2026-06-09 Cal.com license change (verify before deeper coupling); tsdav maintenance status; pg_rrule cannot expand infinite RRULEs (needs UNTIL).