Tools for the Firebase Firestore console: a search box on every list, a filter for the fields of an open document, and a Copy JSON button that copies a whole document without you expanding it first.
Firepanel is an independent project. It is not affiliated with, endorsed by, or sponsored by Google. Firebase and Firestore are trademarks of Google LLC.
It is read-only by construction. It holds one permission (clipboardWrite),
runs only on console.firebase.google.com, talks to no network of its own, and
never clicks a control that could add, edit or delete data — see
Safety.
Every list panel gets a filter box at the top — collections, documents inside a
collection, and the subcollections of a document. Type to filter that list,
click a result to open it, Esc to clear.
The console renders these lists through a virtual scroller, so only the rows on screen exist in the DOM. Names are collected as rows render, and the first time you type the extension scrolls the list once (time-boxed to 3s) to pick up the rest. If that ran out of time, the results end with scan further — a 15s sweep you trigger yourself, so a huge collection is never paged through automatically.
A collection can hold more documents than the console will ever page into the
list, and Firestore cannot search document IDs by substring — no client-side
trick changes that. So the documents box does two things: it filters the IDs
loaded so far, and it offers Open "" (or just press Enter) to navigate
straight to that exact ID, whether or not the list ever reached it.
The direct-open URL is derived from the panel's position in the panel chain, which mirrors the data path — panel 1 lists the documents of path segment 0, and so on. It is offered for documents only; a collections panel's place in that chain is ambiguous.
Typing a partial ID offers Search IDs starting with "…" (or press Enter).
That runs server-side over the entire collection — not over what the list
happened to load — and returns instantly regardless of collection size.
It works by building the console's own query-builder URL. The query builder
accepts a Document ID condition, which it encodes in the URL as the reserved
__name__ field, so the extension writes that URL and opens it in a new tab —
the panel you were browsing stays where it was:
?view=query-view
&query=2|LIM|2/50|WH|1|8/__name__|GTE|STR|3/p60
&scopeType=collection&scopeName=orders
<clauses>|LIM|<len>/<limit>|WH|<conditions>|<len>/<field>|<op>|<type>|<len>/<value>,
every value prefixed with its length. src/query-view.ts builds it; that file
is the only thing to change if the console's encoding moves.
The condition is >= with a limit of 50, so results start at your prefix and
run on past it in ID order — the matches are the ones at the top. The prefix is
case-sensitive, because it is a range over byte order.
The plain filter box still filters the IDs already loaded, case-insensitively,
which is faster when what you want is on screen — matches show the matched span
highlighted. Shift+Enter, or the Open row, opens the typed ID as an exact
document, also in a new tab.
The data view stops serving rows into a document list after a few hundred (~600
in the collections we tried), no matter how far you scroll — a console limit, not a Firestore
one. Neither can the panel's funnel filter reach document IDs: it validates
against real field names and rejects __name__. Hence the query-builder URL,
which is subject to neither.
An open document gets a Filter fields by key or value box above its field
list. Type part of a key or of a leaf value; everything that neither matches nor
contains a match is hidden. A match keeps its whole value on screen, so matching
a map shows what is inside it. Esc clears.
The count on the right reports the two kinds separately — 3 keys · 2 values —
because a value hit shows a row whose key does not contain what you typed, and
an unlabelled total makes that look like a bug.
Only leaf fields hold a value: a map's value is its children, so a map is never matched on its contents directly, though a match inside one still surfaces it as the parent.
Nested keys and values only exist in the DOM once their branch is open, so the first search expands the document — the same read-only expansion the copy button uses. That means the first keystroke on a large document takes a moment, and the document stays expanded afterwards.
The filter is display-only: Copy JSON always copies the full document, not the filtered view.
Open a document and click Copy JSON in the breadcrumb bar. Every collapsed map and array is expanded first, so the copied JSON is the complete document — not just the branches you happened to have open.
{
"category": "UTILITY",
"config": { "retries": 3, "targets": ["sms", "whatsapp"] },
"createdAt": "August 27, 2026 at 7:15:00 AM UTC+5:30"
}Timestamps, geopoints and references come out as the string the console
displays, because that is all the rendered DOM holds. String fields containing
stringified JSON are unwrapped for readability — flip PARSE_STRINGIFIED_JSON
in src/document-parser.ts if you want the raw string.
The reason this is a frontend-only extension is that it must not be able to change anything. Two rules hold that up:
- No credentials, no API calls, no background page. It reads the DOM the console has already rendered.
- Expanding a document only ever clicks a disclosure control: an element with
aria-expanded="false", or, when the console ships no ARIA, an element sitting to the left of the field key (where the console puts the chevron and never puts a row action). Anything labelled edit / delete / add / copy is excluded, and a field whose disclosure cannot be identified is skipped and reported — the button then readsCopied (N not expanded).
From the Chrome Web Store (recommended — it updates itself):
Listing link goes here once the first review has passed.
Or from a release: download firepanel.zip from
Releases, extract
it to a folder you will keep, then chrome://extensions/ → Developer mode
on → Load unpacked → select that folder. Chrome reads the folder from disk
on every start, so it has to stay put.
npm i
npm test # unit tests
npm run typecheck
npm run package # → firepanel.zip, and dist/ to load unpackeddist/manifest.json is generated from package.json at build time, so the
version, name and description have one source of truth and the store's field
limits are checked before submission rather than after.
To release: bump the version in package.json, tag it vX.Y.Z and push the
tag. CI checks that the tag matches, runs the tests, builds the zip and creates
the GitHub release. Nothing is built by hand.
| File | Role |
|---|---|
src/console-dom.ts |
Every console selector, in one place |
src/list-filter.ts |
PanelListFilter per list, PanelFilterManager reconciles them |
src/field-filter.ts |
Key filter for an open document's fields |
src/document-expander.ts |
Opens collapsed maps/arrays before a copy or field search |
src/document-parser.ts |
Fields DOM → plain object |
src/copy-button.ts |
The breadcrumb button and clipboard write |
src/console-watcher.ts |
Re-injects across SPA navigation, ignores our own mutations |
src/theme.ts |
Resolves colours from what the console actually paints, light or dark |
The console has light and dark themes and marks neither on the elements we
inject into. Rather than assume, src/theme.ts reads the first painted
ancestor behind an injected element and uses that as its background, decides
light-vs-dark from the page's own luminance, and picks the matching accent
(#1a73e8 light, #8ab4f8 dark). Hovers, borders and dividers are neutral
greys so they read on either. Flipping the theme with the page open remounts
everything, so nothing keeps stale colours.
Firebase's markup is not a public contract, so a console release can break the
selectors. Everything the extension matches on lives in src/console-dom.ts,
and the expander is the piece most likely to drift. Open a document with a
collapsed map, run window.__fctDebug() in DevTools, and it prints the real
markup of one collapsed field's click target — enough to pin the selector.
Firepanel collects nothing, stores nothing and sends nothing anywhere. The full policy is in PRIVACY.md.
MIT.
Prior art: the collection-filter approach (filtering a virtual-scrolled list via
a harvested name cache and an overlay) is taken from
xorrier/copy-fb-doc by Amrit Sharma.