GenerateSaaS

Sign-in and security

Browser-based device authorization sign-in plus the shell's security boundaries - CSP, external links, the OS keychain, and the deny-by-default preload bridge.

No password form. Sign-in runs the OAuth 2.0 Device Authorization Grant via the Better Auth deviceAuthorization and bearer plugins (the desktop client uses client_id "desktop"):

The app's "Sign in" requests a device/user code, then opens the system browser to ${baseUrl}/auth/device?user_code=XXXX and shows the user code.
The user approves in the web app, where they are already signed in.
The desktop polls and receives a bearer token, stored in the OS keychain via Electron safeStorage through a validated IPC handler (window.api.auth), never exposed to the renderer's JavaScript.

The verification page lives in your web frontend's /auth/device page and its deviceAuthorizationClient wiring. See your project's Authentication guide for the shared Better Auth setup.

Security and native boundaries

The renderer is fully isolated - contextIsolation on, sandbox on, nodeIntegration off - and reaches the main process only through the narrow, validated preload bridge (window.api); the raw ipcRenderer is never exposed. Every main-process handler validates its inputs. Boundaries to know when you add features:

BoundaryBehaviorWhere to extend
Content-Security-Policyconnect-src is 'self' and names no backend origin: the renderer never fetches the backend directly - every backend call rides the main-process bridge (backend-transport.ts), which CSP does not govern. img-src allows any HTTPS image (avatars from social/OAuth providers, etc.).Repoint the backend by setting config.desktop.baseUrl (pinned at build time in apps/electron/src/main/backend-url.ts), not connect-src. To restrict image hosts, edit img-src in buildCspPolicy (apps/electron/src/main/csp-policy.ts).
External linksOpen in the system browser via window.api.shell.openExternal, which accepts only http(s) URLs (isHttpUrl), so a stray file: or javascript: link cannot be opened. New windows are always denied.-
NavigationTop-level navigation and redirects are allowlisted to the app's own document (the dev server origin, or the packaged renderer's file: document); anything else is blocked, so the bridged renderer can never be pointed at an attacker origin.-
Auth tokenStored in the OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) via Electron safeStorage; a dev-only file-backed fallback is used when safeStorage is unavailable. Never in the renderer's web storage.-
IPC surfaceEvery method the renderer may call is an explicit window.api.* entry backed by one validating ipcMain.handle; there is no general-purpose bridge.Add a typed method to the preload bridge + a validating handler in apps/electron/src/main/ipc.ts.
Device sign-inAccepts any client_id by default (the desktop sends "desktop").Set validateClient on the deviceAuthorization plugin in @repo/auth to restrict clients in production.

CSP is enforced two ways from one policy builder (csp-policy.ts) so dev and packaged cannot drift: the dev server gets a header CSP (onHeadersReceived), and the packaged file: renderer gets a build-time <meta> CSP (header CSP does not apply to file:).

A CSP-blocked request surfaces only as a console CSP violation, not a thrown error - but CSP governs only the renderer's own resources, such as images under img-src. Backend calls do not go through CSP: they ride the main-process bridge, so a failing API call is a bridge or backend-URL issue - confirm config.desktop.baseUrl points at the backend you expect, not connect-src.

The renderer never reaches your backend directly. It hands the main process a request PATH; main composes the URL against the origin pinned at build time from config.desktop.baseUrl and refuses any path that resolves off it, so a compromised renderer cannot point main's bearer-carrying fetch at another host. Auth is a bearer token (not cookies), and main sends no Origin header in dev or packaged builds, so the backend treats it as a native client and never CORS-checks it - there is no dev-vs-packaged origin to configure. Your backend's TRUSTED_ORIGINS (config.origins) still governs your web frontends - see your project's Configuration guide.

On this page