better-auth-electron

Better Auth - Electron Desktop Integration

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "better-auth-electron" with this command: npx skills add 5dlabs/cto/5dlabs-cto-better-auth-electron

Better Auth - Electron Desktop Integration

Better Auth works seamlessly with Electron using the React client in the renderer process with secure IPC patterns.

AI Tooling

IMPORTANT: Before implementing Better Auth in Electron, consult:

Use Context7 to look up Better Auth patterns:

get_library_docs({ libraryName: "better-auth", topic: "react client" }) get_library_docs({ libraryName: "better-auth", topic: "session management" })

Installation

Install Better Auth and electron-store for session persistence

npm install better-auth electron-store

Architecture

┌─────────────────────────────────────────────────────┐ │ Main Process │ │ - Session validation via IPC │ │ - Secure token storage (electron-store) │ │ - Auth state management │ └───────────────────┬─────────────────────────────────┘ │ IPC (contextBridge) ┌───────────────────▼─────────────────────────────────┐ │ Preload Script │ │ - Expose safe auth APIs to renderer │ │ - No direct Node.js access │ └───────────────────┬─────────────────────────────────┘ │ ┌───────────────────▼─────────────────────────────────┐ │ Renderer Process │ │ - Better Auth React client │ │ - UI components (React/shadcn) │ │ - Uses window.authApi from preload │ └─────────────────────────────────────────────────────┘

Backend Configuration

Your Electron app needs a Better Auth backend (can be local or remote):

Backend (server/auth.ts ):

import { betterAuth } from "better-auth" import { drizzleAdapter } from "better-auth/adapters/drizzle" import { db } from "./db"

export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }), emailAndPassword: { enabled: true, }, socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, }, // Trust Electron app origin trustedOrigins: [ "app://.", // Electron custom protocol "file://", // File protocol "http://localhost", // Dev server ], })

Main Process

main.ts :

import { app, BrowserWindow, ipcMain, shell } from "electron" import Store from "electron-store" import path from "path"

// Secure persistent storage for auth state const store = new Store({ name: "auth", encryptionKey: "your-encryption-key", // Use secure key management })

function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, // REQUIRED - security nodeIntegration: false, // REQUIRED - security sandbox: true, // RECOMMENDED }, })

// Open OAuth callbacks in external browser win.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith("http")) { shell.openExternal(url) return { action: "deny" } } return { action: "allow" } })

win.loadFile("index.html") }

// IPC handlers for auth operations ipcMain.handle("auth:get-stored-session", async () => { return store.get("session", null) })

ipcMain.handle("auth:store-session", async (_, session) => { store.set("session", session) })

ipcMain.handle("auth:clear-session", async () => { store.delete("session") })

ipcMain.handle("auth:get-api-url", async () => { return process.env.AUTH_API_URL || "http://localhost:3000" })

app.whenReady().then(createWindow)

Preload Script

preload.ts :

import { contextBridge, ipcRenderer } from "electron"

// Expose secure auth API to renderer contextBridge.exposeInMainWorld("authApi", { // Session persistence getStoredSession: () => ipcRenderer.invoke("auth:get-stored-session"), storeSession: (session: unknown) => ipcRenderer.invoke("auth:store-session", session), clearSession: () => ipcRenderer.invoke("auth:clear-session"),

// Config getApiUrl: () => ipcRenderer.invoke("auth:get-api-url"),

// Events onAuthStateChange: (callback: (session: unknown) => void) => { ipcRenderer.on("auth:state-changed", (_, session) => callback(session)) }, })

// Type declarations for renderer declare global { interface Window { authApi: { getStoredSession: () => Promise<unknown> storeSession: (session: unknown) => Promise<void> clearSession: () => Promise<void> getApiUrl: () => Promise<string> onAuthStateChange: (callback: (session: unknown) => void) => void } } }

Renderer Process (React)

Auth Client (src/lib/auth-client.ts ):

import { createAuthClient } from "better-auth/react"

// Get API URL from main process const getAuthClient = async () => { const baseURL = await window.authApi.getApiUrl()

return createAuthClient({ baseURL, // Custom fetch to handle Electron environment fetchOptions: { credentials: "include", }, }) }

// Export singleton let authClientPromise: ReturnType<typeof getAuthClient> | null = null

export const getClient = () => { if (!authClientPromise) { authClientPromise = getAuthClient() } return authClientPromise }

// Hook for components export function useAuthClient() { const [client, setClient] = useState<Awaited<ReturnType<typeof getAuthClient>> | null>(null)

useEffect(() => { getClient().then(setClient) }, [])

return client }

Simplified Client (if API URL is static):

import { createAuthClient } from "better-auth/react"

export const authClient = createAuthClient({ baseURL: "http://localhost:3000", // Your auth server })

export const { signIn, signUp, signOut, useSession } = authClient

Sign In Component

import { useState } from "react" import { authClient } from "@/lib/auth-client" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"

export function SignIn() { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [loading, setLoading] = useState(false) const [error, setError] = useState<string | null>(null)

const handleSignIn = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setError(null)

const { data, error } = await authClient.signIn.email({
  email,
  password,
})

if (error) {
  setError(error.message)
} else if (data?.session) {
  // Persist session to main process store
  await window.authApi.storeSession(data.session)
}

setLoading(false)

}

return ( <Card className="w-full max-w-md mx-auto"> <CardHeader> <CardTitle>Sign In</CardTitle> </CardHeader> <CardContent> <form onSubmit={handleSignIn} className="space-y-4"> <Input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required /> <Input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} required /> {error && <p className="text-sm text-destructive">{error}</p>} <Button type="submit" className="w-full" disabled={loading}> {loading ? "Signing in..." : "Sign In"} </Button> </form> </CardContent> </Card> ) }

Session Management

import { useEffect } from "react" import { authClient } from "@/lib/auth-client" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"

export function UserMenu() { const { data: session, isPending } = authClient.useSession()

// Sync session changes to main process useEffect(() => { if (session) { window.authApi.storeSession(session) } }, [session])

const handleSignOut = async () => { await authClient.signOut() await window.authApi.clearSession() }

if (isPending) { return <div className="h-8 w-8 animate-pulse rounded-full bg-muted" /> }

if (!session) { return <Button variant="outline">Sign In</Button> }

return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="relative h-8 w-8 rounded-full"> <Avatar className="h-8 w-8"> <AvatarImage src={session.user.image || ""} /> <AvatarFallback> {session.user.name?.[0]?.toUpperCase()} </AvatarFallback> </Avatar> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem className="font-medium"> {session.user.name} </DropdownMenuItem> <DropdownMenuItem className="text-muted-foreground"> {session.user.email} </DropdownMenuItem> <DropdownMenuItem onClick={handleSignOut}> Sign Out </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) }

Restore Session on App Launch

// App.tsx - Restore session when app starts import { useEffect, useState } from "react" import { authClient } from "@/lib/auth-client"

export function App() { const [initialized, setInitialized] = useState(false)

useEffect(() => { async function restoreSession() { // Check for stored session from previous launch const storedSession = await window.authApi.getStoredSession()

  if (storedSession) {
    // Validate session with server
    const { data: currentSession } = await authClient.getSession()
    
    if (!currentSession) {
      // Session expired, clear stored data
      await window.authApi.clearSession()
    }
  }
  
  setInitialized(true)
}

restoreSession()

}, [])

if (!initialized) { return <div>Loading...</div> }

return <MainApp /> }

OAuth in Electron

For OAuth flows, open the auth URL in the system browser and handle the callback:

import { shell } from "electron" // Main process only

// In main process - handle OAuth callback app.setAsDefaultProtocolClient("myapp") // Register custom protocol

app.on("open-url", (event, url) => { // Handle OAuth callback: myapp://auth/callback?code=... if (url.includes("/auth/callback")) { mainWindow.webContents.send("auth:oauth-callback", url) } })

Security Best Practices

  • Always use contextIsolation - Never expose Node.js to renderer

  • Encrypt stored sessions - Use electron-store with encryption

  • Validate sessions on startup - Check with server before trusting local state

  • Handle OAuth via system browser - More secure than in-app browser

  • Use sandbox mode - Additional security layer

  • Clear sensitive data on sign out - Both in-memory and persistent storage

Documentation: https://better-auth.com/docs

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

expo-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

elysia-llm-docs

No summary provided by upstream source.

Repository SourceNeeds Review
General

better-auth-expo

No summary provided by upstream source.

Repository SourceNeeds Review
General

anime-js

No summary provided by upstream source.

Repository SourceNeeds Review