Electron Integration
Electron is a popular framework for building cross-platform desktop applications using web technologies.
Better Auth can be integrated into Electron apps to provide secure authentication flows, leveraging the system browser.
Installation
Configure a Better Auth front- & back-end
Before integrating with Electron, ensure you have a Better Auth server and client set up.
To get started, check out our installation guide for setting up Better Auth.
Install the required packages
Install the Better Auth server package and the Electron integration package in your server, Electron app, and web client projects.
In each project, run:
npm install better-auth @better-auth/electronAdd the Electron plugin to your Better Auth server
Add the Electron plugin to your Better Auth server.
import { betterAuth } from "better-auth";
import { electron } from "@better-auth/electron";
export const auth = betterAuth({
plugins: [electron()],
emailAndPassword: {
enabled: true,
},
social: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
});Add the proxy plugin to your web client
On your frontend, add the proxy plugin to handle redirects back into the Electron app.
import { createAuthClient } from "better-auth/client";
import { electronProxyClient } from "@better-auth/electron/proxy";
export const authClient = createAuthClient({
baseURL: "http://localhost:8081",
plugins: [
electronProxyClient({
protocol: {
scheme: "com.example.app"
},
}),
],
});Initialize the Electron client
import { createAuthClient } from "better-auth/client";
import { electronClient } from "@better-auth/electron/client";
export const authClient = createAuthClient({
baseURL: "http://localhost:8081", // Base URL of your Better Auth frontend
plugins: [
electronClient({
signInURL: "https://app.example.com/sign-in", // The URL to redirect to for authentication
protocol: {
scheme: "com.example.app" // The custom protocol scheme registered by your Electron app
},
storage: {
getItem: async (key) => {
// retrieve entry from storage
},
setItem: async (key, value) => {
// set entry in storage
},
},
}),
],
});If you'd rather not implement your own storage solution, we offer a default option:
- Make sure to install the
confpackage:
npm install conf- Then, you can use it as follows:
import { storage } from "@better-auth/electron/storage";
electronClient({
storage: storage(),
});You should never expose the authClient directly to the renderer process. Instead, create an IPC bridge to securely communicate between the main and renderer processes.
Scheme and Trusted Origins
The Electron plugin uses deep links to redirect users back to your app after authentication.
To enable this, you need to add your app's protocol scheme to the trustedOrigins on your Better Auth server.
First, make sure you have a custom protocol scheme registered in your build configuration.
module.exports = {
packagerConfig: {
protocols: [{
name: "MyApp Protocol",
schemes: ["com.example.app"],
}],
},
// ...other config options
};{
"protocols": [
{
"name": "MyApp Protocol",
"schemes": ["com.example.app"]
}
],
// ...other config options
}Then, update your Better Auth config to include the scheme in trustedOrigins:
export const auth = betterAuth({
trustedOrigins: ["com.example.app:/"],
});Configure the BrowserWindow
Ensure that your BrowserWindow is configured with nodeIntegration set to false and contextIsolation set to true, to ensure NodeJS APIs aren't exposed to any JavaScript process running in the browser.
import { BrowserWindow } from "electron";
import { join } from "node:path";
const win = new BrowserWindow({
webPreferences: {
preload: join(__dirname, "preload.mjs"),
nodeIntegration: false,
contextIsolation: true,
},
});Setup the Main Process
In your Electron main process, use the setupMain() method from the Electron auth client to handle necessary configurations.
This will:
- Register the protocol handler for deep links.
- Set up content security policies.
- Set up IPC bridges for communication between main and renderer processes.
import { authClient } from "./lib/auth-client";
authClient.setupMain();Note that this must be called before the app is ready.
Setup the Renderer Process
In your preload script, use the setupRenderer() method from the Electron auth client to expose safe IPC bridges to the renderer process.
import { authClient } from "./lib/auth-client";
authClient.setupRenderer();Note that this must also be called before the app is ready.
To infer the types of the exposed bridges, you can use authClient.$Infer.Bridges to extend the Window interface.
import type { authClient } from "./lib/auth-client";
declare global {
type Bridges = typeof authClient.$Infer.Bridges;
interface Window extends Bridges {}
}Usage
Handling Authorization in the Browser
In order to redirect users back to your Electron application, you need to call the ensureElectronRedirect() method on your web sign-in callback page. Also make sure to preserve any PKCE and state parameters during the sign-in initiation, by passing them via fetchOptions.query.
The following example uses React, but the same logic applies to any framework:
import { useEffect, use } from "react";
import { authClient } from "../auth-client";
function SignIn({
searchParams,
}: {
searchParams: Promise<{
client_id?: string | undefined;
state?: string | undefined;
code_challenge?: string | undefined;
code_challenge_method?: string | undefined;
}>;
}) {
const query = use(searchParams);
useEffect(() => {
const id = authClient.ensureElectronRedirect();
return () => {
clearTimeout(id);
}
}, []);
return (
<button
onClick={() =>
authClient.signIn.social({
provider: "google",
fetchOptions: { query }, // preserve PKCE/state
});
}
>
Sign in with Google
</button>
);
}Handling Authentication in Electron
In your Electron renderer process, you can use the IPC bridges exposed by the preload script.
function Auth() {
useEffect(() => {
const unsubscribeAuthenticated =
window.onAuthenticated((user) => {
console.log("Authenticated user:", user);
});
const unsubscribeAuthError =
window.onAuthError((ctx) => {
toast.error(`Authentication error: ${ctx.message}`);
});
return () => {
unsubscribeAuthenticated();
unsubscribeAuthError();
};
}, []);
return (
<>
<button onClick={() => window.requestAuth()}>
Sign in with Browser
</button>
<button
onClick={() =>
window.requestAuth({
provider: "google", // sign in with social provider, redirecting directly to the provider
});
}
>
Sign in with Google
</button>
</>
);
}In the main process, you can call from the auth client directly:
import { authClient } from "./lib/auth-client";
authClient.requestAuth();Sign Out
To sign out the user inside the renderer process, you can use the signOut bridge in the renderer process:
<button onClick={() => window.signOut()}>Sign out</button>Subscribing to User Updates
You can listen for user changes via the onUserUpdated bridge in the renderer process:
useEffect(() => {
const unsubscribe = window.onUserUpdated((user) => {
console.log("User updated:", user);
});
return () => unsubscribe();
}, []);Handling Errors
Listen for authentication errors via the onAuthError bridge in the renderer process. The bridge will receive error context forwarded from fetch hooks.
useEffect(() => {
const unsubscribe = window.onAuthError((ctx) => {
console.error("Authentication error:", ctx);
});
return () => unsubscribe();
}, []);Creating IPC bridges
You should create IPC bridges to extend the functionality exposed to your renderer process. This ensures a minimal, safe API surface.
First, create an IPC handler in the main process that uses the authClient to perform the desired action:
import { authClient } from "./lib/auth-client";
import { ipcMain } from "electron";
ipcMain.handle("myBridge", async (_event, data) => {
const cookie = authClient.getCookie();
return await authClient.someEndpoint({
data,
fetchOptions: {
headers: { cookie },
},
});
});Next, expose the bridge in your preload script using contextBridge:
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("myBridge", (data: Record<string, any>) => {
return ipcRenderer.invoke("myBridge", data);
});To infer the types of your bridge, extend the Window interface:
import type { authClient } from "./lib/auth-client";
declare global {
type Bridges = typeof authClient.$Infer.Bridges;
interface Window extends Bridges {
myBridge: (data: Record<string, any>) => Promise<any>;
}
}Now you can call your custom bridge from anywhere in the renderer process:
useEffect(() => {
window
.myBridge({
foo: "bar",
})
.then((res) => {
console.log("Bridge response:", res);
});
}, []);For more details, check out Electron's Inter-Process Communication tutorial.
Options
Server plugin
codeExpiresIn?
The duration, in seconds, for which the authorization code is valid. (defaults to 300)
Note that the authorization code will be refreshed during active endpoint usage.
electron({
codeExpiresIn: 300, // 5 minutes
});redirectCookieExpiresIn?
The duration, in seconds, for which the redirect cookie remains valid. (defaults to 120)
electron({
redirectCookieExpiresIn: 120, // 2 minutes
});The redirect cookie name is derived by the clientID.
cookiePrefix?
The prefix to use for cookies set by the plugin. (defaults to better-auth)
electron({
cookiePrefix: "better-auth",
});clientID?
The client id to use for identifying the Electron client during authorization. (defaults to electron)
Make sure this matches the clientID provided to both the proxy and electron client plugin.
electron({
clientID: "electron",
});disableOriginOverride?
Override the origin for Electron API routes. (defaults to false)
Enable this if you're facing cors origin issues with Electron API routes.
electron({
disableOriginOverride: true,
});Proxy client
protocol
The protocol scheme to use for deep linking in Electron.
Should follow the reverse domain name notation to ensure uniqueness.
Make sure this matches the protocol scheme provided to the Electron client and trustedOrigins.
electronProxyClient({
protocol: "com.example.app",
});electronProxyClient({
protocol: {
scheme: "com.example.app",
},
});callbackPath?
The callback path to use for authentication redirects. (defaults to /auth/callback)
Make sure this matches the path provided to the electron client plugin.
electronProxyClient({
callbackPath: "/auth/callback",
});clientID?
The client id to use for identifying the Electron client during authorization. (defaults to electron)
Make sure this matches the clientID provided to both the server and electron client plugin.
electronProxyClient({
clientID: "electron",
});cookiePrefix?
The prefix to use for cookies set by the plugin. (defaults to better-auth)
electronProxyClient({
cookiePrefix: "better-auth",
});Client plugin
signInURL
The URL to redirect to for authentication.
electronClient({
signInURL: "http://localhost:3000/sign-in",
});protocol
The protocol scheme to use for deep linking in Electron.
Should follow the reverse domain name notation to ensure uniqueness.
Make sure this matches the protocol scheme provided to the proxy client and trustedOrigins.
electronProxyClient({
protocol: "com.example.app",
});electronProxyClient({
protocol: {
scheme: "com.example.app",
},
});callbackPath?
The callback path to use for authentication redirects. (defaults to /auth/callback)
Make sure this matches the path provided to the proxy client plugin.
electronClient({
callbackPath: "/auth/callback",
});storage
Storage solution to use to store session and cookie data.
By default a storage file is generated in the userData directory. The name is derived by the project name.
electronClient({
storage: {
getItem: (key) => {
// get entry from storage
},
setItem: (key, value) => {
// set entry in storage
},
},
});storagePrefix?
Prefix for local storage keys. (defaults to better-auth)
electronClient({
storagePrefix: "better-auth",
});cookiePrefix?
Prefix(es) for server cookie names to filter. (defaults to better-auth)
This is used to identify which cookies belong to better-auth to prevent infinite refetching when third-party cookies are set.
electronClient({
cookiePrefix: "better-auth",
});electronClient({
cookiePrefix: ["better-auth", "my-app"],
});channelPrefix?
Channel prefix for IPC bridges. (defaults to better-auth)
electronClient({
channelPrefix: "myapp",
});clientID?
The client id to use for identifying the Electron client during authorization. (defaults to electron)
Make sure this matches the clientID provided to both the server and proxy client plugin.
electronClient({
clientID: "electron",
});disableCache?
Whether to disable caching the session data locally. (defaults to false)
electronClient({
disableCache: true,
});