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/electron

Add the Electron plugin to your Better Auth server

Add the Electron plugin to your Better Auth server.

web/lib/auth.ts
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.

web/lib/auth-client.ts
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

electron/lib/auth-client.ts
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:

  1. Make sure to install the conf package:
npm install conf
  1. Then, you can use it as follows:
electron/lib/auth-client.ts
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.

forge.config.js
module.exports = {
  packagerConfig: {
    protocols: [{
      name: "MyApp Protocol",
      schemes: ["com.example.app"],
    }],
  },
  // ...other config options
};
electron-builder.json5
{
  "protocols": [
    {
      "name": "MyApp Protocol",
      "schemes": ["com.example.app"]
    }
  ],
  // ...other config options
}

Then, update your Better Auth config to include the scheme in trustedOrigins:

web/lib/auth.ts
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.

electron/main.ts
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.
electron/main.ts
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.

electron/preload.ts
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.

electron/preload.d.ts
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:

web/pages/sign-in.tsx
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.

electron/App.tsx
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:

electron/main.ts
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:

electron/App.tsx
<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:

electron/App.tsx
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.

electron/error-listener.tsx
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:

electron/main.ts
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:

electron/preload.ts
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:

electron/preload.d.ts
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:

electron/App.tsx
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.

web/auth.ts
electron({
  codeExpiresIn: 300, // 5 minutes
});

redirectCookieExpiresIn?

The duration, in seconds, for which the redirect cookie remains valid. (defaults to 120)

web/auth.ts
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)

web/auth.ts
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.

web/auth.ts
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.

web/auth.ts
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.

web/auth-client.ts
electronProxyClient({
  protocol: "com.example.app",
});
web/auth-client.ts
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.

web/auth-client.ts
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.

web/auth-client.ts
electronProxyClient({
  clientID: "electron",
});

cookiePrefix?

The prefix to use for cookies set by the plugin. (defaults to better-auth)

web/auth-client.ts
electronProxyClient({
  cookiePrefix: "better-auth",
});

Client plugin

signInURL

The URL to redirect to for authentication.

electron/auth-client.ts
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.

web/auth-client.ts
electronProxyClient({
  protocol: "com.example.app",
});
web/auth-client.ts
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.

electron/auth-client.ts
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.

electron/auth-client.ts
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)

electron/auth-client.ts
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.

electron/auth-client.ts
electronClient({
  cookiePrefix: "better-auth",
});
electron/auth-client.ts
electronClient({
  cookiePrefix: ["better-auth", "my-app"],
});

channelPrefix?

Channel prefix for IPC bridges. (defaults to better-auth)

electron/auth-client.ts
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.

electron/auth-client.ts
electronClient({
  clientID: "electron",
});

disableCache?

Whether to disable caching the session data locally. (defaults to false)

electron/auth-client.ts
electronClient({
  disableCache: true,
});