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