Using Firestore JS client SDK as an admin client (bypass rules)

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:

Why not use 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.

Cloudflare Workers

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.)

Tauri

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.

Alternative options

So, no firebase-admin on Cloudflare Workers or in a Tauri app. What do we do then?

Rest API

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.

Third-party SDKs

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. 🙃

Client SDK with admin permissions

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%.

Wrapping up

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. 😂


  1. 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. ✌️ ↩︎

Want to leave a comment?

Start a conversation on X or send me an email! 💌
This post helped you? Buy me a coffee! 🍻