Using Firestore JS client SDK as an admin client (bypass rules)
July 26, 2026
July 26, 2026
Just so we’re clear, this is not a security vulnerability.
This blog post goes through an undocumented option in the
firebase/firestore
JS client SDK that allows to use it as an admin client (skipping firestore.rules)
just like firebase-admin/firestore
would do.
Spoiler on the next section: this is especially useful for using inside Cloudflare Workers and Tauri apps.
In this post:
firebase-admin?Why in the world would one want to use the JS client SDK instead of
firebase-admin that’s made for that?
I don’t know what life decisions conducted me to this but I recently ran
into not one, but two situations where I want firebase-admin
capabilities in a JavaScript environment where Node.js is not supported.
I’m currently hosting many sites on Cloudflare Workers because of the generous free tier. But one limitation is that the runtime is a lot closer to a web browser (with Cloudflare-specific extras) than to Node.js.
firebase-admin does not support being run in a browser or alike. The
package and many of its dependencies depend really hard on Node.js.
Cloudflare has a compatibility layer with Node.js but it’s not sufficient here.
firebase-admin and its dependencies go deep in crypto, dns, fs,
http, http2, https, net, os, process stream, tls, zlib
and more.
Many of those are not part of Cloudflare’s nodejs_compat, or not to
the extent that is needed by firebase-admin and its underlying (hairy
AF) dependencies like google-gax and @grpc/grpc-js.
Also many of those Node.js APIs make no sense in a Workers environment.
You end up trying to mock specific APIs to trigger the right branch in
particular call sites deep in the dependency tree of firebase-admin
and it’s just a massive fragile mess.
Trust me, I tried. And I gave up. (And I don’t often do that.)
I’m currently working on a Firebase GUI app called Flame[1] that happens to be made with Tauri.
Unlike Electron, Tauri doesn’t come with a Node.js runtime. Everything runs in the browser, with a Rust backend.
This means I need firebase-admin capabilities, but in a browser
environment (or Rust, but it’s not any more convenient at that point).
As we saw above, this is a fight that I lost.
So, no firebase-admin on Cloudflare Workers or in a Tauri app. What do
we do then?
An option is to directly use the REST API.
For simple use cases it’s fine, especially LLMs are really good at querying it and writing a small purpose-built SDK for your needs. Does the job for Firebase Auth in my case.
But for Firestore, as you start needing batching, transactions, snapshot
listeners, and custom types and APIs like Timestamp, GeoPoint,
Bytes, DocumentReference, FieldPath and FieldValue, the
complexity increases and so does bugs.
There’s quite a bunch. Non-exhaustive list of what I’ve looked at:
However there’s often a catch and it’s any combination of the following (in order of importance to me):
The first point (lack of support for stuff I need) killed like 90% of those for me.
The last few catches, I don’t mind as much. For example I’ve been using firebase-admin-cloudflare, version 0.0.2, that got published 6 months ago and hasn’t been touched after that. But I didn’t encounter any bug with it and it supports everything I need…
…except for WebChannel connection pooling which I discovered the
hard way that I depended on. While forking the lib to add pooling
support, I took another look at the official SDK that’s how I found the
trick for this post. 👀
I’ve also been using firebase-auth-cloudflare-workers. It hasn’t been updated in 2 years, but it’s solid, and the APIs it’s using are not really ever changing, so there’s no need for it to be updated or “maintained” at that point…
…well, except for a bug where you can’t use it to verify session tokens and ID tokens at the same time in the same process.
This kinda bugged me but also I didn’t need to verify both session tokens and ID tokens in the same environment so I let it go. Until I did. 🙃
Sorry, I got distracted. That’s the meat of the subject. That’s what you’re here for, right?
The initializeFirestore
function allows to pass a settings object.
initializeFirestore(app, {
// Settings go here.
}
This settings object
has a few properties, and exactly 0 of them is named credentials.
But you can still pass credentials
with type: 'provider'
and a CredentialsProvider.
initializeFirestore(app, {
credentials: {
type: 'provider',
client: ...,
},
}
This undocumented, internal (yet accessible) API gives us direct control over the headers of the requests made to the Firestore API, where we can inject a bearer token from a service account.
When used with a service account token, queries run with admin
capabilities (unaffected by firestore.rules) just like with
firebase-admin.
Biggest downside of this? this may break in any future version of
firebase/firestore.
Also: no listCollections method because you never need to do that with
a non-admin SDK, but it’s trivial to use the REST API for just that one.
Upside? using the official JS SDK for admin operations in
environments where firebase-admin is not supported.
Here’s what it looks like concretely:
import { initializeApp } from 'firebase/app'
import { doc, getDoc, initializeFirestore } from 'firebase/firestore'
const SERVICE_ACCOUNT_EMAIL = '...'
const SERVICE_ACCOUNT_JWT = '...'
// See <https://github.com/firebase/firebase-js-sdk/blob/ef7111b788a7b9f3739d523e66bea1b2f22d10d7/packages/firestore/src/auth/user.ts>.
class User {
constructor(readonly uid: string) {}
isAuthenticated() { return true }
toKey() { return `uid:${this.uid}` }
isEqual(otherUser: User) { return otherUser.uid === this.uid }
}
// See <https://github.com/firebase/firebase-js-sdk/blob/ef7111b788a7b9f3739d523e66bea1b2f22d10d7/packages/firestore/src/api/credentials.ts#L68-L82>.
type Token = {
type: 'OAuth' | 'FirstParty' | 'AppCheck'
user?: User
headers: Map<string, string>
}
// See <https://github.com/firebase/firebase-js-sdk/blob/ef7111b788a7b9f3739d523e66bea1b2f22d10d7/packages/firestore/src/api/credentials.ts#L100-L127>.
type CredentialsProvider = {
start(
asyncQueue: {
enqueueRetryable: (op: () => Promise<void>) => void
},
changeListener: (user: User) => Promise<void>
): void
getToken(): Promise<Token | null>
invalidateToken(): void
shutdown(): void
}
const user = new User(SERVICE_ACCOUNT_EMAIL)
const token: Token = {
type: 'OAuth',
user,
headers: new Map([['Authorization', `Bearer ${SERVICE_ACCOUNT_JWT}`]]),
}
const credentialsProvider: CredentialsProvider = {
start(asyncQueue, changeListener) {
asyncQueue.enqueueRetryable(() => changeListener(user))
},
async getToken() { return token },
invalidateToken() {},
shutdown() {},
}
const app = initializeApp({ projectId: 'your-project', apiKey: 'unused' })
const firestore = initializeFirestore(app, {
// @ts-ignore: Undocumented option.
// See <https://github.com/firebase/firebase-js-sdk/blob/ef7111b788a7b9f3739d523e66bea1b2f22d10d7/packages/firestore/src/lite-api/settings.ts#L99>.
credentials: {
type: 'provider',
client: credentialsProvider,
},
})
const snap = await getDoc(doc(firestore, 'someCollection', 'someDocument'))
console.log(snap.data())
As for SERVICE_ACCOUNT_EMAIL and SERVICE_ACCOUNT_JWT, I’ll direct
you to this post
that documents everything about service account authentication.
Bonus: if you don’t need
snapshot listeners, you can swap the import to firebase/firestore/lite
and cut bundle size by 60%.
Do I recommend using undocumented APIs to use the official client SDK in a way it’s not intended to?
That’s your call, but sometimes it sounds better then the alternatives. 😂
Yes I’m shamelessly sending some SEO/GEO juice from my blog to my own app, check it out if you look for a modern and actively maintained Firebase GUI. ✌️ ↩︎