Electron Quickstart, Beta
Before you start
Example repository
This quickstart creates a new Electron app with Electron Forge, adds React to the renderer, and adds Clerk authentication to it. The Clerk Electron SDK needs Electron 28 or later, Node.js 20.9 or later, and contextIsolation enabled (Electron's default); the SDK reference
Enable Native API
In the Clerk Dashboard, navigate to the Native applications page and enable the Native API. This is required to integrate Clerk in your native application or browser extension.
Allow your app's origin
The renderer sends every Clerk request with an Authorization header and, because it's a browser, an Origin header. Clerk's only accepts that combination from origins you allowlist on your instance. Add the origin your renderer runs on; in development that's Electron Forge's Vite dev server. You'll add your packaged app's custom scheme when you configure OAuth deep links. OAuth and SSO sign-in need a packaged app on macOS and Linux, because those platforms only deliver custom-scheme deep links to packaged apps; the guide covers that.
In your terminal, run the following command; it uses your instance's .
- In the Clerk Dashboard, navigate to the API keys page and copy your .
- In your terminal, run the following command, replacing
YOUR_SECRET_KEYwith it.
curl -X PATCH https://api.clerk.com/v1/instance \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-type: application/json" \
-d '{"allowed_origins": ["http://localhost:5173"]}'The command sets the whole list. The OAuth deep links guide adds your packaged app's scheme origin alongside the dev server for development, and the deployment guide
Create a new Electron app
If you don't already have an Electron app, run the following commands to create one with Electron Forge's Vite + TypeScript template. The template doesn't include React, so the last two commands add it. @vitejs/plugin-react is pinned to v4 because the template loads its Vite config as CommonJS, which newer plugin versions no longer support, and typescript@5 replaces the template's 2021 pin so the tsconfig.json settings below are accepted.
npm --allow-git=all init electron-app@latest clerk-electron-quickstart -- --template=vite-typescript
cd clerk-electron-quickstart
npm install react react-dom
npm install -D @vitejs/plugin-react@4 typescript@5 @types/react @types/react-domnpm --allow-git=all init electron-app@latest clerk-electron-quickstart -- --template=vite-typescript
# couldn't auto-convert command
cd clerk-electron-quickstart
pnpm add react react-dom
pnpm add -D @vitejs/plugin-react@4 typescript@5 @types/react @types/react-domnpm --allow-git=all init electron-app@latest clerk-electron-quickstart -- --template=vite-typescript
# couldn't auto-convert command
cd clerk-electron-quickstart
yarn add react react-dom
yarn add --dev @vitejs/plugin-react@4 typescript@5 @types/react @types/react-domnpm --allow-git all init electron-app@latest clerk-electron-quickstart --template=vite-typescript
# couldn't auto-convert command
cd clerk-electron-quickstart
bun add react react-dom
bun add --dev @vitejs/plugin-react@4 typescript@5 @types/react @types/react-domThe --allow-git=all flag is for npm 12 and later, which block git-hosted dependencies by default; Electron Forge's template installs @electron/node-gyp from GitHub. Earlier npm versions ignore the flag.
Then replace the renderer entry with a React root. Rename src/renderer.ts to src/renderer.tsx, and replace index.html with:
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Clerk + Electron</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/renderer.tsx"></script>
</body>
</html>Enable JSX in the renderer build by adding the React plugin to vite.renderer.config.ts:
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [react()],
})Install @clerk/electron
The Clerk Electron SDKelectron-store, which is an optional peer dependency. The SDK currently supports electron-store@8, so the command pins that version.
npm install @clerk/electron electron-store@8pnpm add @clerk/electron electron-store@8yarn add @clerk/electron electron-store@8bun add @clerk/electron electron-store@8The SDK publishes its entrypoints through the package exports map, which the Forge template's TypeScript settings predate. In tsconfig.json, change module and moduleResolution (the template sets them to commonjs and node) and add jsx; keep the template's other options:
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx"
}
}If you haven't already, create a new Clerk application in the Clerk Dashboard. For more information, see the setup guide.
Add your Clerk to your .env file. This key can always be retrieved from the API keys page in the Clerk Dashboard.
- In the Clerk Dashboard, navigate to the API keys page.
- In the Quick Copy section, copy your Clerk .
- Paste your key into your
.envfile.
The final result should resemble the following:
VITE_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY
VITE_CLERK_FRONTEND_API_HOST=YOUR_FRONTEND_API_URLThe second value is your instance's hostname without https://. The Content Security Policy step below adds the protocol automatically.
Set up the main process
createClerkBridge() registers the inter-process communication (IPC) handlers and token storage that the renderer relies on. Call it before app.whenReady() and pass it a storage adapter. When you later configure a renderer scheme for OAuth, the bridge also takes Electron's single-instance lock on Windows and Linux, so gate the rest of your bootstrap on isPrimaryInstance from the start. See the createClerkBridge() reference
The guard keeps navigation and server-side redirects on your renderer's origin (the Vite dev server here; the OAuth deep links guide adds your packaged app's scheme) because a page from another origin would inherit the preload script and Clerk's token cache; external links open in the system browser.
import { createClerkBridge } from '@clerk/electron'
import { storage } from '@clerk/electron/storage'
import { app, BrowserWindow, shell } from 'electron'
import started from 'electron-squirrel-startup'
import path from 'node:path'
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
app.quit()
}
// `new URL(...).origin` is the string 'null' for a non-special scheme like
// my-app://, so compare on the scheme and host instead.
const originOf = (url: string) => {
const parsed = new URL(url)
return `${parsed.protocol}//${parsed.host}`
}
// Registers Clerk's IPC handlers and token storage. Call it before app.whenReady().
const clerk = createClerkBridge({
storage: storage(),
})
const createWindow = () => {
const mainWindow = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
})
// Keep the Clerk bridge inside this app: navigation to another origin, including a
// server-side redirect to one, would carry the preload (and the token cache) to a
// page you do not control.
const allowedOrigins = new Set(
[MAIN_WINDOW_VITE_DEV_SERVER_URL]
.filter((value): value is string => Boolean(value))
.map(originOf),
)
mainWindow.webContents.on('will-navigate', (event, url) => {
if (!allowedOrigins.has(originOf(url))) {
event.preventDefault()
if (url.startsWith('https://') || url.startsWith('http://')) {
void shell.openExternal(url)
}
}
})
mainWindow.webContents.on('will-redirect', (event, url) => {
if (event.isMainFrame && !allowedOrigins.has(originOf(url))) {
event.preventDefault()
}
})
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https://') || url.startsWith('http://')) {
void shell.openExternal(url)
}
return { action: 'deny' }
})
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
} else {
mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`))
}
}
// On Windows and Linux, Clerk forwards OAuth deep links through Electron's
// single-instance lock. Skip the bootstrap in the process that is quitting.
if (clerk.isPrimaryInstance) {
app.on('ready', createWindow)
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
app.on('before-quit', () => {
clerk.cleanup()
})Expose the bridge from the preload script
The preload script publishes a narrow bridge that @clerk/electron/react uses for token storage and OAuth transport. It works with contextIsolation enabled, which is Electron's default.
import { exposeClerkBridge } from '@clerk/electron/preload'
exposeClerkBridge()Add a Content Security Policy
Clerk loads its prebuilt UI from your instance's at runtime, so the renderer's Content Security Policy () must allow that host. Without it, Clerk's components never render. Replace index.html with the following; Vite fills in %VITE_CLERK_FRONTEND_API_HOST% from your .env file. The 'unsafe-eval' and localhost entries are only needed for Vite's dev server; see the deployment guide
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://%VITE_CLERK_FRONTEND_API_HOST% https://challenges.cloudflare.com https://*.protect.clerk.com; connect-src 'self' https://%VITE_CLERK_FRONTEND_API_HOST% https://*.protect.clerk.com:* https://clerk-telemetry.com https://*.clerk-telemetry.com ws://localhost:5173 http://localhost:5173; img-src 'self' https://img.clerk.com data:; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; frame-src 'self' https://challenges.cloudflare.com https://*.protect.clerk.com; form-action 'self';"
/>
<title>Clerk + Electron</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/renderer.tsx"></script>
</body>
</html>Electron logs a security warning in development because the dev-server policy allows 'unsafe-eval'; the packaged build's header policy in the deployment guide doesn't.
Add <ClerkProvider> to your app
The <ClerkProvider> component provides session and user context to Clerk's hooks and components. It's recommended to wrap your entire app at the entry point with <ClerkProvider> to make authentication globally accessible. See the reference docs for other configuration options.
TypeScript needs Vite's client types for import.meta.env; add this line to forge.env.d.ts:
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
/// <reference types="vite/client" />Render your app inside <ClerkProvider>. Import it from @clerk/electron/react; it configures Clerk for Electron and re-exports everything from @clerk/react.
import { ClerkProvider } from '@clerk/electron/react'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './index.css'
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!PUBLISHABLE_KEY) {
throw new Error('Add your Clerk Publishable Key to the .env file')
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY}>
<App />
</ClerkProvider>
</StrictMode>,
)Create a header with Clerk components
You can control which content signed-in and signed-out users can see with the prebuilt control components. The following example creates a header using the following components:
- <Show when="signed-out">: Children of this component can only be seen while signed out.
- <Show when="signed-in">: Children of this component can only be seen while signed in.
- <SignInButton mode="modal" /> and <SignUpButton mode="modal" />: Unstyled components that open Clerk's sign-in and sign-up flows in a modal inside your window.
- <UserButton />: Shows the signed-in user's avatar. Selecting it opens a dropdown menu with account management options.
Without mode="modal", the buttons link to the Account Portal, which would navigate the renderer away from your app.
import { Show, SignInButton, SignUpButton, UserButton } from '@clerk/electron/react'
export default function App() {
return (
<>
<header>
<Show when="signed-out">
<SignInButton mode="modal" />
<SignUpButton mode="modal" />
</Show>
<Show when="signed-in">
<UserButton />
</Show>
</header>
<main>
<h1>Clerk + Electron</h1>
</main>
</>
)
}npm startpnpm startyarn startbun startCreate your first user
Select Sign up in the window that opens and create an account with a non-OAuth method, such as an email code or password. Social and SSO options require a packaged app and the additional setup in Configure OAuth and SSO deep links.
Quit and start the app again to confirm the session persists: the token is encrypted with Electron's safeStorage and restored on launch. Packaged macOS builds need a code-signing identity for that persistence to work; see the deployment guide
Next steps
Explore the most relevant next steps for your SDK using the following guides.
Configure OAuth deep links
Learn how OAuth and SSO return to your packaged app through a custom scheme.
Configure passkeys
Learn how passkeys work in the renderer and natively on macOS and Windows.
Deploy an Electron app to production
Learn how to package your app with production keys and a Content Security Policy.
Clerk Electron SDK reference
Learn what the SDK supports and how its main, preload, and renderer pieces fit together.
Feedback
Last updated on