This project uses Clerk for client-side authentication in Vite + React.
Create .env.local from .env.example and set:
VITE_CLERK_PUBLISHABLE_KEY=your_publishable_key_hereIf this key is missing, auth UI degrades gracefully and shows a disabled login state.
src/main.jsx: wraps the app inClerkProvidersrc/components/AuthUI.jsx: renders auth UI (Show,SignInButton,UserButton)src/components/ProtectedFeature.jsx: feature gating with Clerk auth state
- App bootstraps with
ClerkProviderinmain.jsx. - Header shows login button when signed out.
- Signed-in users see profile and menu via
UserButton. - Protected components use
useAuth()(isLoaded,isSignedIn) to gate access.
ShowSignInButtonUserButtonuseAuthuseUser
-
Login button visible but modal does not load: Verify
VITE_CLERK_PUBLISHABLE_KEYand restart dev server. -
Protected content never appears: Check
isLoadedandisSignedInusage inProtectedFeature.jsx. -
App loads without auth features: Ensure
ClerkProvideris present insrc/main.jsx.
- Do not commit
.envor.env.local. - Only commit placeholder values in
.env.example. User clicks "Sign in with Google" ↓ Google login popup appears ↓ User enters credentials ↓ Google redirects back to app ↓ Firebase creates user session ↓ Session saved to localStorage ↓ AuthContext updates user state ↓ AuthUI shows user profile ↓ App unlocks protected features
### Sign-Out Flow
User clicks profile menu → Sign Out ↓ logout() in AuthContext called ↓ Firebase Auth signs out ↓ localStorage session cleared ↓ AuthContext updates user to null ↓ AuthUI shows sign-in button ↓ Protected features hidden
### Error Handling
Common errors and how they're handled:
| Error | Handling |
|-------|----------|
| User closes popup | Silently ignored, no error shown |
| Network error | Error displayed to user |
| Invalid config | Logged to console, auth unavailable |
| Firebase not initialized | Error caught, app still works |
---
## Session Persistence
### How It Works
1. **Login**: Firebase stores `idToken` in `localStorage`
2. **Page Refresh**: `onAuthStateChanged` reads token from storage
3. **Token Valid**: User stays logged in
4. **Token Expired**: Firebase refreshes automatically
5. **Logout**: Token removed from storage
### Configuration
Session persistence is **enabled by default** via `browserLocalPersistence` in `firebase.js`:
```javascript
setPersistence(auth, browserLocalPersistence)
This means:
- ✅ User stays logged in after page refresh
- ✅ User stays logged in after closing browser
- ✅ User stays logged in across tabs/windows
- ✅ Token auto-refreshes silently
To require re-login on page refresh, modify firebase.js:
// Comment out:
// setPersistence(auth, browserLocalPersistence)Session is cleared when:
- User clicks logout
- Token expires and can't be refreshed
- User clears browser localStorage manually
- App is uninstalled (mobile PWA)
Wrap features that require authentication:
import { ProtectedFeature } from './components/ProtectedFeature';
<ProtectedFeature feature="Advanced Simulation">
<AdvancedSimulator />
</ProtectedFeature>Shows lockscreen if not authenticated, otherwise renders component.
Show content only when authenticated:
import { AuthGuard, PublicGuard } from './components/ProtectedFeature';
// Show only for authenticated users
<AuthGuard>
<SavedSimulations />
</AuthGuard>
// Show only for non-authenticated users
<PublicGuard>
<LandingPage />
</PublicGuard>Access auth state directly:
import { useAuth } from './context/AuthContext';
function MyComponent() {
const { user, isAuthenticated, loading, logout } = useAuth();
if (loading) return <div>Loading...</div>;
if (isAuthenticated) {
return <div>Hello {user.displayName}</div>;
}
return <div>Please sign in</div>;
}Returns authentication state and methods.
const {
user, // Firebase User object or null
isAuthenticated, // boolean
loading, // boolean (true while checking auth state)
error, // Error object or null
signInWithGoogle, // async function()
logout // async function()
} = useAuth();Example: User Object
{
uid: "user_id_123",
email: "user@gmail.com",
displayName: "John Doe",
photoURL: "https://lh3.googleusercontent.com/...",
metadata: {
creationTime: "2024-02-13T10:30:00Z",
lastSignInTime: "2024-02-13T12:45:00Z"
}
}Wraps your app to provide authentication context.
<AuthProvider>
<App />
</AuthProvider>Setup in main.jsx - already done for you.
Displays sign-in button or user profile.
<AuthUI />
// Props: None (uses useAuth internally)Renders:
- When not authenticated: "Sign in with Google" button
- When authenticated: User profile with logout menu
- While loading: Spinner
Restricts component access to authenticated users.
<ProtectedFeature
feature="Advanced Simulation"
showLock={true}
fallback={<CustomLockScreen />}
>
<AdvancedContent />
</ProtectedFeature>Props:
children(required): Content to show when authenticatedfeature(optional): Feature name for UI textshowLock(optional): Show lock icon (default: true)fallback(optional): Custom UI when not authenticated
Conditionally render content based on auth state.
<AuthGuard fallback={<SignInPrompt />}>
<ProtectedContent />
</AuthGuard>Opposite of AuthGuard - shows when NOT authenticated.
<PublicGuard fallback={<AlreadySignedIn />}>
<LandingPage />
</PublicGuard>This step is CRITICAL for production.
- Open Firebase Console
- Select your project
- Go to Authentication → Settings → Authorized domains
- Click Add domain
- Add:
localhost:5173(for Vite dev server) - Add:
127.0.0.1:5173
- Add your domain:
yourdomain.com - OR:
app.yourdomain.com - Do NOT add
localhost:*
Why This Matters:
- Without authorized domains, Google OAuth will be blocked
- Users will see "Invalid OAuth Redirect" error
- Your app will appear to break for users
- Unauthorized domains are automatically blocked for security
- Go to APIs & Services → OAuth consent screen
- Choose External (for public use)
- Fill in Application name, support email, etc.
- Add your domain under Authorized domains
- Click Save and Continue
- Add scopes:
profile,email
✅ DO:
- Store Firebase config in
.env.local - Add
.env.localto.gitignore - Use
VITE_prefix for frontend visibility - Keep API key in environment variables
❌ DON'T:
- Commit
.env.localto Git - Hardcode Firebase config in source
- Expose API key in public repos (it's public anyway, use domain restriction)
- Store sensitive data in localStorage
✅ DO:
- Restrict authorized domains
- Enable Google provider only (no email/password)
- Review user list regularly
- Monitor sign-in activity
- Use strong project passwords
❌ DON'T:
- Leave
localhost:*in production - Enable anonymous auth
- Enable phone auth (if not needed)
- Use weak project security rules
✅ DO:
- Use HTTPS in production
- Let Firebase handle token refresh
- Use browserLocalPersistence (default)
- Clear localStorage on logout
❌ DON'T:
- Store tokens manually
- Disable session persistence
- Redirect to non-HTTPS
- Expose user token to third parties
✅ DO:
- Use
AuthGuardfor UX (hide features) - Check
isAuthenticatedbefore showing UI - Validate on backend (if you have one)
❌ DON'T:
- Rely only on client-side auth checks
- Send sensitive data without backend validation
- Trust localStorage without verification
Cause: User closed the Google login popup
Solution: No action needed - this is normal. User will see the sign-in button again.
Cause: Your domain is not in Firebase's authorized domains list
Solution:
- Go to Firebase Console
- Authentication → Settings → Authorized domains
- Add your current domain
- Wait 5 minutes for changes to propagate
- Try again
Cause: Environment variables not set correctly
Solution:
- Check
.env.localhas all required variables - Verify values from Firebase Console
- Restart dev server:
npm run dev
Required variables:
VITE_FIREBASE_API_KEYVITE_FIREBASE_AUTH_DOMAINVITE_FIREBASE_PROJECT_ID
Cause: Session persistence not working, usually due to:
- Subdomain mismatch in authorized domains
- HTTPS/HTTP mismatch
- localStorage cleared
Solution:
- Check domain exactly matches (with/without
www) - Ensure HTTPS in production
- Check browser hasn't blocked localStorage
- Clear browser cache and cookies
Cause: AuthProvider might not be wrapping app
Solution:
- Check
main.jsxhas<AuthProvider> - Verify imports in
App.jsx - Check browser console for errors
- Restart dev server
Cause: initializeFirebase() wasn't called or failed
Solution:
- Check
main.jsxcallsinitializeFirebase() - Check
.env.localhas Firebase config - Check browser console for error message
- Look at detailed error in console
Cause: CSS variables not updated for dark mode
Solution:
- CSS variables in
AuthUI.cssuse@media (prefers-color-scheme: dark) - Ensure your OS or browser has dark mode enabled
- Or manually test with DevTools
A credential picker that appears automatically when user visits your site while logged into Google. Users can sign in with one click, no popup needed.
-
Set in
.env.local:VITE_ENABLE_ONE_TAP=true VITE_GOOGLE_CLIENT_ID=your_client_id_from_google_cloud -
Get Google Client ID:
- Go to Google Cloud Console
- APIs & Services → Credentials
- Create OAuth 2.0 Client ID (Web)
- Copy Client ID
-
One Tap will now appear (only if user is logged into Google)
One Tap is configured in AuthContext.jsx:
provider.setCustomParameters({
hd: 'yourdomain.com', // Restrict to domain (optional)
prompt: 'select_account' // Always show account selector
});- Firebase Auth Documentation
- Firebase Auth Best Practices
- Google OAuth Documentation
- Browser Local Storage API
For issues or questions:
- Check Troubleshooting section above
- Review Firebase Console → Authentication → Authorized domains
- Check browser console for error messages
- Verify
.env.localconfiguration - Check Firebase project is active
Last Updated: February 2024 Firebase SDK Version: 12.9.0+ Tested with: React 19.2.0, Vite 7.2.4