Skip to content

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 ask panel 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 lanterns collection. 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 activeLanternCount descending. Note that nothing is lit on dev right now, so it ranks the list rather than filtering it: a filter of > 0 would currently return an empty table.

A timestamp filter needs a full ISO-8601 string. โ€‹

  • 2026-08-01T00:00:00Z, with the Z or 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-01 is 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. โ€‹

  • venues is where lanterns get lit, offers is what merchants are running, merchantProfiles is account counts and statuses only, and featureRequests is 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 are ENABLE_* 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.
  • users carries 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: merchantProfiles once 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 id column and some do not, and that is a decision rather than an oversight.
  • venues, offers and featureRequests are keyed by generated document ids, which name a row. merchantProfiles is 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 Saved panel 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? โ€‹

LimitValueWhy
Rows per query500, and 50 by defaultServer-enforced. The caller cannot raise it, and there is no cursor or offset
Filters per query10Matches what Firestore will accept before the query stops being cheap
Values in in, not-in, array-contains-any30Firestore'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-query on analytics-api, with the allowlist served from GET /analytics/admin/firestore-schema.
  • The allowlist itself is FIRESTORE_COLLECTION_POLICY in services/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/.

Built with VitePress