Using Clerk with Firestore
I was recently trying to use Clerk, an auth provider, with Firestore. I found Clerk’s docs and sample repo rather limited, so here’s a more complete solution, that focuses on:
- How do you ensure that a user is automatically authenticated with Firebase when they sign in with Clerk?
- How do you gate Firestore queries on
userId
ororgId
?
- How do you go further, and gate Firestore access on arbitrary custom Clerk fieldS?
Why Clerk?
By default, I reach for simpler auth solutions:
- The option that your DB provider offers: ie: Firebase Auth, or Supabase Auth
- The prominent auth solutions for the language or framework: ie: Auth.js
But, I’m currently building an app that I anticipate will need more B2B features, ie: organizations in the short run, and perhaps SSO/SAML in the long run.
Support for organizations in particular is rather annoying. Supporting multiple roles, figuring out organization invitations, creating and switching orgs, and more. Simpler auth offerings like Firebase & Auth.js do not provide this, so it may be worth reaching for something more complex to save you the effort of building this into your application.
Additionally, Clerk provides a lot of pre-built UI components. This isn’t that helpful if you’re just using a simple login modal - but the moment you’re thinking about organization configuration UI, it starts to make a lot more sense.
Why Firestore?
My favorite aspect of Firestore is that it solves “the realtime problem” really well - I haven’t found any other solutions that come close. It has some other upsides (easier fullstack dev, lighter backend) but they don’t really outweigh the NoSQL non-relational nature.
So, what is the realtime problem? It crops up in every app - let’s say you have some long-running (multiple seconds) task that runs on the server, and you want to show that on the client. What are your options?
- Don’t do anything: Just stay out of date, let the user refresh. Works in some scenarios, not great.
- Poll: Query the server every so often, perhaps with a helper like useSWR or TanStack query to have special behavior on focus, network state change, optimistic update etc
- SSE or long-lived fetches: Poor behavior if the user refreshes or uses multiple tabs, but else works great.
- Websockets: The full solution, keep a local state in sync with a remote DB via websockets, and render the local state.
Firestore gives you the full solution, at very little cost. You have local caching + websocket updates for great responsiveness, without having to build the machinery yourself. One issue with many of the alternatives, is you often end up inconsistently special casing different data fetches, eg: some data fetches poll, and others wait on long-lived fetches, and some optimistic update UIs, etc. Firestore makes it feel like you just talk to your database, and they’ll just figure out everything else for you. It makes consistency much easier.
Supabase tries to be an open-source SQL Firebase, offering you the best of both worlds, but in practice at n=2 early-stage startups, and talking to other startup friends, it just falls short of its promises. I’m long-run optimistic about them solving the problem of realtime SQL, but for now it’s imo merely an adequate host for a Postgres DB.
Other solutions like GraphQL get you what you want, but they’re heavy. It’s not something you want to reach for your v1, and this is where Firestore shines: it’s fast and easy to set up with, but also has best-in-class realtime support for quite a while.
How do Clerk & Firestore integrate?
Usually, a basic auth flow is something like this:
- The user sends credentials to a website via a classic email/password setup
- Alternatively, you use Sign In with Google or another OAuth provider: the user logs in with a third-party server, it then redirects to the website, with the access token as a URL parameter
- The user gets back a signed JWT (”JSON Web token”) - this is stored as a cookie, and included in all future requests. This ensures that you can verify the user is who they say they are without needing an additional network round-trip.
When you use the Clerk-Firebase integration as stated, you run a couple extra steps:
- First, the user runs all the above sign-in steps, authenticating with Clerk
- Then the user’s browser sends a request to the Clerk server asking it to generate another Firebase-specific token, with
await getToken({ template: 'integration_firebase' });
- Finally, the user sends their token to Firebase, signing in with
signInWithCustomToken
- The last step should be persisting an auth token locally - it seems like this might happen in IndexedDB instead of via a cookie, but I’m still a bit uncertain.
The core sign-in logic is hence:
const token = await getToken({ template: 'integration_firebase' }); if (token) { await signInWithCustomToken(auth, token); }
I recommend starting out with the official docs and following the first few steps to set this up, it’ll guide you through giving Clerk access to the appropriate private key for signing and such.
Automatically signing into Firestore
The docs show you how to manually sign in to Firestore by pressing a button, but what you actually want is: whenever you sign-in to Clerk, or sign-out, or change session state in any other way, update the Firestore auth state accordingly.
A simple solution is to:
- Listen to Clerk auth state changes with a
useEffect
onuserId
fromuseAuth
- Sign in or out of Firebase accordingly
Here’s code for a provider:
export const FirebaseAuthProvider = ({ children }: { children: React.ReactNode }) => { const { getToken, userId } = useAuth(); useEffect(() => { if (!userId) { void signOut(auth); return; } const syncFirebaseAuth = async () => { try { const token = await getToken({ template: 'integration_firebase' }); if (token) { await signInWithCustomToken(auth, token); } } catch (err) { console.error('Firebase auth sync failed:', err); } }; void syncFirebaseAuth(); }, [userId, getToken]); return ( <FirebaseAuthContext.Provider value={auth}> {children} </FirebaseAuthContext.Provider> ); };
Some complexities are still missing from this sample implementation. You may, for example, want to add more variables from the Clerk hook to the useEffect array, eg:
orgId
Gating on Clerk user session info
A lot of Firestore’s magic relies not on server-side auth-gating, but rather doing this via Firestore rules.
The example Firestore rules in the Clerk docs sidestep this by allowing any authenticated user to read any document:
service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null; } } }
But, you can indeed set up more granular rules! In order to reverse engineer what the fields are, let’s set up two logs:
- Log the output of Clerk’s
getToken
to see what it’s passing to Firestore
const token = await getToken({ template: 'integration_firebase' }); console.log("token", token); if (token) { await signInWithCustomToken(auth, token); }
- Log the state of Firebase’s
auth
object
const app = initializeApp(firebaseConfig); const auth = getAuth(app); auth.currentUser?.getIdToken(true).then(console.log).catch(console.error);
You’ll get some base-64 encoded strings, use a JWT decoder to see the values.
And so, the token from Clerk’s
getToken
looks like:{ "iss": "[email protected]", "aud": "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit", "exp": 1735422875, "iat": 1735419275, "sub": "[email protected]", "uid": "user_2qa33lYAE2kVP9mI1mXtdR4Ko4Z", "claims": { "active_org_id": "org_2qmIadaoKw0oFt6r6bbm5puAZtD", "email": "[email protected]" } }
And the one from Firebase’s
auth
object:{ "active_org_id": "org_2qmIadaoKw0oFt6r6bbm5puAZtD", "email": "[email protected]", "iss": "https://securetoken.google.com/...", "aud": "...", "auth_time": 1735419275, "user_id": "user_2qa33lYAE2kVP9mI1mXtdR4Ko4Z", "sub": "user_2qa33lYAE2kVP9mI1mXtdR4Ko4Z", "iat": 1735419276, "exp": 1735422876, "firebase": { "identities": {}, "sign_in_provider": "custom" } }
The latter is the one you want to design your Firestore rules around. As you can see
user_id
and active_org_id
are passed through, and elevated to the top level instead of being inside “claims”, so, you could use the following rule to gate all documents on an org_id
field:rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null && request.auth.token.active_org_id == resource.data.org_id } } }
I’ve tested the above Firestore rule - and it works as expected!
You might want to eg: gate different documents on userId vs orgId vs something else. One word of caution though: the above is insecure for brevity, I’ll include an expanded & corrected version of it in the appendix.
Gating on custom claims
The next step is figuring out how to use more custom conditions.
For example, you might want to gate on more than just org-membership, but rather on being in a specific role within an org.
I haven’t tested this out yet, but it does seem likely doable, and here’s a rough sketch of how to approach it:
- Create a custom JWT template for Clerk and update your code to use it
- Add in any new claims you care about
- Use the above flow to debug + make sure the claims are correctly propagated to the Firebase JWT
- Add Firestore rules to gate on them
And there you go! You now can access Firestore as a given Clerk user, and customize document access based on their Clerk roles.
Appendix: Better Firestore Rules
It’s easy to write unsafe Firestore rules, here’s a more fleshed out set of Firestore rules that I’d consider. Please make sure you test it before you use it though, I’ve not tested it.
This gates all document access on a document field
org_id
and is a reasonable default rule for a B2B tool. This does require that all new docs you create do have an org_id
field.rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, delete: if request.auth != null && request.auth.token.active_org_id == resource.data.org_id allow update: if request.auth != null && request.auth.token.active_org_id == resource.data.org_id && request.auth.token.active_org_id == request.resource.data.org_id allow create: if request.auth != null && request.auth.token.active_org_id == request.resource.data.org_id } } }
Appendix: Why you shouldn’t use Firestore
Firestore is cool for really great realtime + local caching, but for many apps that’s not the make-or-break feature. There’s plenty of reasons to not use Firestore that you should consider.
- Locked in to Firebase / Google
- SQL implementations are open-source and portable
- Firebase often suddenly gets expensive, and you have no way out. This is the most common reason for migrating off Firebase I’ve heard
- NoSQL can be a bit painful
- You often end up needing to denormalize data, which can end up being a little tricky to keep in sync etc.
- Relational DBs are just more powerful, and you’re going to bump up against that issue a lot
- Everyone knows SQL
- Your spending some of your novelty budget on Firestore - and you might struggle with some of your engineers being less familiar with how to schematize in a Firestore world.
- There’s other weirdnesses too, eg: transactions in Firestore don’t behave as you’d expect.
- Odd security model
- Firestore security rules are tricky to get right, and it’s easier to accidentally introduce vulnerabilities.