Query Console โ
The place you go to ask the data a question. Two sources sit behind one page, and picking the right one is most of the job.
Which source do I use? โ
BigQuery answers what HAPPENED. Firestore answers what IS. โ
- An event that fired last Tuesday, a count across a month, a trend: that is BigQuery. It holds the event stream and you query it with SQL.
- An offer sitting in the queue right now, a venue's live lantern count, how many merchant accounts are mid-setup: that is Firestore. It holds current state and you query it by picking a collection, some filters, and an order.
The quick test: does your question have a time range in it? โ
- "How many lanterns were lit in July" has a range, so it is BigQuery.
- "How many lanterns are lit right now" does not, so it is Firestore.
- If you catch yourself wanting to count rows across days, you are in the wrong tab.
How do I query Firestore? โ
Firestore has no query language, so there is no box to type into. โ
- You build the query instead: a collection, up to ten filters, an optional ordering, and a limit. That is the whole grammar, and it is what the builder puts on screen.
- This is a property of Firestore, not a simplification. There is no SQL to fall back to and no way to join two collections, so the console cannot offer either.
Start from a question in the rail, not from the dropdown. โ
- The
What you can askpanel holds real questions, grouped by collection and collapsed until you open one. Clicking a question FILLS THE BUILDER IN, so you get a working query you can then edit rather than a blank form. - Use the filter box to find one by wording. A matching group opens itself, so nothing hides behind a collapsed chevron.
- Once you are building by hand, each dropdown option carries a line saying what that collection or field answers, so the explanation sits where the choice is made.
A worked example: how many lanterns are lit at a venue? โ
- The instinct is to look for a
lanternscollection. There isn't one, and there deliberately never will be (see below). - The field you want is
venues.activeLanternCount, which is the live count for each venue. - The rail has this one ready as "Which venues are busiest right now?", which orders by
activeLanternCountdescending. Note that nothing is lit on dev right now, so it ranks the list rather than filtering it: a filter of> 0would currently return an empty table.
A timestamp filter needs a full ISO-8601 string. โ
2026-08-01T00:00:00Z, with theZor an offset like-08:00. The zone is required, because without it the same filter means different instants on different machines.- A bare date like
2026-08-01is treated as ordinary text and will match nothing, silently. A confident empty table is the worst failure available here, which is why the zone is enforced rather than guessed.
A compound filter may need an index, and the error tells you how to make one. โ
- Filtering on one field while ordering by another can need a composite index that does not exist yet.
- Firestore's refusal carries a create-index link, and the console renders it as a link rather than burying it. Clicking it provisions the exact index the query needs.
Why can I not read some collections? โ
Four collections are readable: venues, offers, merchantProfiles and featureRequests. โ
venuesis where lanterns get lit,offersis what merchants are running,merchantProfilesis account counts and statuses only, andfeatureRequestsis the in-app "request a feature" flow (each one auto-files a GitHub issue when it lands).- There used to be a fifth,
featureFlags. It was removed on 2026-08-27 for having zero documents and zero writers anywhere in the codebase: feature flags here areENABLE_*constants in source plus environment variables, not Firestore rows.
users, lanterns, waves and adminProfiles are refused on purpose, and the console says so. โ
- They are listed in the rail under
Not readable, on purpose, each with its reason, so an absent collection never has to be mistaken for a missing feature. - Lanterns and waves ARE the record of who was where, when, and who reached out to whom. Minimizing how long that is kept, and who can read it, is the point of the privacy architecture rather than a gap in this tool.
userscarries sealed-identity material, including the phone hash that the whole design exists to keep unlinkable.
Within a readable collection, the fields are enumerated rather than the collection waved through. โ
- The console can only return fields that were reviewed one at a time. A field added to a document later does not start appearing here until somebody adds it to the allowlist.
- The reason is concrete:
merchantProfilesonce carried an admin password hash under a collection that had already been approved. Approving a collection wholesale approves everything anyone adds to it afterwards.
A document id is reviewed the same way. โ
- Some collections show an
idcolumn and some do not, and that is a decision rather than an oversight. venues,offersandfeatureRequestsare keyed by generated document ids, which name a row.merchantProfilesis keyed by an account id, which names a person, so its key is withheld.
What can I save? โ
A saved inquiry keeps the structured query, not a string. โ
- Press Save on the Firestore tab and the collection, filters, ordering and limit are stored as an object. Re-open it from the rail's
Savedpanel and the builder comes back exactly as you left it. - Loading a saved inquiry replaces the whole query rather than merging it, because the readable field list is per collection and a leftover field would simply be refused.
BigQuery and Firestore keep separate saved lists. โ
- They read the same store, filtered by source. A saved query written before the Firestore source existed is a BigQuery one.
What are the limits? โ
| Limit | Value | Why |
|---|---|---|
| Rows per query | 500, and 50 by default | Server-enforced. The caller cannot raise it, and there is no cursor or offset |
| Filters per query | 10 | Matches what Firestore will accept before the query stops being cheap |
Values in in, not-in, array-contains-any | 30 | Firestore's own cap on those operators |
- The console is read-only. There is no write path, and there is no way to join across collections, because Firestore cannot do either.
Where does this live? โ
- The endpoint is
POST /analytics/admin/firestore-queryon analytics-api, with the allowlist served fromGET /analytics/admin/firestore-schema. - The allowlist itself is
FIRESTORE_COLLECTION_POLICYinservices/api/analytics/src/services/firestoreQuery.service.js. Adding a collection or a field means editing that map, which is deliberately the only way in. - Project notes, including the design verification and the open backlog, are in
docs/projects/query-console/.