./projects/bahala-foundation
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}.sqliteblob in Spaces. The API ATTACHes them at runtime as named aliases and queries withUNION 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.jsonin Spaces pointing at the most recent SQLite blob. The API polls (or gets triggered) to compare versions and atomically swap to a new file viadetach→rename→attach.
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)