./projects/bahala-foundation
← back

Bahala Calendar

An event consolidator and RSVP platform for the City of Santa Monica. Pulls events from 8 upstream sources (Notion, Eventbrite, scraped venue sites) into a unified per-source SQLite snapshot served by a small Express API, with a React calendar frontend and a Go ETL pipeline running nightly on DigitalOcean App Platform.

Bahala Calendar is the public-facing event calendar for the Bahala Foundation, consolidating events from across Santa Monica into a single, filterable, RSVP-able view at calendar.bahala.org.

Architecture

The system is a monorepo with three deployable components plus a serverless function, all on DigitalOcean App Platform:

  • web — React 18 + TypeScript + Vite. The calendar UI: weekly grid view, tag/organization filtering, full-text search built from a client-side index, event modals with RSVP, and embed views for syndication.
  • api — Express + TypeScript + better-sqlite3. Serves event data over a thin JSON API and owns a separate transactional SQLite database for RSVPs. Notion writes for the operational owner's view of "who's coming to what" happen here.
  • etl — Go 1.25. A scheduled cron container that runs at midnight LA time, extracts events from 8 sources concurrently, transforms into a per-source SQLite snapshot, and uploads to DigitalOcean Spaces. A failure-alert system classifies each source's run as OK / FAILED / NO EVENTS / NOT SHIPPED and emails the operations owner via Mailjet when something needs attention.
  • functions/notion-sync — A DO Function that fires on a Notion webhook so Deb's manual event edits propagate to the calendar within seconds instead of waiting for the next midnight cron.

Architectural decisions worth calling out

  • Per-source SQLite snapshots, not a unified database. Each source produces its own {source}.sqlite blob in Spaces. The API ATTACHes them at runtime as named aliases and queries with UNION ALL. The trade — query-layer complexity for fault isolation — pays off every time one upstream API breaks and the other seven keep shipping fresh data.
  • The API as a relay for events, but origin-of-truth for RSVPs. Events live upstream (Notion, Eventbrite, etc.) so the API is just a cache. RSVPs are born inside the system, so the API persists them transactionally to local SQLite, backs the file up to Spaces on an interval, and writes a copy to Notion so the operational owner can see who's coming in the tool she already uses.
  • Manifest-driven SQLite refresh. Each source has a {source}/manifest.json in Spaces pointing at the most recent SQLite blob. The API polls (or gets triggered) to compare versions and atomically swap to a new file via detachrenameattach.

Recent work

  • ETL failure alerts (FAILED / NO EVENTS / NOT SHIPPED classification, Mailjet emails to operations)
  • Confetti + form-anchored celebrations on successful RSVP submit, with sibling-widget state sync across modal and card instances
  • Per-program location defaults for recurring events (Coffee & Connections fills in venue automatically when blank in Notion)
  • Resilience fixes for upstream CDN changes (custom User-Agent header to defeat Cloudflare blocks on default Go HTTP clients)