This document details the complete rewrite of the backend authentication middleware from Firebase Admin SDK to Supabase JWT verification.
- Overview
- Before: Firebase Admin SDK
- After: Supabase JWT Verification
- Backend Supabase Client
- Token Verification Flow
- Development Bypass Mode
- Environment Variables
The backend authentication middleware (server/middleware/auth.js) is the gatekeeper for all protected API endpoints. It verifies the JWT token sent by the frontend in the Authorization: Bearer <token> header.
During the migration, this file was completely rewritten to replace Firebase Admin SDK's verifyIdToken() with Supabase's auth.getUser() method.
const { initializeApp, cert } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
let adminInitialized = false;
if (process.env.FIREBASE_PROJECT_ID && process.env.FIREBASE_CLIENT_EMAIL && process.env.FIREBASE_PRIVATE_KEY) {
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
})
});
adminInitialized = true;
}
const verifyToken = async (req, res, next) => {
const idToken = authHeader.split('Bearer ')[1];
if (!adminInitialized) {
req.user = { uid: "DEV_MOCK_UID_" + idToken.substring(0, 5) };
return next();
}
const decodedToken = await getAuth().verifyIdToken(idToken);
req.user = decodedToken;
next();
};Required env vars:
FIREBASE_PROJECT_IDFIREBASE_CLIENT_EMAILFIREBASE_PRIVATE_KEY
How it worked:
- Firebase Admin SDK initialized with service account credentials
verifyIdToken()decoded and verified the Firebase JWT- The decoded token contained
uid,email,email_verified, etc. req.user.uidwas used throughout the app as the user identifier
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
let supabaseAdmin = null;
if (supabaseUrl && supabaseServiceRoleKey) {
supabaseAdmin = createClient(supabaseUrl, supabaseServiceRoleKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
});
}
const verifyToken = async (req, res, next) => {
const token = authHeader.split('Bearer ')[1];
if (!supabaseAdmin) {
req.user = { uid: "DEV_MOCK_UID_" + token.substring(0, 5), email: "dev@localhost" };
return next();
}
const { data: { user }, error } = await supabaseAdmin.auth.getUser(token);
if (error || !user) {
return res.status(401).json({ error: 'Unauthorized: Invalid token' });
}
req.user = { uid: user.id, email: user.email, ...user };
next();
};Required env vars:
SUPABASE_URLSUPABASE_SERVICE_ROLE_KEY
How it works:
- Supabase admin client initialized with the service role key (bypasses RLS)
supabase.auth.getUser(token)verifies the JWT and returns the user objectreq.user.uidis set touser.id(Supabase UUID)req.user.emailis set touser.email- The full user object is spread into
req.userfor any additional fields
A separate Supabase client was created for backend use: server/supabaseClient.js
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
const supabase = createClient(supabaseUrl, supabaseServiceRoleKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
});
module.exports = supabase;Why a separate client?
- The backend uses the service role key, which bypasses Row Level Security (RLS). This is necessary for server-side operations that need to read/write across all users.
persistSession: false— the server doesn't need to maintain a session.autoRefreshToken: false— the server doesn't need to refresh tokens.- This client is imported by both
middleware/auth.jsandserver/index.jsfor data operations.
Frontend Backend
──────── ───────
User logs in ──►
Supabase returns
access_token (JWT)
API call made with
Authorization: Bearer <JWT>
──►
middleware/auth.js
supabase.auth.getUser(token)
│
├─ Valid token ──► req.user = { uid, email, ... }
│ next() ──► route handler
│
└─ Invalid/expired ──► 401 Unauthorized
| Field | Type | Source |
|---|---|---|
uid |
string (UUID) | user.id from Supabase |
email |
string | user.email from Supabase |
...user |
object | Full Supabase user object (for any additional fields) |
Important: The uid field is named to maintain backward compatibility with existing route handlers that use req.user.uid. In Supabase, this is the user's UUID (user.id).
When Supabase environment variables are not set, the middleware enters a development bypass mode:
if (!supabaseAdmin) {
req.user = { uid: "DEV_MOCK_UID_" + token.substring(0, 5), email: "dev@localhost" };
return next();
}This allows local development without a Supabase project configured. The mock UID is derived from the first 5 characters of the token, providing consistent user identification across requests.
Warning messages on startup:
⚠️ WARNING: Supabase environment variables not found in server/.env. Skipping Supabase Admin initialization.
⚠️ WARNING: API requests will bypass authentication. THIS IS FOR LOCAL DEV ONLY.
# server/.env
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-keyThe following env vars are no longer needed and should be removed from server/.env:
# REMOVE THESE — no longer used
FIREBASE_PROJECT_ID=...
FIREBASE_CLIENT_EMAIL=...
FIREBASE_PRIVATE_KEY=...- Go to Supabase Dashboard
- Select your project
- Go to Settings → API
- Copy:
- Project URL → use as
SUPABASE_URL - service_role secret → use as
SUPABASE_SERVICE_ROLE_KEY
- Project URL → use as
⚠️ NEVER use theanonkey asSUPABASE_SERVICE_ROLE_KEY. The anon key is for frontend use only and is subject to RLS policies.
npm uninstall firebase-admin # in server/
npm install @supabase/supabase-js # in server/
The firebase-admin package (142 dependencies) was completely removed from the backend.