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:
scheduledLanternsdocs 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
scheduledLanternCountaggregate, 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 โ
| Layer | Change |
|---|---|
services/api/lanterns/src/services/schedule.service.js | New 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.js | 501 stubs โ real POST/GET/DELETE with zod validation, schedule rate-limit bucket, forge events (lantern_schedule_created / _cancelled) |
services/api/lanterns/src/routes/cleanup.js | POST /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.js | Now honors service-layer err.status (4xx); previously geofence 403s / venue 404s all surfaced as 500s |
firestore.rules / firestore.indexes.json | Owner-only read rules; composite indexes for the list query and the sweep |
apps/web/src/lib/scheduledLightService.js | API client (writes) + Firestore listener (reads) + display formatters |
apps/web/src/screens/dashboard/Dashboard.jsx | upcomingLights 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) |
| OpenAPI | Schedule + 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:
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).