Skip to main content

Deploy an Electron app to production,

Beta

This feature is currently in beta. Functionality may change before general availability. If you run into any issues, please reach out to our support team.

This guide is written for Electron Forge, but the steps apply to any packaging tool. Adapt the file names and configuration to yours.

Create a production instance

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.

Warning

Enabling the Native API opens a public request pathway that bypasses browser-based CAPTCHA challenges. Learn more about how the Native API affects bot protection.

A Clerk production instance needs a domain even when your Electron app has no website: the production is served from clerk.<your-domain>, and that is the host your app talks to. Follow the guide on deploying your Clerk app to production.

Set your production Publishable Key

In the Clerk Dashboard, select your production instance from the instance dropdown at the top of the page. Then:

.env.production
VITE_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY
VITE_CLERK_FRONTEND_API_HOST=YOUR_FRONTEND_API_URL

Use .env.production rather than .env: Vite reads it for packaged builds, so your development key stays in .env. Vite inlines VITE_-prefixed variables at build time, so both values are baked into the packaged app. Never add CLERK_SECRET_KEY to an Electron project.

Allow your production origin

Repeat the allowed_origins step from the OAuth deep links guide against your production instance with its production . List your scheme origin (for example, my-app://renderer), and leave the dev-server origin out of production.

Serve the renderer from a custom scheme

OAuth and SSO callbacks return to a custom scheme, and Clerk doesn't treat file:// as a valid redirect origin. Follow Configure OAuth deep links to register the scheme with createClerkBridge(), serve the packaged renderer with protocol.handle(), and declare the scheme in forge.config.ts.

Set a production Content Security Policy

The quickstart'sElectron Icon <meta> policy includes 'unsafe-eval' and the Vite dev server for hot reloading. For packaged builds, set the policy as a response header from the main process, without those entries. The policy is built from your production instance's Frontend API host.

src/main.ts
import { app, session } from 'electron'

const FAPI_HOST = import.meta.env.VITE_CLERK_FRONTEND_API_HOST

if (!FAPI_HOST) {
  throw new Error('Add VITE_CLERK_FRONTEND_API_HOST to the .env file')
}

// Packaged builds: the CSP comes from a response header on the renderer's own origin,
// without the dev-server allowances the index.html meta tag carries.
const applyContentSecurityPolicy = () => {
  session.defaultSession.webRequest.onHeadersReceived(
    { urls: [`${RENDERER_SCHEME}://${RENDERER_HOST}/*`] },
    (details, callback) => {
      if (details.resourceType !== 'mainFrame') {
        callback({ responseHeaders: details.responseHeaders })
        return
      }
      callback({
        responseHeaders: {
          ...details.responseHeaders,
          'Content-Security-Policy': [
            [
              "default-src 'self'",
              `script-src 'self' 'unsafe-inline' https://${FAPI_HOST} https://challenges.cloudflare.com https://*.protect.clerk.com`,
              `connect-src 'self' https://${FAPI_HOST} https://*.protect.clerk.com:* https://clerk-telemetry.com https://*.clerk-telemetry.com`,
              "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'",
            ].join('; '),
          ],
        },
      })
    },
  )
}

if (clerk.isPrimaryInstance) {
  app.on('ready', () => {
    if (!MAIN_WINDOW_VITE_DEV_SERVER_URL) {
      registerRendererProtocol()
      applyContentSecurityPolicy()
    }
    createWindow()
  })
}

Set VITE_CLERK_FRONTEND_API_HOST in .env.production to your production instance's hostname without https://. Register the listener in the same ready handler that registers the scheme and creates the window, before createWindow(), so the first navigation already carries the header. The urls filter limits it to your renderer's origin, and the resourceType check applies it to the main document only. Electron keeps only the last listener attached to a webRequest event, so if your app already calls onHeadersReceived(), merge this header into that listener instead of registering a second one.

If you use Clerk Billing or other features that load additional origins, add them per the CSP guide.

Package and sign

npm run make
pnpm run make
yarn make
bun run make

The Electron Forge template's Fuses configuration needs no Clerk-specific changes. Sign and notarize the macOS build for distribution.

Signing is also what keeps users signed in across launches. @clerk/electron/storage encrypts the session token with Electron's safeStorage, which on macOS needs a Keychain entry tied to the app's signing identity. An unsigned or ad-hoc-signed package (what npm run package produces by default) logs Keychain lookup failed followed by Clerk: failed to securely persist a token; it will only be available until the app exits., and the app starts signed out after every relaunch even though sign-in worked.

Configure osxSign (and osxNotarize for distribution) in forge.config.ts with your Developer ID; native passkeysElectron Icon require the same identity. Windows and Linux builds need no Clerk-specific signing steps.

Identify your app in session activity

Pass userAgent to createClerkBridge() so sessions created by your app show its name and version on the user's session activity list:

src/main.ts
createClerkBridge({
  storage: storage(),
  renderer: { scheme: 'my-app', host: 'renderer' },
  userAgent: `My App/${app.getVersion()}`,
})

Handle single-instance behavior on Windows and Linux

When renderer is set, Clerk takes Electron's single-instance lock on Windows and Linux so OAuth callbacks reach the running app. If your app relies on multiple instances, see single-instance behaviorElectron Icon before shipping.

Feedback

What did you think of this content?

Last updated on