Authentication (Firebase)
Authenticate user and store in Firestore.
This snippet sets up a small but complete authentication flow for a Next.js app using Firebase Auth and Firestore. The goal is to let a user sign in with Google, keep the current auth state available everywhere in the component tree through a React context, and create a matching profile document in a Firestore users collection the first time someone signs in. It solves the common problem of needing a single source of truth for "who is logged in" plus a persistent user record you can attach app data to.
Create a User Context
import { createContext } from 'react'
export const UserContext = createContext({ user: null, authLoading: true })
Add the Provider in the App.js (Next.js _app.jsx file is used here)
import 'regenerator-runtime/runtime'
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import Script from 'next/script'
import { UserContext } from '../lib/context'
import { useUserData } from '../lib/hooks'
function MyApp({ Component, pageProps }: AppProps) {
const userData = useUserData()
return (
<>
<UserContext.Provider value={userData}>
<Component {...pageProps} />
</UserContext.Provider>
</>
)
}
export default MyApp
Initialize firebase app with YOUR configurations.
import { initializeApp, getApps, getApp } from 'firebase/app'
import { getAuth, GoogleAuthProvider } from 'firebase/auth'
import {
getFirestore,
collection,
where,
getDocs,
query,
limit,
} from 'firebase/firestore'
import { getStorage } from 'firebase/storage'
// Get this from project settings (Settings -> Project settings -> Found at the bottom.)
const firebaseConfig = {
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_AUTH_DOMAIN',
projectId: 'YOUR_PROJECT_ID',
storageBucket: 'YOUR_STORAGE_BUCKET',
messagingSenderId: 'YOUR_MESSAGING_SENDER_ID',
appId: 'YOUR_APP_ID',
measurementId: 'YOUR_MEASUREMENT_ID',
}
function createFirebaseApp(config) {
try {
return getApp()
} catch {
return initializeApp(config)
}
}
const firebaseApp = createFirebaseApp(firebaseConfig)
export const auth = getAuth(firebaseApp)
export const googleAuthProvider = new GoogleAuthProvider()
export const firestore = getFirestore(firebaseApp)
// Storage exports
export const storage = getStorage(firebaseApp)
export const STATE_CHANGED = 'state_changed'
Hook to check for auth state change
import { doc, onSnapshot, getFirestore } from 'firebase/firestore'
import { auth, firestore } from './firebase'
import { useEffect, useState } from 'react'
import { useAuthState } from 'react-firebase-hooks/auth'
import { useRouter } from 'next/router'
// Custom hook to read auth record and user profile doc
export function useUserData() {
const [user, loading] = useAuthState(auth)
const router = useRouter()
useEffect(() => {
// turn off realtime subscription
let unsubscribe
if (user) {
const ref = doc(getFirestore(), 'users', user.uid)
unsubscribe = onSnapshot(ref, (doc) => {console.log('doc', doc)})
}
return unsubscribe
}, [user])
return { user, authLoading: loading }
}
When auth state changes - context updates and components rerender.
Use the logic in your protected routes and login components
import { signInWithPopup } from 'firebase/auth'
import { doc, getFirestore, getDoc, setDoc } from 'firebase/firestore'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { useContext, useEffect } from 'react'
import MetadataComponent from '../components/MetadataComponent'
import { UserContext } from '../lib/context'
import { auth, googleAuthProvider } from '../lib/firebase'
const Login: NextPage = () => {
const router = useRouter()
const { user, authLoading } = useContext(UserContext)
useEffect(() => {
if (!authLoading && user) {
// user is already logged in.
redirectToDashboard()
}
}, [user])
const redirectToDashboard = () => {
router.push('/dashboard')
}
const signInWithGoogle = async () => {
signInWithPopup(auth, googleAuthProvider)
.then((res) => {
console.log('successful', res)
writeUserToFirestore(res.user)
})
.catch((err) => {
console.log('err', err)
})
}
const writeUserToFirestore = async (currentUser) => {
const userRef = doc(getFirestore(), 'users', currentUser.uid)
const userDoc = await getDoc(userRef)
if (!userDoc.exists()) {
const userData = {
displayName: currentUser.displayName,
email: currentUser.email,
photoURL: currentUser.photoURL,
uid: currentUser.uid,
isAdmin: false,
}
await setDoc(userRef, userData)
redirectToDashboard()
} else {
console.log('user already exists...')
redirectToDashboard()
}
}
const signOut = () => {
auth
.signOut()
.then(() => {
console.log('signed out')
})
.catch((err) => {
console.log('err', err)
})
}
return (
<div className="flex h-screen w-full items-center justify-center bg-[#111827]">
<MetadataComponent />
<main className="flex flex-col items-center justify-center">
<button
onClick={signInWithGoogle}
className="rounded-md border border-white px-4 py-2 text-white"
>
Sign in with google
</button>
</main>
</div>
)
}
export default Login
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { useContext, useEffect } from 'react'
import MetadataComponent from '../components/MetadataComponent'
import { UserContext } from '../lib/context'
import { auth } from '../lib/firebase'
const Dashboard: NextPage = () => {
const router = useRouter()
const { user, authLoading } = useContext(UserContext)
useEffect(() => {
if (!authLoading && !user) {
redirectToLogin()
}
}, [authLoading, user])
console.log('user...', user)
console.log('authLoading...', authLoading)
const redirectToLogin = () => {
router.push('/login')
}
const signOut = () => {
auth
.signOut()
.then(() => {
console.log('signed out')
router.push('/login')
})
.catch((err) => {
console.log('err', err)
})
}
return (
<div className="flex h-screen w-full items-center justify-center bg-[#111827]">
<MetadataComponent />
<main className="flex flex-col items-center justify-center">
<p className="text-white">Dashboard</p>
<button
onClick={signOut}
className="rounded-md border border-white px-4 py-2 text-white"
>
Signout
</button>
</main>
</div>
)
}
export default Dashboard
How it works
The pieces fit together as a chain that starts at the Firebase SDK and ends at your screens.
firebase.ts initializes the Firebase app once. The createFirebaseApp helper calls getApp() and falls back to initializeApp(config) only when no app exists yet, which avoids the "Firebase app already exists" error that Next.js fast refresh and repeated imports tend to trigger. It then exports the auth instance, a GoogleAuthProvider, the firestore instance, and storage.
hooks.ts exposes useUserData, a custom hook built on useAuthState from react-firebase-hooks. It returns the current user and a loading flag, and whenever a user is present it opens an onSnapshot real-time subscription to that user's document at users/{uid}. The hook returns the subscription's unsubscribe function from the effect so the listener is cleaned up when the user changes or the component unmounts. The hook surfaces its result as { user, authLoading }.
_app.jsx calls useUserData() at the root and feeds the result into UserContext.Provider, so every page can read auth state with useContext(UserContext). Because the provider value updates when auth state changes, components re-render automatically on sign-in and sign-out.
login.tsx drives the actual sign-in. signInWithGoogle calls signInWithPopup with the Google provider, then hands the returned user to writeUserToFirestore. That function reads users/{uid} with getDoc; if the document does not exist it writes a new one with setDoc containing displayName, email, photoURL, uid, and isAdmin: false. Either way it redirects to /dashboard. The page also redirects already-authenticated users away from the login screen. Dashboard.tsx does the inverse guard: if loading has finished and there is no user, it pushes back to /login, and it offers a sign-out button that calls auth.signOut().
Notes & gotchas
- Keep your real Firebase config out of source control where it matters. The web
apiKeyis not a secret, but lock down access with Firestore Security Rules so users can only read and write their ownusers/{uid}document. The client-sideisAdmin: falsefield is convenience data, not a trust boundary, never gate privileged actions on it without server-side rule enforcement. - Auth guards here are client-side redirects. They improve UX but do not protect data; that protection must live in your Security Rules.
writeUserToFirestoreonly creates the document when it is missing, so later profile edits are never overwritten on subsequent logins. If you change the shape of the user document, existing users will keep their old fields until you migrate them.- The
onSnapshotlistener in the hook currently just logs the document. To actually use the profile in your UI, set it into state and include it in the context value. - Avoid calling
getFirestore()ad hoc in multiple places; prefer the singlefirestoreexport fromfirebase.tsto keep one initialized instance.