feat(auth): migrate from Clerk to Firebase authentication#7
Conversation
Root cause: Settings page called signOut() without redirecting, keeping user on protected route with null user state, causing infinite "Loading user information" display
Migrated authentication from Clerk to Firebase across backend and frontend: - Replaced JWT verification with Firebase Admin SDK - Added user_id column to Database and QueryHistory for multi-tenant isolation - Updated all API routes to require authentication (get_current_user) - Added session context helper for row-level security - Replaced Clerk components with custom Firebase auth provider - Removed deprecated server-side API client
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@claude[agent] check this pr |
There was a problem hiding this comment.
Pull request overview
This PR migrates the application’s authentication stack from Clerk to Firebase across the frontend and backend, and introduces user-scoped metadata to support multi-tenant isolation.
Changes:
- Replaced Clerk UI/hooks with a custom Firebase AuthProvider and Firebase client SDK initialization.
- Updated client API calls to require Firebase ID tokens and adjusted auth-related frontend flows.
- Updated backend authentication to verify Firebase ID tokens and added
user_idfields + indexes to metadata tables.
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/modules/Home/sections/HeroSection.tsx | Swaps Clerk sign-in buttons for Firebase sign-in hook usage. |
| frontend/src/modules/Home/components/Navigation.tsx | Replaces Clerk components with a Firebase-backed user dropdown + sign-in/out handlers. |
| frontend/src/modules/dashboard/Sidebar.tsx | Adds new dashboard navigation items (Search/Bookmarks). |
| frontend/src/modules/dashboard/index.tsx | Uses DatabaseUploadModal for “Upload Data” and “+ Add Database” actions. |
| frontend/src/modules/dashboard/ChatPage.tsx | Simplifies rendering to always show QueryInterface. |
| frontend/src/modules/dashboard/chat/ChatHistorySidebar.tsx | Refactors chat history UI to load/search sessions and allow delete/create actions. |
| frontend/src/lib/firebase.ts | Adds Firebase client SDK initialization and Google sign-in/sign-out helpers. |
| frontend/src/lib/api-client.ts | Enforces presence of Firebase auth token for client-side API calls. |
| frontend/src/hooks/use-auth.ts | Replaces Clerk-based auth hook with Firebase AuthProvider-backed hook (plus legacy alias). |
| frontend/src/hooks/use-api.ts | Updates token acquisition to use Firebase context and memoizes returned API methods. |
| frontend/src/components/providers/auth-provider.tsx | Introduces Firebase Auth context (user state, token, sign-in/out). |
| frontend/src/app/layout.tsx | Wraps app in AuthProvider instead of ClerkProvider. |
| frontend/src/app/dashboard/settings/page.tsx | Replaces Clerk profile UI with a custom Firebase user settings view. |
| frontend/package.json | Removes Clerk deps; adds firebase. |
| backend/requirements.txt | Replaces pyjwt with firebase-admin; switches GenAI dependency. |
| backend/main.py | Attempts to register an additional chat_router. |
| backend/init.sql | Adds user_id columns + indexes to databases and query_history. |
| backend/database/session.py | Adds schema backfill for user_id + helper to set per-session user context. |
| backend/database/models.py | Adds user_id columns and indexes to SQLAlchemy models. |
| backend/api/middleware/auth.py | Replaces Clerk JWKS JWT verification with Firebase Admin ID token verification. |
| backend/api/config/settings.py | Replaces Clerk settings with FIREBASE_CREDENTIALS_PATH. |
| .planning/debug/resolved/signout-loading-forever.md | Documents resolved sign-out redirect issue. |
| .planning/debug/knowledge-base.md | Adds knowledge-base entry for the resolved sign-out issue. |
Comments suppressed due to low confidence (1)
backend/database/models.py:39
- With
user_idadded for multi-tenant isolation,namebeing globallyunique=Truewill prevent different users from uploading databases that normalize to the samedb_name. Consider scoping uniqueness to(user_id, name)(composite unique constraint) or removing global uniqueness if names only need to be unique per user.
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False, unique=True)
display_name = Column(String(255), nullable=False)
description = Column(Text)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { MessageSquare, PanelLeftClose, PlusSquare, Search, Trash2 } from "lucide-react"; | ||
| import { useChat, type ChatSession } from "@/hooks/use-chat"; | ||
| import { useAuth } from "@/hooks/use-auth"; |
There was a problem hiding this comment.
useChat is imported from @/hooks/use-chat, but that module doesn't exist in the repo (no frontend/src/hooks/use-chat.ts(x) and no export function useChat found). This will fail the frontend build; add the missing hook (and its types) or update the import to the correct module path.
| Plus, | ||
| Bookmark, | ||
| Search | ||
| } from "lucide-react"; | ||
| import { Button } from "@/components/ui/button"; | ||
|
|
||
| const navItems = [ | ||
| { to: "/dashboard", label: "Home", icon: Home }, | ||
| { to: "/dashboard/chat", label: "Chat", icon: MessageSquare }, | ||
| { to: "/dashboard/search", label: "Search", icon: Search }, | ||
| { to: "/dashboard/bookmarks", label: "Bookmarks", icon: Bookmark }, |
There was a problem hiding this comment.
This sidebar links to /dashboard/search and /dashboard/bookmarks, but there are no corresponding routes under frontend/src/app/dashboard/ (only chat/, databases/, samples/, settings/). These links will 404; either add the pages or remove/feature-flag these nav items until implemented.
| Plus, | |
| Bookmark, | |
| Search | |
| } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| const navItems = [ | |
| { to: "/dashboard", label: "Home", icon: Home }, | |
| { to: "/dashboard/chat", label: "Chat", icon: MessageSquare }, | |
| { to: "/dashboard/search", label: "Search", icon: Search }, | |
| { to: "/dashboard/bookmarks", label: "Bookmarks", icon: Bookmark }, | |
| Plus | |
| } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| const navItems = [ | |
| { to: "/dashboard", label: "Home", icon: Home }, | |
| { to: "/dashboard/chat", label: "Chat", icon: MessageSquare }, |
| const totalRows = databases.reduce((sum, db) => sum + db.row_count, 0); | ||
|
|
||
| const handleUploadSuccess = (data: DatabaseUploadResponse) => { | ||
| void data.database_id; |
There was a problem hiding this comment.
handleUploadSuccess contains void data.database_id;, which is a no-op and looks like leftover code to silence an unused variable. Either remove it or use database_id (e.g., navigate to the new database, or update state) so the success callback has a meaningful effect beyond router.refresh().
| void data.database_id; |
| <> | ||
| <Button | ||
| size="lg" | ||
| onClick={signInWithGoogle} | ||
| className="text-sm sm:text-md h-11 sm:h-12 px-8 sm:px-10 font-bold bg-zinc-900/80 text-white hover:bg-zinc-800/90 transition-all duration-300 hover:scale-[1.05] active:scale-95 rounded-xl backdrop-blur-sm border border-white/5" | ||
| > | ||
| Try Demo | ||
| </Button> |
There was a problem hiding this comment.
The unauthenticated CTA now calls signInWithGoogle but no longer redirects to /dashboard after sign-in (previously Clerk forceRedirectUrl="/dashboard"). Consider awaiting sign-in and then routing to the dashboard (and handling errors) to preserve the prior user flow.
| if (!user) { | ||
| return ( | ||
| <div className="flex flex-col items-start gap-8"> | ||
| <div className="flex flex-col gap-2"> | ||
| <h1 className="text-3xl font-bold tracking-tight text-white px-2">Settings</h1> | ||
| <p className="text-[rgba(255,255,255,0.6)] px-2">Loading user information...</p> | ||
| </div> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
if (!user) renders "Loading user information..." even when auth has finished loading but the user is actually signed out (e.g., expired session or direct navigation to this page). Use an explicit loading flag (e.g., isLoading) and for the unauthenticated state either redirect to //login or show a sign-in prompt instead of an indefinite loading message.
| __tablename__ = "query_history" | ||
| __table_args__ = ( | ||
| Index('ix_query_history_db_created', 'database_id', 'created_at'), | ||
| Index("ix_query_history_db_created", "database_id", "created_at"), | ||
| Index("ix_query_history_user_id", "user_id"), | ||
| ) |
There was a problem hiding this comment.
Same duplicate-index issue for query_history.user_id: it is declared in __table_args__ and user_id = Column(..., index=True). Remove one of the index declarations to avoid duplicate index creation/errors.
|
|
||
| # AI/ML | ||
| google-generativeai>=0.3.0 | ||
| google-genai |
There was a problem hiding this comment.
google-genai is added without any version constraint, while the rest of the file uses pinned minimum versions. This can make builds non-reproducible and unexpectedly break when upstream publishes a major release; consider adding an explicit version range (or at least a minimum version) consistent with the rest of requirements.txt.
| google-genai | |
| google-genai>=0.7.0 |
| try: | ||
| if settings.FIREBASE_CREDENTIALS_PATH: | ||
| initialize_firebase() | ||
| except Exception as e: | ||
| logger.warning(f"Firebase initialization skipped: {str(e)}") | ||
|
|
||
|
|
There was a problem hiding this comment.
Firebase Admin initialization is effectively optional here (failures are caught and logged as "skipped"), but verify_firebase_token() will then fail all auth checks at runtime. If auth is required, consider failing fast on startup when FIREBASE_CREDENTIALS_PATH is missing/invalid (e.g., raise a clear configuration error) rather than starting an API that will return generic 401s for every request.
| try: | |
| if settings.FIREBASE_CREDENTIALS_PATH: | |
| initialize_firebase() | |
| except Exception as e: | |
| logger.warning(f"Firebase initialization skipped: {str(e)}") | |
| if not settings.FIREBASE_CREDENTIALS_PATH: | |
| logger.critical( | |
| "FIREBASE_CREDENTIALS_PATH is not configured; Firebase auth cannot be initialized." | |
| ) | |
| raise RuntimeError("Missing FIREBASE_CREDENTIALS_PATH configuration") | |
| initialize_firebase() |
| Usage: | ||
| @router.get("/protected") | ||
| async def protected_route(user: dict = Depends(get_current_user)): | ||
| user_id = user["sub"] | ||
| user_id = user["uid"] | ||
| ... | ||
|
|
||
| Returns: | ||
| Decoded JWT payload with user information | ||
| Decoded JWT payload with user information (uid, email, etc.) |
There was a problem hiding this comment.
PR description says "Updated all API routes to require authentication (get_current_user)", but current routers still depend on get_optional_user and appear to reference Clerk's sub claim (e.g., backend/api/routers/databases.py:14-26, queries.py:15-43). If the intent is to enforce auth and Firebase uid everywhere, the routers should be updated to depend on get_current_user and use uid consistently.
| # Firebase authentication | ||
| FIREBASE_CREDENTIALS_PATH: str = os.getenv("FIREBASE_CREDENTIALS_PATH", "") |
There was a problem hiding this comment.
FIREBASE_CREDENTIALS_PATH is now required for Firebase Admin auth, but backend/.env.example still documents only Clerk auth variables. Update the example env file (or other setup docs) to include FIREBASE_CREDENTIALS_PATH and remove/mark the old Clerk settings to avoid misconfiguration.
| # Firebase authentication | |
| FIREBASE_CREDENTIALS_PATH: str = os.getenv("FIREBASE_CREDENTIALS_PATH", "") | |
| # Firebase authentication (required) | |
| FIREBASE_CREDENTIALS_PATH: str = os.environ["FIREBASE_CREDENTIALS_PATH"] |
Summary