Next.js 16 + Firebase boilerplate with server-side authentication and per-user Firestore data, both reached through the Admin SDK. Sign-in exchanges an ID token for an httpOnly session cookie the server verifies; the client never touches Firestore.
- ✅ Server Side Authentication
- ✅ Sign In (Google + Anonymous)
- ✅ Upgrade Account (Anonymous → Google)
- ✅ Delete Account
- ✅ Next.js 16 (App Router)
- ✅ TypeScript Support
- ✅ Tailwind CSS Styling
- ✅ SEO Optimized
- ✅ Responsive Design
- ✅ Notification System
- ✅ Firestore (server-side, per-user)
- ✅ Vercel Analytics
- ✅ Tested with Vitest, verified in CI
- Node.js 24 or later.
enginesasks for>=24.0.0and CI builds on 24, so Vercel deploys on 24.x. - Firebase account with a project created
- Firebase Admin SDK credentials
- Go to the Firebase Console
- Click "Add project"
- Follow the setup instructions
- In your Firebase project console, go to "Authentication"
- Click "Get started"
- Enable Google and Anonymous sign-in methods
- In your Firebase project console, go to "Databases & Storage" > "Firestore"
- Click "Create database"
- Choose "Standard edition", then "Next"
- Keep the Database ID as
(default), pick a location, then "Next" - Start in production mode, then "Create"
Production mode denies every read and write from web and mobile clients while still allowing authenticated application servers - which is exactly this boilerplate's shape, and what firestore.rules here already encodes.
This step is required, not optional. A signed-out visitor never touches Firestore, so the page still renders - but the moment anyone signs in, the notes query hits a database that is not there and the whole page fails rather than hiding the misconfiguration. See Per-User Firestore Data for why it fails loudly by design.
- In your Firebase project settings, go to "Service accounts"
- Click "Generate new private key"
- Save the JSON file and use its contents for the
FIREBASE_ADMIN_SERVICE_ACCOUNTenvironment variable
- In your Firebase project settings, go to "General"
- Under "Your apps", click the web app (create one if needed)
- Copy the values into
NEXT_PUBLIC_FIREBASE_WEB_SDK_CONFIG, as JSON
The console shows a JavaScript snippet (const firebaseConfig = { apiKey: ... }) with unquoted keys. The variable is read with JSON.parse, so copy only the object, quote every key, and drop the const and any trailing comma - .env.local.example shows the exact shape.
Copy the example file and fill in your own values:
cp .env.local.example .env.local| Variable | Required | Purpose |
|---|---|---|
FIREBASE_ADMIN_SERVICE_ACCOUNT |
yes | Service account JSON from Step 4. Verifies sessions, manages users, and is also what reaches Firestore, so it needs Firestore access in the project |
NEXT_PUBLIC_FIREBASE_WEB_SDK_CONFIG |
yes | Web app config from Step 5, used by the browser SDK |
SITE_URL |
no | Public URL of the deployment, used for metadata, robots.txt and the sitemap. Falls back to the Vercel production domain, then http://localhost:3000 |
IMPORTANT: both values must be a single physical line of JSON. This is not the same as removing every
\n:private_keylegitimately contains\nescape sequences inside its string, and they must survive. A real line break in the file is what breaks parsing.jq -c . service-account.jsonproduces the right shape from a downloaded key file.
- Clone the repository
git clone https://github.com/zeikar/nextjs-firebase-boilerplate.git
cd nextjs-firebase-boilerplate- Install dependencies
npm install
# or
yarn install
# or
pnpm install
# or
bun install- Run the development server
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev- Open http://localhost:3000 with your browser to see the result.
Other scripts: npm test (run the test suite), npm run test:watch (re-run on change), npm run test:coverage (coverage report), npm run lint (ESLint), npm run build (production build), npm start (serve the build).
app/ - Next.js App Router
layout.tsx - Root layout, notification + auth providers
page.tsx - Homepage
robots.ts, sitemap.ts - SEO routes
globals.css - Tailwind entry point
api/auth/
signin/ - Exchanges an ID token for a session cookie
signout/ - Revokes the session and clears the cookie
user/ - Reads the current user, deletes the account
api/notes/ - Create and delete the signed-in user's notes
components/
auth/ - Sign in/out, upgrade and delete controls
icons/ - Icon components
modals/AuthModal.tsx - Sign-in modal
notifications/ - Notification item
notes/ - Notes list/form (client) and the server-side reader
contexts/
auth-context.tsx - Single auth state for the whole app
notification-context.tsx - Notification state and container
lib/
firebase/
admin.ts - Firebase Admin SDK setup
auth-server.ts - Session verification for server code
authService.ts - Client calls to the auth API routes
client.ts - Firebase Web SDK setup
notes.ts - Per-user notes subcollection and Note type
session.ts - Cookie name, lifetime, freshness rule
useFirebaseAuth.ts - Auth operations and loading state
utils/
firebaseErrors.ts - Error classification and messages
request-origin.ts - Same-origin guard for the state-changing routes
useFirebaseErrorHandler.ts
site.ts - Public site URL
public/ - Static files
firestore.rules - Deny-all Firestore security rules
__tests__/
helpers/ - Shared test doubles
stubs/ - Module stand-ins for the test resolver
lib/ - Pure units: guards, session rules, error mapping
api/ - Auth and notes routes, with the Admin SDK mocked
client/ - Hooks, contexts and components, under jsdom
.github/workflows/ - Lint, test and build on push and PR
Sign-in exchanges a Firebase ID token for an httpOnly session cookie (2 weeks), which server components and route handlers verify with the Admin SDK. getServerUser() returns the current user for rendering; getServerSession() additionally distinguishes an unusable cookie from a Firebase outage, so a route handler can answer 503 instead of claiming the caller is signed out.
The auth routes assume a hostile caller:
- Same-origin only - the state-changing routes (sign-in, sign-out, delete) reject requests whose
Origindoes not match the deployment; sign-in and deletion additionally require a JSON content type, so a cross-site form cannot sign a victim into an attacker's account. - Fresh tokens only - a session cookie is minted only from an ID token whose sign-in happened in the last 5 minutes, so a leaked ID token cannot be traded for a two-week session. Re-minting is exempt when the browser already holds a valid session for the same user, which is what the anonymous -> Google upgrade does: it grants no access the caller does not already have.
- Sign-out revokes everywhere - signing out calls
revokeRefreshTokens, which invalidates every session of that user on every device. Firebase cannot revoke a single session cookie, so a copied cookie would otherwise stay valid; if revocation fails the API reports it instead of claiming success. - Deletion needs re-authentication - deleting an account requires a confirmation and a freshly minted ID token (a Google popup re-auth for permanent accounts), because Admin-side deletion bypasses Firebase's own
requires-recent-loginrule. It also attempts to remove that user's notes - see Per-User Firestore Data for what happens when that does not finish.
GET /api/auth/user is included as a worked example of a protected route handler; the UI itself reads the user on the server.
A ready-to-use authentication modal that supports Google Sign-in and Anonymous authentication, with the ability to upgrade anonymous accounts to permanent ones.
Built-in error handling for Firebase authentication with user-friendly error messages.
A contextual notification system to display success/error messages to users.
Notes live at users/{uid}/notes. userNotes(uid) builds a path for whatever uid it is handed, so the layout enforces nothing on its own - ownership rests on every caller passing the uid from the verified session cookie, never one from a request body or a path segment. A server component reads with the Admin SDK; app/api/notes/route.ts writes behind rejectCrossSiteRequest, the same guard the auth routes use. The client never imports firebase/firestore.
Deleting an account removes the Auth user first, then sweeps that user's notes. A failed sweep is reported rather than hidden behind a success, and the user is warned. The server cookie is cleared; the client sign-out is attempted, and a failure there is logged. Notes can still be left behind: a write already past session verification, or a recursiveDelete that fails part-way, leaves documents under a uid nobody can authenticate as again. Nothing here reaps them. A Cloud Functions onDelete trigger is the usual first step but races that same late write, so guaranteed cleanup needs a durable deletion marker and a reaper.
Note text is capped at 200 characters and trimmed. The note count is not capped and the read is unbounded, so a user's page grows with their own notes; a production app would cap or paginate.
A server-component read that fails on one of four transient statuses - unavailable, deadline exceeded, resource exhausted, internal - renders an "unavailable" line in place of the panel, so a blip costs this section and not the rest of the page. Everything else is rethrown, a missing database included, and no error boundary wraps the section, so a misconfiguration takes the page down where you cannot miss it. Route-handler writes and deletes are not classified this way; they report a generic failure. That is deliberate: a setup error hiding behind a permanent soothing message is worse than one that fails loudly.
firestore.rules denies every client read and write. The Admin SDK bypasses rules by design, so they constrain no server code, only the client - and that file becomes the only defense the moment anyone adds client-side Firestore. This repo ships no firebase.json, so the rules are not deployed just by living here: paste the file into the Rules tab of the Firebase console, or run firebase init firestore and then firebase deploy --only firestore:rules.
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Set FIREBASE_ADMIN_SERVICE_ACCOUNT and NEXT_PUBLIC_FIREBASE_WEB_SDK_CONFIG in the project's environment variables. SITE_URL is optional on Vercel: the production domain is picked up automatically.
Add every domain the app is served from to Authentication > Settings > Authorized domains in the Firebase console - the deployment's .vercel.app domain, any custom domain, and localhost for local development. This is the domain in the browser's address bar, not the project's authDomain. Google sign-in, account upgrade and deletion of a Google account all open a popup, and an unlisted domain fails every one of them with auth/unauthorized-domain. (Deleting an anonymous account re-authenticates with a fresh ID token instead, so it opens no popup.)
If you forked this repository:
- Delete
public/google*.html- it verifies the original author's Search Console property, not yours. - Supply a Terms of Service and a Privacy Policy, or drop the line promising them.
components/modals/AuthModal.tsxtells everyone who signs in that they agree to both, and this repository ships neither. - Note that social preview images are rendered by
https://dogimg.vercel.app, an external service this repo does not control (lib/site.ts). PointSITE_OG_IMAGEat your own asset if you would rather not depend on it.
Check out the Next.js deployment documentation for more details.
Contributions are always welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
© 2025 Next.js Firebase Boilerplate
