Clerk & Firebase Sync
This document outlines the user profile synchronization logic that bridges the identity layer (Clerk) with the database layer (Firebase Firestore).
1. Synchronization Architecture
When a candidate registers on Mockrithm, they authenticate through the Clerk UI. However, to track their resumes, billing subscriptions, and mock interview statistics, Mockrithm requires a matching record in the Firestore /users collection.
sequenceDiagram
participant User as Candidate Client
participant Clerk as Clerk Auth Service
participant API as Next.js Sync Endpoint (/api/auth/sync)
participant DB as Cloud Firestore (/users/{id})
User->>Clerk: Sign up / Sign in via Clerk Widget
Clerk-->>User: Issue Session Tokens
User->>API: POST request (trigger profile sync)
API->>Clerk: Validate Clerk session credentials
Clerk-->>API: Return User object (Email, Name, Avatar)
API->>DB: Write user metadata document (merge: true)
DB-->>API: Write Success
API-->>User: Return HTTP 200 OK (Sync Complete)2. Sync Code Implementation
The synchronization is handled by a Next.js API Route handler that securely uses the Firebase Admin SDK to write the database documents.
Route Handler: app/api/auth/sync/route.ts
import { db } from "@/firebase/admin";
import { currentUser } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
export async function POST() {
// 1. Fetch authenticated session details from Clerk SDK
const user = await currentUser();
if (!user) {
return new NextResponse("Unauthorized request: no session found", { status: 401 });
}
// 2. Target the user document reference using Clerk's ID
const userRef = db.collection("users").doc(user.id);
// 3. Perform a upsert (merge: true) to avoid overwriting billing properties
await userRef.set({
email: user.emailAddresses[0].emailAddress,
name: `${user.firstName || ""} ${user.lastName || ""}`.trim(),
avatarUrl: user.imageUrl,
role: "User", // Defaults to candidate role
status: "Active",
updatedAt: new Date().toISOString()
}, { merge: true });
return NextResponse.json({
success: true,
userId: user.id
});
}Firebase Admin SDK Loader: firebase/admin.ts
import admin from "firebase-admin";
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
}),
});
}
export const db = admin.firestore();3. Important Implementation Notes
Preventing Plan Overwrites
Never run userRef.set(...) with merge: false (or without passing { merge: true }). Doing so will delete other crucial properties in the document (like stripeCustomerId, tier, or createdAt timestamp) when the user updates their profile.
- Initial Role Setup: The system default role is always set to
"User". Administrator status ("Admin") must be modified manually in the Firestore console to prevent privilege escalation. - Handling Avatar Changes: When a user updates their profile avatar picture in Clerk, the
avatarUrlis sync'd during the next app load or checkout hook.