Setting Up Firebase Authentication in an Electron App — React/Typescript/Tailwind/Electron-Forge
So, you wanna build an Electron app and need authentication? Good call. But let’s be real — this isn’t as easy as it sounds. Getting…
Setting Up Firebase Authentication in an Electron App — React/Typescript/Tailwind/Electron-Forge

Setting Up Firebase Authentication in an Electron App — React/Typescript/Tailwind/Electron-Forge
So, you wanna build an Electron app and need authentication? Good call. But let’s be real — this isn’t as easy as it sounds. Getting Firebase to play nicely with Electron can be a pain, and there’s barely any useful information out there. I know because I built an app using this tech stack, and finding the right approach was a struggle. So, instead of piecing this together from random sources, I’m documenting what actually worked for me. In this guide, we’ll go from zero to a working Electron app with Firebase authentication. Let’s roll.
1. Understanding IPC, Preload, and Main Processes
Before diving into setup, it’s crucial to understand how Electron works:
- Main Process: Manages the Electron app lifecycle and creates browser windows.
- Renderer Process: Runs your UI (React, in this case) and handles frontend interactions.
- Preload Script: Acts as a bridge between the two, exposing safe APIs for the renderer.
- IPC (Inter-Process Communication): Allows secure messaging between the renderer and main process.
Electron is essentially a Node.js app, but the renderer process behaves more like a web app. This creates a challenge: Firebase’s authentication library expects to run in a browser environment, but Electron’s main process runs in Node.js. Directly using Firebase in the main process isn’t an option, so we need to create a bridge between the renderer and main process using IPC and a preload script. This ensures Firebase authentication runs safely in the renderer while keeping the main process secure.
Security-wise, never enable nodeIntegration in the renderer—always go through the preload script!
2. Setting Up the Electron Project with Electron Forge, Webpack, TypeScript, Tailwind and React
Install Electron Forge with Webpack and React with Tailwind
mkdir my-electron-app && cd my-electron-app
npx create-electron-app@latest my-app --template=webpack-typescript
cd my-app
npm install react react-dom @types/react @types/react-dom
npm install tailwindcss postcss autoprefixer postcss-loader css-loader
In the end , this is how my webpack.renderer.config.ts looks like:
import type { Configuration } from 'webpack';
import { rules } from './webpack.rules';
import { plugins } from './webpack.plugins';
import { DefinePlugin } from 'webpack';
import * as dotenv from 'dotenv';
dotenv.config();
rules.push({
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
},
],
});
export const rendererConfig: Configuration = {
module: {
rules,
},
plugins: [
...plugins,
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.FIREBASE_API_KEY': JSON.stringify(process.env.FIREBASE_API_KEY),
'process.env.FIREBASE_AUTH_DOMAIN': JSON.stringify(process.env.FIREBASE_AUTH_DOMAIN),
'process.env.FIREBASE_PROJECT_ID': JSON.stringify(process.env.FIREBASE_PROJECT_ID),
'process.env.FIREBASE_STORAGE_BUCKET': JSON.stringify(process.env.FIREBASE_STORAGE_BUCKET),
'process.env.FIREBASE_MESSAGING_SENDER_ID': JSON.stringify(process.env.FIREBASE_MESSAGING_SENDER_ID),
'process.env.FIREBASE_APP_ID': JSON.stringify(process.env.FIREBASE_APP_ID),
'process.env.FIREBASE_MEASUREMENT_ID': JSON.stringify(process.env.FIREBASE_MEASUREMENT_ID)
})
],
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
},
};
Tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {}
plugins: [],
// Purge unused styles
purge: {
enabled: process.env.NODE_ENV === "production",
content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
options: {
safelist: [
/^bg-/,
/^text-/,
/^border-/,
// Add other dynamic classes
],
},
},
};
Postscss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Tsconfig.json
{
"compilerOptions": {
"target": "ES6",
"allowJs": true,
"module": "commonjs",
"jsx": "react",
"skipLibCheck": true,
"esModuleInterop": true,
"noImplicitAny": true,
"sourceMap": true,
"baseUrl": ".",
"outDir": "dist",
"moduleResolution": "node",
"resolveJsonModule": true,
"paths": {
"*": ["node_modules/*"]
}
},
"include": ["src/**/*"]
}
As you might notice, i haven’t posted here my entire package list or configuration, that should be common sense, such as having dotenv installed. You’ll figure it out. Now let’s get back to on how we setup firebase for authentication.
The main obstacle here was that i was not able to make the auth workflow only on the APP itself, so we need a web server as well, where we do the actual authentication. Here is the flow:
Basic Idea
- Electron App will create a UUID for the login session and launch the hosted web endpoint passing the created UUID. (e.g localhost:5173/login?sessionId=e5df736f-ae02–41d8-b175-c6f5e3dca775)
- We’ll host a custom web endpoint that will be running in the user’s default browser and let the user sign in using their Google Account on behalf of our Electron App.
- Once the user signs in, we’ll grab the ID TOKEN returned by Firebase, and invoke the cloud function with the UUID, and the grabbed ID TOKEN.
- Firebase Cloud Function will verify the ID TOKEN using the Firebase Admin SDK.
- On successful verification, the function will create a custom firebase token and update it to the corresponding UUID node in the Firebase Realtime Database.
- Electron App will be listening to the value for the generated UUID in the first step and will receive the custom token, which can be used to authenticate the user using Firebase Auth.
Web App logic
Once the user is logged in and we grab the session id we will call this cloud function:
// Cloud Function to exchange an ID token for a custom token
exports.exchangeToken = onCall(async (request) => {
const { data } = request;
try {
// Verify the ID token
const decodedToken = await getAuth().verifyIdToken(data.idToken);
// Instead of creating a custom token, just return the verified user info
// The Electron app can use this info to authenticate
return {
uid: decodedToken.uid,
email: decodedToken.email,
emailVerified: decodedToken.email_verified,
displayName: decodedToken.name,
photoURL: decodedToken.picture
};
} catch (error) {
console.error('Error exchanging token:', error);
throw new Error(error.message);
}
});
This exchangeToken function is called by handleAuth() once we login with the google provider:
async function handleAuth(userObj) {
try {
const idToken = await userObj.getIdToken(true);
token = idToken;
// If we have a sessionId, we need to store the token
if (sessionId && !sessionProcessed) {
processingSession = true;
const userInfo = await exchangeToken(idToken);
// Setting up the realtime database session info
await set(ref(database, `auth_sessions/${sessionId}`), {
idToken: idToken,
verified: true,
created: new Date().toISOString(),
user: {
uid: userObj.uid,
email: userObj.email,
displayName: userObj.displayName || '',
photoURL: userObj.photoURL || ''
}
});
// Mark session as processed to prevent duplicate processing
sessionProcessed = true;
// Important: Set these states to show success message and stop loading
showSuccessMessage = true;
processingSession = false;
} catch (e) {
console.error('Error in handleAuth:', e);
error = e.message;
processingSession = false;
}
}
Electron App Logic
Once we store the token/session info our listener in the Electron App will pick it up and authenticate us in the Electron App. But how do we actually setup firebase methods in the renderer? Let’s see the entire flow:
In my main IPC folder i have a file called authHandlers.ts . In this file i create the ipc handlers which i want to expose to the renderer. For example:
// Add handler for signing in with ID token
ipcMain.handle("sign-in-with-token", async (_, token: string) => {
try {
const userData = await signInWithIdToken(token);
// Return a sanitized user object
return {
user: userData,
};
} catch (error) {
console.error("Error signing in with ID token:", error);
throw error;
}
});
In the same authHandlers i have exposed my open auth window method that opens the web page when clicking the login button:
export function setupAuthHandlers() {
ipcMain.handle("open-auth-window", async (_, sessionId: string) => {
if (!sessionId) throw new Error("No session ID provided");
// Use the development auth URL
const authBaseUrl = process.env.AUTH_BASE_URL || "http://localhost:5173/login";
// Make sure sessionId is properly encoded in the URL
const authUrl = `${authBaseUrl}?sessionId=${encodeURIComponent(sessionId)}`;
// Listen for token in Firebase Realtime Database
const sessionRef = ref(database, `auth_sessions/${sessionId}`);
const listener = onValue(
sessionRef,
snapshot => {
const data = snapshot.val();
if (data && data.idToken) {
// Find the main window
const mainWindow = BrowserWindow.getAllWindows()[0];
if (mainWindow) {
// Send both token and user info to the renderer
mainWindow.webContents.send("auth-token-received", {
token: data.idToken,
user: data.user || null,
});
// Remove the listener after receiving the token
off(sessionRef, "value", listener);
}
}
},
error => {
// Add error handling for the database listener
console.error("Firebase database error:", error);
}
);
// Open the auth URL in the default browser
await shell.openExternal(authUrl);
// Set a timeout to remove the listener after 5 minutes
setTimeout(() => {
off(sessionRef, "value", listener);
}, 5 * 60 * 1000);
// Return the auth URL so the renderer can display it to the user
return authUrl;
});
Here we can call other functions also defined in the IPC folder ( can be same file depending on your preffer structured ), such as singInWithIdToken, pure node logic simply put:
export const signInWithIdToken = async (idToken: string) => {
try {
// Check if adminAuth was initialized properly
if (!adminAuth) {
throw new Error("Firebase Admin SDK not initialized");
}
// Verify the ID token using the Admin SDK
const decodedToken = await adminAuth.verifyIdToken(idToken);
// Get the user record
const userRecord = await adminAuth.getUser(decodedToken.uid);
// Return user data
return {
uid: userRecord.uid,
email: userRecord.email,
displayName: userRecord.displayName,
photoURL: userRecord.photoURL,
emailVerified: userRecord.emailVerified,
};
} catch (error) {
console.error("Error signing in with ID token:", error);
throw error;
}
};
And finally, don’t forget to declare the IPC handlers in the preload.ts and electron.d.ts: Example
preload.ts
contextBridge.exposeInMainWorld("electron", {
openAuthWindow: (sessionId: string) => {
return ipcRenderer.invoke("open-auth-window", sessionId);
},
auth: {
signInWithToken: (token: string) => {
return ipcRenderer.invoke("sign-in-with-token", token);
},
},
});
electron.d.ts
export interface IpcBridge {
auth: {
signInWithToken: (token: string) => Promise<{
user: {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
emailVerified: boolean;
}
}>;
};
openAuthWindow: (sessionId: string) => Promise<void>;
}
And finally in my authService.ts in the “frontend” side of the Electron App i can use these methods now: (window.electron.auth.signInWithToken(token))
// We'll use the IPC bridge to sign in with the token
export const signInWithToken = async (token: string) => {
try {
// Call the main process to verify the token
const result = await window.electron.auth.signInWithToken(token);
if (result && result.user) {
// save user to authStore
await window.authStore.setUser(result.user);
// Since we've verified the token in the main process,
// we can use the user data to update our local state
// Instead of trying to update Firebase Auth state directly,
// we'll just return the user data and let the AuthContext handle it
return result.user;
}
throw new Error("Failed to authenticate user");
} catch (error) {
console.error("Error signing in with token:", error);
throw error;
}
};
That being said, you can still use certain firebase methods in the renderer in Electron. For instance, when i need to update my user realtime database object i just use:
export const updateUserData = async (userData: Partial<User>): Promise<boolean> => {
try {
const currentUser = await window.authStore.getUser();
if (!currentUser) {
throw new Error("No authenticated user found");
}
//Create database reference with explicit path logging
const userPath = `users/${currentUser.uid}`;
const userRef = ref(db, userPath);
await update(userRef, userData);
//Rest of your code here
Obviously, the entire logic and process can get tricky and complex very fast. For example, you might have noticed in the code i have pasted above, i am also using electron-store as a state manager so i have created some methods in the IPC to expose those methods as well. Combined with a firebase CRUD operation service and state manager, we have a lot of logic and files involved.
I have tried to summarize the most important bits here, and give you a clear direction on what you need to do and to give you a real life example of a firebase auth system implemented with an Electron App.
I hope this will help someone.
Later nerds :)
메타데이터
- post_id
- 92f1f424ebfa
- slug
- setting-up-firebase-authentication-in-an-electron-app-react-typescript-tailwind-electron-forge-92f1f424ebfa
- url
- https://medium.com/@mirceagab/setting-up-firebase-authentication-in-an-electron-app-react-typescript-tailwind-electron-forge-92f1f424ebfa
- canonical_url
- https://medium.com/@mirceagab/setting-up-firebase-authentication-in-an-electron-app-react-typescript-tailwind-electron-forge-92f1f424ebfa
- author_url
- https://medium.com/@mirceagab
- status
- ok
- fetched_at
- 2026-07-30 13:51:56