Firestore Indexes and TTL Policies

The composite indexes and TTL policies the app needs.

Both are defined in firestore.indexes.json and deploy together, on every push to main or by hand:

firebase deploy --only firestore:indexes

Composite indexes

Firestore requires composite indexes for queries that filter on multiple fields. Each index is a sorted table covering the fields in order, allowing O(1) lookups instead of O(n) collection scans. Without them, those queries fail with a missing index error. When one is missing, the browser error also links to a page that creates it.

The emulators don't enforce them, so a query missing from firestore.indexes.json works under npm run dev but fails in production.

The app currently requires 8 composite indexes:

CollectionFieldsPurpose
attendees[emailSent, createdAt]Admin tickets panel: query unsent attendees for bulk ticket-email send
badgeActivationCodes[badgeId, createdAt desc]Admin panel: load codes for a badge
records[type, timestamp desc]Activity log by type
records[performedBy, timestamp desc]Activity log by user
records[type, performedBy, timestamp desc]Activity log filtered by type and user
upcomingEvents[published, startAt]Public listing of published upcoming events, sorted by start time
users[group, joinedAt desc]Admin panel: list users by group, newest first
users[group, membershipExpiresAt desc]Admin panel: list current members in a group, latest expiry first

TTL policies

The app relies on TTL-based auto-expiry for rate limiting, audit-log retention, deferred deletion of users/events/badges, cleanup of expired claim/activation codes, scan-event retention, and per-client scan quotas. Ten TTL policies are required:

CollectionFieldPurpose
rateLimitsexpiresAtClean up rate-limit windows
recordsexpiresAtAudit-log retention (30 days)
claimCodesexpiresAtAuto-expire event claim codes past their active window
badgeActivationCodesexpiresAtAuto-expire badge activation codes past their active window
usersdeleteAt48h cooldown for account deletion
pastEventsdeleteAt48h cooldown for past-event deletion
upcomingEventsdeleteAt48h cooldown for upcoming-event deletion
badgesdeleteAt48h cooldown for badge deletion
scansexpiresAtScan-event retention (365 days) for QR codes
scanQuotaexpiresAtPer-client scan-quota windows (two windows' grace)

All ten are defined as fieldOverrides with "ttl": true. Each override also lists the field's ascending, descending, and array-contains indexes. An override replaces a field's default indexes rather than adding to them, so leaving those out would drop indexing on the field and break queries such as the admin Users tab's where('deleteAt', '!=', null). Keep them when adding a new TTL field.

Note

Firestore TTL requires the Blaze plan (pay-as-you-go). It is not available on the free Spark plan.

Without these policies, rate-limit entries and audit logs accumulate indefinitely, and deletion cooldowns will never fire the cleanup triggers.

On this page