Indexes

Firestore indexes every field of every document automatically, so single-field queries work without any setup. Composite indexes are the exception: as soon as a query filters or orders on more than one field, Firestore needs an index that covers that exact combination, and it will not create one for you.

Why composite indexes are easy to get wrong

The usual workflow is accidental. You write a query, it throws FAILED_PRECONDITION: The query requires an index, you click the console link in the error, and Firebase creates the index for you. It works — but the index now exists only in the Firebase Console. It is not in your repository, not in code review, and not in your teammate's emulator. The first sign that it is missing from staging is usually a query failing there.

Declaring indexes on the model instead keeps them next to the queries that need them, and puts them under version control with everything else.

Declaring an index

Stack one @Index decorator per composite index on the collection. Field order matters and must match the order your query uses.

typescript
@Collection('posts')
@Index(['userId', 'createdAt'])
@Index(['status', 'publishedAt'], { order: 'DESCENDING' })
class Post extends BaseModel {
  // ...
}

// Generates firestore.indexes.json

The first index supports queries that filter on userId and order by createdAt — a per-user timeline, for example. The second adds order: 'DESCENDING', which matters because Firestore treats ascending and descending as different indexes. A newest-first feed needs the descending one.

Generating and deploying

Run the generator to turn the decorators into firestore.indexes.json, then deploy it with the Firebase CLI like any other config file.

bash
firemap generate:indexes

firebase deploy --only firestore:indexes

Because the file is generated, the schema stays the single source of truth: a reviewer can see a new index appear in the same pull request as the query that introduced it. Note that large collections take time to backfill after deployment — Firestore builds the index before queries against it will succeed.