Skip to content

Scheduled Lights: Backend Foundation (2026-06-12) โ€‹

Scheduled lights existed as UI-only flow (ScheduleLightForm โ†’ confirmation view โ†’ upcomingLights in local React state) with all three /lanterns/schedule endpoints returning 501. This work makes the feature real end-to-end: persistence, auto-activation, and live sync. Related: #177 ("countdown timer for scheduled lights"; the LanternHub timeUntil chip is now computed from real data).

Privacy model (decided up front) โ€‹

Aggregate counts only. Scheduled lights reveal future whereabouts, which is more sensitive than live presence, so:

  • scheduledLanterns docs are owner-readable only (firestore.rules); all writes go through the lanterns-api (Admin SDK) so per-user limits and counters can't be bypassed client-side.
  • Venues carry a scheduledLanternCount aggregate, so discovery surfaces ("3 lights scheduled here") can be built without exposing identities. Frens-visible identities would be a deliberate, separate decision.
  • No user coordinates at schedule time; the activated lantern uses venue coords truncated to ~111 m (same floor as Sprint A lantern writes).

What shipped โ€‹

LayerChange
services/api/lanterns/src/services/schedule.service.jsNew service: create (30-day window, max 5 pending/user, ยฑ2h dupe check per venue), list, cancel, and the activation sweep
services/api/lanterns/src/routes/schedule.js501 stubs โ†’ real POST/GET/DELETE with zod validation, schedule rate-limit bucket, forge events (lantern_schedule_created / _cancelled)
services/api/lanterns/src/routes/cleanup.jsPOST /cleanup/activate-scheduled (scheduler/admin auth): activates due lights, defers if the owner has an active lantern, marks >30 min stale as missed
services/api/lanterns/src/middleware/errorHandler.jsNow honors service-layer err.status (4xx); previously geofence 403s / venue 404s all surfaced as 500s
firestore.rules / firestore.indexes.jsonOwner-only read rules; composite indexes for the list query and the sweep
apps/web/src/lib/scheduledLightService.jsAPI client (writes) + Firestore listener (reads) + display formatters
apps/web/src/screens/dashboard/Dashboard.jsxupcomingLights now synced from Firestore; schedule/edit/cancel call the API
Proxy plumbing/api/lanterns added to Vite dev proxy, _worker.js, .env.local.example (LANTERNS_API_ORIGIN, VITE_LANTERNS_API_URL)
OpenAPISchedule + activate-scheduled documented with real schemas (lanterns is openapi-sync enforced)

Activated lanterns carry source: 'scheduled' and scheduledLanternId so the UI can distinguish auto-lit lanterns. Profile interests stay client-side encrypted, so unlike interactive lights they can't be denormalized at activation; only the vibe/interest captured at schedule time travel onto the lantern.

Post-deploy step (dev): arm the activation cron โ€‹

The sweep needs a tighter cadence than the TTL cleanup so lights come on near their scheduled minute. After lanterns-api deploys:

bash
gcloud scheduler jobs create http lanterns-activate-scheduled \
  --location=us-central1 --project=lantern-app-dev \
  --schedule="*/2 * * * *" \
  --uri="https://lanterns-api-531553779372.us-central1.run.app/cleanup/activate-scheduled" \
  --http-method=POST \
  --headers="Authorization=Bearer ${SCHEDULER_SECRET}"

(Same SCHEDULER_SECRET bearer model as the /cleanup/expired job; fail-closed if the secret is unset.)

Deliberately out of scope (follow-ups) โ€‹

  • Map visualization of scheduledLanternCount (badge on venue markers / venue pages): data is maintained, UI not built.
  • Notifications when a scheduled light activates ("You'll get a notification when it's live" in the confirmation copy is still aspirational).
  • Recurring schedules, frens-visible scheduled lights, geofence-gated activation (only light if the user is actually at the venue).

Built with VitePress