Skip to content

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:

  1. Cleanup API endpoint in the lanterns API service โ€” callable by Cloud Scheduler on a cron, expires stale documents across all three collections.
  2. 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-JobName header 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: lanterns where status == 'active' AND expiresAt <= now
  • Action: Batch-update to status: 'extinguished', extinguishReason: 'expired', extinguishedAt: now
  • Post-action: Reconcile activeLanternCount on affected venues by counting actual remaining active lanterns (same approach as existing Cloud Function)
  • Returns: { cleaned: number, venuesAffected: number }

cleanupExpiredWaves() โ€‹

  • Query: waves where status == 'pending' AND expiresAt <= now
  • Action: Batch-update to status: 'expired'
  • Returns: { cleaned: number }

cleanupExpiredConnections() โ€‹

  • Query: connections where status == 'active' AND expiresAt <= 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 โ€‹

json
{
  "success": true,
  "lanterns": { "cleaned": 5, "venuesAffected": 2 },
  "waves": { "cleaned": 12 },
  "connections": { "cleaned": 3 }
}

Registration โ€‹

In services/api/lanterns/src/index.js:

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 (selectedVenue is set), re-fetch lanterns via getVenueLanterns() 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 functions
  • services/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/expired endpoint
  • apps/web/src/screens/dashboard/Dashboard.jsx โ€” add visibilitychange listener for venue re-fetch
  • services/functions/firebase/modules/lanternCleanup.js โ€” add deprecation comment

Built with VitePress