TTL Cleanup API + Client-Side Freshness โ
Date: 2026-03-26 Status: Approved
Problem โ
Three Firestore collections (lanterns, waves, connections) have TTL-based expiry but no reliable server-side enforcement:
- Lanterns (2hr TTL): Existing Cloud Function runs every 15 minutes, leaving a staleness window. Venue lantern data is fetched once (no real-time listener), so users who close and reopen the app see stale lanterns until they navigate away and back.
- Waves (1hr TTL): No server-side cleanup at all. Expired waves with
status: 'pending'block re-waving because the sender lacks Firestore write permission to expire them. - Connections (4hr TTL): No server-side cleanup. Same stale-document problem.
Solution โ
Two changes:
- Cleanup API endpoint in the lanterns API service โ callable by Cloud Scheduler on a cron, expires stale documents across all three collections.
- Client-side re-fetch on
visibilitychangeโ when the app returns to foreground, re-fetch venue lanterns so users always see fresh data.
Cleanup API Endpoint โ
Route โ
POST /cleanup/expired in services/api/lanterns/
Authentication โ
No verifyFirebaseToken in the middleware chain. Auth handled inside the route handler:
- Cloud Scheduler:
X-CloudScheduler-JobNameheader present - Admin manual trigger:
req.user?.role === 'admin'
Follows the established pattern in services/api/analytics/src/routes/scheduled.js.
Service Layer โ
New file: services/api/lanterns/src/services/cleanup.service.js
Three independent functions:
cleanupExpiredLanterns() โ
- Query:
lanternswherestatus == 'active'ANDexpiresAt <= now - Action: Batch-update to
status: 'extinguished',extinguishReason: 'expired',extinguishedAt: now - Post-action: Reconcile
activeLanternCounton affected venues by counting actual remaining active lanterns (same approach as existing Cloud Function) - Returns:
{ cleaned: number, venuesAffected: number }
cleanupExpiredWaves() โ
- Query:
waveswherestatus == 'pending'ANDexpiresAt <= now - Action: Batch-update to
status: 'expired' - Returns:
{ cleaned: number }
cleanupExpiredConnections() โ
- Query:
connectionswherestatus == 'active'ANDexpiresAt <= now - Action: Batch-update to
status: 'expired' - Returns:
{ cleaned: number }
Route Handler โ
Calls all three cleanup functions, tracks analytics per collection via forge.track(), returns combined results.
Response Shape โ
{
"success": true,
"lanterns": { "cleaned": 5, "venuesAffected": 2 },
"waves": { "cleaned": 12 },
"connections": { "cleaned": 3 }
}Registration โ
In services/api/lanterns/src/index.js:
import cleanupRoutes from './routes/cleanup.js'
// No verifyFirebaseToken โ auth handled internally
app.use('/cleanup', cleanupRoutes)OpenAPI โ
Add the /cleanup/expired endpoint to services/api/lanterns/openapi.json.
Client-Side Re-fetch โ
Location โ
apps/web/src/screens/dashboard/Dashboard.jsx
Behavior โ
Add a visibilitychange event listener. When document.visibilityState becomes 'visible':
- If a venue is currently selected (
selectedVenueis set), re-fetch lanterns viagetVenueLanterns()and update displayed data - Skip if last fetch was < 30 seconds ago (avoid hammering Firestore on rapid tab switches)
Formatting โ
Reuse the same lantern formatting logic already in handleSelectVenue.
Cloud Function Deprecation โ
The existing cleanupExpiredLanterns Cloud Function in services/functions/firebase/modules/lanternCleanup.js is superseded by the new API endpoint. Mark it as deprecated with a comment pointing to the new endpoint. Remove in a follow-up once the API endpoint is deployed and Cloud Scheduler is configured.
Files to Create/Modify โ
New Files โ
services/api/lanterns/src/services/cleanup.service.jsโ cleanup service functionsservices/api/lanterns/src/routes/cleanup.jsโ route handler
Modified Files โ
services/api/lanterns/src/index.jsโ register cleanup routes (no auth middleware)services/api/lanterns/openapi.jsonโ add/cleanup/expiredendpointapps/web/src/screens/dashboard/Dashboard.jsxโ addvisibilitychangelistener for venue re-fetchservices/functions/firebase/modules/lanternCleanup.jsโ add deprecation comment