This is the full developer documentation for gau # gau > Good OAuth library Flexible Small and self-hostable, works with backend-only, full-stack, and native apps, and on different hosts. Framework Agnostic Core is framework-free and uses Web Crypto, with helpers for frameworks and runtimes. Runtime Agnostic Runs on Bun, Node, Deno, Cloudflare Workers, and even Tauri. Database Agnostic Can support any database via adapters. # Database Adapters > How to use database adapters with gau. `gau` can work with any database, though currently no databases are supported directly. It’s adviced to use an ORM, which abstracts the database away. `gau` currently supports Drizzle with SQLite and PostgreSQL drivers. [Drizzle](/adapters/drizzle/) The `createAuth` function not only sets up your authentication logic but also returns a set of database methods from your adapter, allowing you to interact with your user data directly. ## Database Methods [Section titled “Database Methods”](#database-methods) Here are the methods returned by `createAuth`, which you can use in your application’s server-side logic: ### `getUser` [Section titled “getUser”](#getuser) > **Type**: `(id: string) => Promise` Retrieves a user by their unique ID. ### `getUserByEmail` [Section titled “getUserByEmail”](#getuserbyemail) > **Type**: `(email: string) => Promise` Finds a user by their email address. ### `getUserByAccount` [Section titled “getUserByAccount”](#getuserbyaccount) > **Type**: `(provider: string, providerAccountId: string) => Promise` Fetches a user based on their linked OAuth account. ### `getAccounts` [Section titled “getAccounts”](#getaccounts) > **Type**: `(userId: string) => Promise` Retrieves all accounts linked to a user. ### `getUserAndAccounts` [Section titled “getUserAndAccounts”](#getuserandaccounts) > **Type**: `(userId: string) => Promise<{ user: User, accounts: Account[] } | null>` Retrieves a user and all their linked accounts in a single call. ### `createUser` [Section titled “createUser”](#createuser) > **Type**: `(data: NewUser) => Promise` Creates a new user in the database. ### `linkAccount` [Section titled “linkAccount”](#linkaccount) > **Type**: `(data: NewAccount) => Promise` Links an OAuth account to an existing user. ### `unlinkAccount` [Section titled “unlinkAccount”](#unlinkaccount) > **Type**: `(provider: string, providerAccountId: string) => Promise` Unlinks an OAuth account from a user. ### `updateUser` [Section titled “updateUser”](#updateuser) > **Type**: `(data: Partial & { id: string }) => Promise` Updates a user’s information. ### `deleteUser` [Section titled “deleteUser”](#deleteuser) > **Type**: `(id: string) => Promise` Deletes a user from the database. ## Extending the Schema [Section titled “Extending the Schema”](#extending-the-schema) You can add custom fields to `gau`-managed tables using a `d.ts` file. #### 1. Update Schema [Section titled “1. Update Schema”](#1-update-schema) Add your custom columns to the Drizzle schema. schema.ts ```diff import { snakeCase, text } from 'drizzle-orm/sqlite-core' export const Users = snakeCase.table('users', { // ... nickname: text(), }) ``` #### 2. Augment Types [Section titled “2. Augment Types”](#2-augment-types) Create a declaration file to override the default `User` interface. This ensures `auth.session.user.nickname` is typed correctly. src/gau.d.ts ```ts import '@rttnd/gau' declare module '@rttnd/gau' { interface User { nickname?: string | null } } ``` # Drizzle > How to set up the Drizzle adapter for gau. `gau` supports Drizzle ORM 1.0, starting with `1.0.0-rc.4`, for SQLite and PostgreSQL. Drizzle 0.x is no longer supported. This guide uses Turso for SQLite; Cloudflare D1 uses the same schema with a different client. Some providers also need [verification storage](#verification-storage). 1. ##### Install Dependencies [Section titled “Install Dependencies”](#install-dependencies) * SQLite * npm ```sh npm i drizzle-orm@1.0.0-rc.4 @libsql/client npm i -D drizzle-kit@1.0.0-rc.4 ``` * pnpm ```sh pnpm add drizzle-orm@1.0.0-rc.4 @libsql/client pnpm add -D drizzle-kit@1.0.0-rc.4 ``` * yarn ```sh yarn add drizzle-orm@1.0.0-rc.4 @libsql/client yarn add -D drizzle-kit@1.0.0-rc.4 ``` * bun ```sh bun add drizzle-orm@1.0.0-rc.4 @libsql/client bun add -D drizzle-kit@1.0.0-rc.4 ``` * ni ```sh ni drizzle-orm@1.0.0-rc.4 @libsql/client ni -D drizzle-kit@1.0.0-rc.4 ``` * PostgreSQL ```sh npm i drizzle-orm@1.0.0-rc.4 @libsql/client npm i -D drizzle-kit@1.0.0-rc.4 ``` * npm ```sh pnpm add drizzle-orm@1.0.0-rc.4 @libsql/client pnpm add -D drizzle-kit@1.0.0-rc.4 ``` * pnpm ```sh yarn add drizzle-orm@1.0.0-rc.4 @libsql/client yarn add -D drizzle-kit@1.0.0-rc.4 ``` * yarn ```sh bun add drizzle-orm@1.0.0-rc.4 @libsql/client bun add -D drizzle-kit@1.0.0-rc.4 ``` * bun ```sh ni drizzle-orm@1.0.0-rc.4 @libsql/client ni -D drizzle-kit@1.0.0-rc.4 ``` * ni * npm ```sh npm i drizzle-orm@1.0.0-rc.4 pg npm i -D drizzle-kit@1.0.0-rc.4 ``` * pnpm ```sh pnpm add drizzle-orm@1.0.0-rc.4 pg pnpm add -D drizzle-kit@1.0.0-rc.4 ``` * yarn ```sh yarn add drizzle-orm@1.0.0-rc.4 pg yarn add -D drizzle-kit@1.0.0-rc.4 ``` * bun ```sh bun add drizzle-orm@1.0.0-rc.4 pg bun add -D drizzle-kit@1.0.0-rc.4 ``` * ni ```sh ni drizzle-orm@1.0.0-rc.4 pg ni -D drizzle-kit@1.0.0-rc.4 ``` * npm ```sh npm i drizzle-orm@1.0.0-rc.4 pg npm i -D drizzle-kit@1.0.0-rc.4 ``` * pnpm ```sh pnpm add drizzle-orm@1.0.0-rc.4 pg pnpm add -D drizzle-kit@1.0.0-rc.4 ``` * yarn ```sh yarn add drizzle-orm@1.0.0-rc.4 pg yarn add -D drizzle-kit@1.0.0-rc.4 ``` * bun ```sh bun add drizzle-orm@1.0.0-rc.4 pg bun add -D drizzle-kit@1.0.0-rc.4 ``` * ni ```sh ni drizzle-orm@1.0.0-rc.4 pg ni -D drizzle-kit@1.0.0-rc.4 ``` 2. ##### Define the Database Schema [Section titled “Define the Database Schema”](#define-the-database-schema) Define the user and account fields shown below. `sessionState` and the account’s `createdAt` are optional. You can add application columns and an optional text `role` column to the user table. Table names are up to you; keep the TypeScript field names shown here. `snakeCase.table()` maps fields such as `emailVerified` to SQL columns such as `email_verified`. User IDs and account identifiers must be non-null strings. User timestamps must use columns that accept JavaScript `Date` values. * SQLite schema.ts ```ts import { integer, primaryKey, snakeCase, text } from 'drizzle-orm/sqlite-core' export const Users = snakeCase.table('users', { id: text().primaryKey().$defaultFn(() => crypto.randomUUID()), name: text(), email: text().unique(), emailVerified: integer({ mode: 'boolean' }), image: text(), createdAt: integer({ mode: 'timestamp' }).$defaultFn(() => new Date()), updatedAt: integer({ mode: 'timestamp' }).$defaultFn(() => new Date()), }) export type SelectUser = typeof Users.$inferSelect export type InsertUser = typeof Users.$inferInsert export const Accounts = snakeCase.table( 'accounts', { userId: text() .notNull() .references(() => Users.id, { onDelete: 'cascade' }), type: text().notNull(), provider: text().notNull(), providerAccountId: text().notNull(), refreshToken: text(), accessToken: text(), expiresAt: integer(), tokenType: text(), scope: text(), idToken: text(), sessionState: text(), createdAt: integer({ mode: 'timestamp' }).$defaultFn(() => new Date()), }, account => [ primaryKey({ columns: [account.provider, account.providerAccountId], }), ], ) export type SelectAccount = typeof Accounts.$inferSelect export type InsertAccount = typeof Accounts.$inferInsert ``` * PostgreSQL schema.ts ```ts import { boolean, integer, primaryKey, snakeCase, text, timestamp } from 'drizzle-orm/pg-core' export const Users = snakeCase.table('users', { id: text().primaryKey(), name: text(), email: text().unique(), emailVerified: boolean(), image: text(), createdAt: timestamp().defaultNow(), updatedAt: timestamp().defaultNow(), }) export type SelectUser = typeof Users.$inferSelect export type InsertUser = typeof Users.$inferInsert export const Accounts = snakeCase.table( 'accounts', { userId: text().notNull().references(() => Users.id, { onDelete: 'cascade' }), type: text().notNull(), provider: text().notNull(), providerAccountId: text().notNull(), refreshToken: text(), accessToken: text(), expiresAt: integer(), tokenType: text(), scope: text(), idToken: text(), sessionState: text(), createdAt: timestamp().defaultNow(), }, account => [ primaryKey({ columns: [account.provider, account.providerAccountId], }), ], ) export type SelectAccount = typeof Accounts.$inferSelect export type InsertAccount = typeof Accounts.$inferInsert ``` 3. ##### Configure DrizzleKit [Section titled “Configure DrizzleKit”](#configure-drizzlekit) Configure the schema path and database credentials for migrations. * SQLite drizzle.config.ts ```ts import { defineConfig } from 'drizzle-kit' export default defineConfig({ schema: './schema.ts', dialect: 'turso', dbCredentials: { authToken: process.env.TURSO_AUTH_TOKEN!, url: process.env.TURSO_DB_URL!, }, }) ``` * PostgreSQL drizzle.config.ts ```ts import { defineConfig } from 'drizzle-kit' export default defineConfig({ schema: './schema.ts', dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL!, }, }) ``` Now you can run `npx drizzle-kit push` to apply your schema to the database. 4. ##### Set up the Database Client [Section titled “Set up the Database Client”](#set-up-the-database-client) Initialize the Drizzle client with your database connection. * SQLite db.ts ```ts import { createClient } from '@libsql/client' import { drizzle } from 'drizzle-orm/libsql' const client = createClient({ url: process.env.TURSO_DB_URL!, authToken: process.env.TURSO_AUTH_TOKEN!, }) export const db = drizzle({ client }) ``` For Cloudflare D1, create the client from your Worker binding instead: db.ts ```ts import { env } from 'cloudflare:workers' import { drizzle } from 'drizzle-orm/d1' export const db = drizzle(env.DB) ``` * PostgreSQL db.ts ```ts import { drizzle } from 'drizzle-orm/node-postgres' import { Pool } from 'pg' const pool = new Pool({ connectionString: process.env.DATABASE_URL!, }) export const db = drizzle({ client: pool }) ``` 5. ##### Integrate with `createAuth` [Section titled “Integrate with createAuth”](#integrate-with-createauth) Finally, pass the Drizzle client and schema tables to the `DrizzleAdapter` in your `createAuth` configuration. auth.ts ```ts import { DrizzleAdapter } from '@rttnd/gau/adapters/drizzle' import { createAuth } from '@rttnd/gau/core' import { GitHub } from '@rttnd/gau/oauth' import { db } from './db' import { Accounts, Users } from './schema' export const auth = createAuth({ adapter: DrizzleAdapter(db, Users, Accounts), providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), ], jwt: { secret: process.env.AUTH_SECRET!, }, }) export type Auth = typeof auth ``` ### Verification storage [Section titled “Verification storage”](#verification-storage) Email and ChatGPT web sign-in require `Adapter.verification` to store short-lived records. With Drizzle, add this table to your schema, apply the migration, and pass it as the fourth argument to `DrizzleAdapter`. * SQLite schema.ts ```ts import { index, integer, snakeCase, text } from 'drizzle-orm/sqlite-core' export const Verification = snakeCase.table('verification', { id: text().primaryKey(), value: text().notNull(), expiresAt: integer().notNull(), version: integer().notNull(), }, table => [index('verification_expiry').on(table.expiresAt)]) ``` * PostgreSQL schema.ts ```ts import { bigint, index, integer, snakeCase, text } from 'drizzle-orm/pg-core' export const Verification = snakeCase.table('verification', { id: text().primaryKey(), value: text().notNull(), expiresAt: bigint({ mode: 'number' }).notNull(), version: integer().notNull(), }, table => [index('verification_expiry').on(table.expiresAt)]) ``` `expiresAt` stores Unix milliseconds, not a `Date`. auth.ts ```ts import { DrizzleAdapter } from '@rttnd/gau/adapters/drizzle' import { db } from './db' import { Accounts, Users, Verification } from './schema' const adapter = DrizzleAdapter(db, Users, Accounts, Verification) ``` `MemoryAdapter()` includes verification storage for tests and local development. Use shared, persistent storage when running more than one server. Custom adapters must implement `Adapter.verification` with a `set` operation that compares the version and writes the record atomically. ## Upgrading from Drizzle 0.x [Section titled “Upgrading from Drizzle 0.x”](#upgrading-from-drizzle-0x) Upgrade both `drizzle-orm` and `drizzle-kit` to the versions above. The adapter call remains `DrizzleAdapter(db, Users, Accounts)`. * Move `casing: 'snake_case'` from the database client and Kit configuration to `snakeCase.table()` in the schema. Preserve existing SQL column names; explicit column names still work. * Use `drizzle({ client })` for libSQL and PostgreSQL clients. D1 continues to use `drizzle(env.DB)`. * Remove the old `schema` client option. Gau does not require relational queries. If your application uses them, migrate to Drizzle’s `defineRelations()` API and pass `relations` to the client. * Use `timestamp()` in PostgreSQL and `integer({ mode: 'timestamp' })` in SQLite for the user’s `createdAt` and `updatedAt` fields. Text timestamp columns do not meet the adapter’s schema contract. Existing Drizzle Kit migration history also needs the [Drizzle 1.0 upgrade procedure](https://orm.drizzle.team/docs/v0-v1-changes). Changing the adapter alone does not require new auth tables or renamed columns. # Advanced Use Cases > Programmatic sessions and custom authentication flows in gau. Beyond standard OAuth flows, `gau` exposes APIs for building custom auth flows. This includes **programmatic session issuing** for guest logins and invites, and **session refresh** to keep users signed in. ## The `issueSession` method [Section titled “The issueSession method”](#the-issuesession-method) This method lets you programmatically create sessions for any user. It returns both a JWT token and a ready-to-use `Set-Cookie` header. ```ts const { token, cookie, cookieName, maxAge } = await auth.issueSession(userId, { data: { customClaim: 'value' }, // Optional: custom JWT claims ttl: 3600 // Optional: override default TTL (seconds) }) // For web apps: set the cookie response.headers.set('Set-Cookie', cookie) // For native/mobile apps: use the token directly headers: { Authorization: `Bearer ${token}` } ``` This enables use cases that OAuth alone can’t handle: * **Guest sessions** - Let users use your app without signing up, and still save their data in the db * **Invite redemption** - Auto sign-in after claiming an invite * **Device claiming** - Authenticate IoT devices or kiosks * **Magic links / OTP** - Build your own email auth flow Tip For **admin impersonation**, use the built-in [User Impersonation](/advanced/user-impersonation) feature instead of manually issuing sessions with custom claims. It provides proper session management, audit hooks, and secure session restoration. ## The `refreshSession` method [Section titled “The refreshSession method”](#the-refreshsession-method) Extend an existing session’s lifetime by issuing a new token. Preserves all custom claims from the original. You can pass either a raw token string or a `Request` object, which auto-detects cookie vs bearer token. ```ts const refreshed = await auth.refreshSession(request, { ttl: 3600, // Optional: override TTL threshold: 0.5 // Optional: only refresh if past 50% of TTL }) ``` Or a raw token string, and use the `source` field. ```ts const refreshed = await auth.refreshSession(existingToken, { threshold: 0.5 }) if (refreshed) { if (refreshed.source === 'cookie') { response.headers.set('Set-Cookie', refreshed.cookie) } } ``` **Options:** * `ttl` - Override the default TTL for the new token * `threshold` - Only refresh if the session is past this fraction of its TTL (0-1). **Result includes:** * `token` - The new JWT token * `cookie` - Ready-to-use `Set-Cookie` header value * `source` - How the original token was provided: `'cookie'`, `'bearer'`, or `'token'` Returns `null` if the token is invalid, expired, below threshold, or the user no longer exists. For framework-specific middleware helpers (SolidStart/SvelteKit), see the [Middleware](/guides/middleware) guide. *** ## Guest Login [Section titled “Guest Login”](#guest-login) Let users use your app without requiring OAuth sign-up. Later, they can upgrade their guest account by linking an OAuth provider. 1. ##### Create a Guest User [Section titled “Create a Guest User”](#create-a-guest-user) When a user wants to use your app as a guest, create a user record and issue a session. Consider not doing this on page visit, instead require a button click or captcha. routes/api/guest/+server.ts ```ts import { auth } from '$lib/server/auth' export async function POST() { const guestUser = await auth.createUser({ name: `Guest ${Date.now()}`, }) const { cookie } = await auth.issueSession(guestUser.id, { data: { isGuest: true }, ttl: 60 * 60 * 24 * 365, // 1 year }) return new Response(JSON.stringify({ success: true }), { headers: { 'Set-Cookie': cookie }, }) } ``` Tip Consider refreshing the guest session on each visit to keep it alive. See [Keep Sessions Alive](#keep-sessions-alive-optional). If you want guest accounts to persist for a long time, use a large TTL (e.g. 1 year) instead of `0`. Setting `ttl: 0` will cause the session cookie to be deleted immediately by the browser. 2. ##### Detect Guest Users [Section titled “Detect Guest Users”](#detect-guest-users) In your app, check for the guest claim: routes/+layout.server.ts ```ts export async function load({ locals }) { const session = await locals.getSession() return { user: session?.user, isGuest: session?.session?.isGuest === true, } } ``` 3. ##### Keep Sessions Alive (Optional) [Section titled “Keep Sessions Alive (Optional)”](#keep-sessions-alive-optional) Use the middleware helpers to automatically extend sessions on each visit. See the [Middleware](/guides/middleware) guide. 4. ##### Upgrade Guest to Full Account [Section titled “Upgrade Guest to Full Account”](#upgrade-guest-to-full-account) When a guest decides to sign up, they simply sign in with an OAuth provider. Thanks to [Account Linking](/guides/account-linking), their existing user record will be linked to the new provider. Tip You may want to add a name and email to the guest user during the linking process using the `onAfterLinkAccount` hook. *** ## Private Beta Invites (with Auto Sign-In) [Section titled “Private Beta Invites (with Auto Sign-In)”](#private-beta-invites-with-auto-sign-in) Build a closed beta where users need a valid invite token to join. When they redeem their invite, automatically sign them in. 1. ##### Generate Invite Tokens [Section titled “Generate Invite Tokens”](#generate-invite-tokens) Use `auth.signJWT` to create invite tokens. Add a `purpose` claim to distinguish them from session tokens, and a `jti` (JWT ID) for tracking: invites.ts ```ts import { auth } from './auth' export async function createInviteToken() { const token = await auth.signJWT( { purpose: 'beta-invite', jti: crypto.randomUUID(), }, { ttl: 60 * 60 * 24 * 7 }, // Expires in 7 days ) return token } ``` 2. ##### Store Used Tokens [Section titled “Store Used Tokens”](#store-used-tokens) Prevent tokens from being redeemed multiple times by tracking them: * SQLite db/schema.ts ```diff // ... existing Users and Accounts tables +export const UsedInviteTokens = sqliteTable('used_invite_tokens', { jti: text().primaryKey(), usedAt: integer({ mode: 'timestamp' }).$defaultFn(() => new Date()), +}); ``` * PostgreSQL db/schema.ts ```diff // ... existing Users and Accounts tables +export const UsedInviteTokens = pgTable('used_invite_tokens', { jti: text().primaryKey(), usedAt: timestamp().defaultNow(), +}); ``` 3. ##### Verify and Redeem Invite [Section titled “Verify and Redeem Invite”](#verify-and-redeem-invite) When a user redeems their invite, verify the token, create their account, and immediately issue a session: routes/api/redeem-invite/+server.ts ```ts import { auth } from '$lib/server/auth' import { db } from '$lib/server/db' import { UsedInviteTokens } from '$lib/server/db/schema' export async function POST({ request }) { const { token, email } = await request.json() // Verify the invite token const payload = await auth.verifyJWT<{ purpose?: string, jti?: string }>(token) if (payload?.purpose !== 'beta-invite' || !payload.jti) return new Response('Invalid invite token', { status: 400 }) // Check if token was already used const used = await db.query.UsedInviteTokens.findFirst({ where: (t, { eq }) => eq(t.jti, payload.jti), }) if (used) return new Response('Invite already used', { status: 400 }) // Check if email already exists const existing = await auth.getUserByEmail(email) if (existing) return new Response('Email already registered', { status: 400 }) // Create user and mark token as used const newUser = await db.transaction(async (tx) => { await tx.insert(UsedInviteTokens).values({ jti: payload.jti! }) return await auth.createUser({ email }) }) // Auto sign-in: issue a session immediately const { cookie } = await auth.issueSession(newUser.id, { data: { inviteRedeemed: true }, }) return new Response(JSON.stringify({ success: true }), { headers: { 'Set-Cookie': cookie }, }) } ``` 4. ##### Complete the Profile [Section titled “Complete the Profile”](#complete-the-profile) The user is now signed in! Prompt them to complete their profile or link an OAuth provider for easier future logins. *** ## Device / Kiosk Authentication [Section titled “Device / Kiosk Authentication”](#device--kiosk-authentication) Authenticate devices that can’t perform OAuth flows (IoT devices, kiosks, POS terminals). 1. ##### Generate a Claim Code [Section titled “Generate a Claim Code”](#generate-a-claim-code) Create a short-lived, human-readable code for the device: devices.ts ```ts export async function generateClaimCode(deviceId: string) { const code = Math.random().toString(36).substring(2, 8).toUpperCase() // Store code -> deviceId mapping with expiration await redis.setex(`claim:${code}`, 300, deviceId) // 5 min expiry return code } ``` 2. ##### User Claims the Device [Section titled “User Claims the Device”](#user-claims-the-device) The user enters the code on your web app (where they’re already authenticated): routes/api/claim-device/+server.ts ```ts export async function POST({ request, locals }) { const session = await locals.getSession() if (!session?.user) return new Response('Unauthorized', { status: 401 }) const { code } = await request.json() const deviceId = await redis.get(`claim:${code}`) if (!deviceId) return new Response('Invalid or expired code', { status: 400 }) // Issue a long-lived session for the device const { token } = await auth.issueSession(session.user.id, { data: { deviceId, isDevice: true }, ttl: 60 * 60 * 24 * 365, // 1 year }) // Delete the claim code await redis.del(`claim:${code}`) // Send token to device via your preferred method await notifyDevice(deviceId, { token }) return new Response(JSON.stringify({ success: true })) } ``` 3. ##### Device Uses Token [Section titled “Device Uses Token”](#device-uses-token) The device stores the token and uses it for all API requests: ```ts fetch('https://api.example.com/data', { headers: { Authorization: `Bearer ${storedToken}` } }) ``` *** ## JWT Configuration [Section titled “JWT Configuration”](#jwt-configuration) You can configure JWT behavior globally in `createAuth`: auth.ts ```ts export const auth = createAuth({ // ... jwt: { secret: process.env.AUTH_SECRET, algorithm: 'ES256', // or 'HS256' ttl: 3600 * 24 * 7, // Default: 7 days iss: 'my-app', // Issuer claim aud: 'my-audience', // Audience claim }, }) ``` The `issueSession` method respects these defaults but allows per-call overrides via the `ttl` option. See the [`jwt`](/guides/configuration#jwt) option in the configuration guide for more details. # User Impersonation > Allow admins or support staff to sign in as other users for debugging or customer support. Impersonation allows admins or support to sign in as other users for debugging, customer support, or demonstrations. ## Configuration [Section titled “Configuration”](#configuration) Enable impersonation in your `createAuth` configuration: auth.ts ```diff export const auth = createAuth({ adapter, providers, roles: { adminRoles: ['admin'], adminUserIds: ['special-admin-id'], }, impersonation: { enabled: true, allowedRoles: ['admin', 'support'], cannotImpersonate: ['admin'], maxTTL: 3600, +onImpersonate: ({ adminUserId, targetUserId, reason, timestamp }) => { +console.log(`Audit: ${adminUserId} impersonated ${targetUserId}: ${reason}`) }, }, }) ``` ### Options [Section titled “Options”](#options) * `enabled` - Set to `true` to enable the impersonation feature * `allowedRoles` - Array of roles that can impersonate others (defaults to `adminRoles`) * `cannotImpersonate` - Array of roles that cannot be impersonated (defaults to `adminRoles`) * `maxTTL` - Maximum duration of an impersonation session in seconds (default: 3600) * `onImpersonate` - Hook called when impersonation starts ## Starting Impersonation [Section titled “Starting Impersonation”](#starting-impersonation) Use the `startImpersonation` method to begin impersonating a user. `reason` is optional, it’s passed to the `onImpersonate` hook for auditing purposes, you can also override the default `ttl` here. routes/api/admin/impersonate/+server.ts ```ts import { auth } from '$lib/server/auth' export async function POST({ request, locals }) { const session = await locals.getSession() if (!session?.user?.isAdmin) return new Response('Forbidden', { status: 403 }) const { targetUserId, reason } = await request.json() const result = await auth.startImpersonation(session.user.id, targetUserId, { reason, ttl: 60 * 30, }) if (!result) return new Response('Impersonation failed', { status: 500 }) return new Response(JSON.stringify({ success: true }), { headers: [ ['Set-Cookie', result.cookie], ['Set-Cookie', result.originalCookie], ], }) } ``` ### Result [Section titled “Result”](#result) * `token` - The impersonation session JWT containing `impersonatedBy` and `impersonationExpiresAt` claims * `cookie` - Set-Cookie header for the impersonation session * `originalCookie` - Set-Cookie header to stash the admin’s session info * `maxAge` - The actual TTL used (capped by maxTTL) ## Detecting Impersonation [Section titled “Detecting Impersonation”](#detecting-impersonation) Check if the current session is an impersonation session: routes/+layout.server.ts ```ts import { isImpersonating } from '@rttnd/gau' export async function load({ locals }) { const session = await locals.getSession() return { user: session?.user, isImpersonating: isImpersonating(session?.session), impersonatedBy: session?.session?.impersonatedBy, } } ``` Use this to show a visual indicator in your UI that the user is in impersonation mode. ## Ending Impersonation [Section titled “Ending Impersonation”](#ending-impersonation) Restore the admin’s original session when they’re done: routes/api/admin/unimpersonate/+server.ts ```ts import { auth } from '$lib/server/auth' export async function POST({ request }) { const result = await auth.endImpersonation(request) if (!result) return new Response('No active impersonation', { status: 400 }) const headers = new Headers() headers.set('Set-Cookie', result.cookie) result.clearCookies.forEach(cookie => headers.append('Set-Cookie', cookie)) return new Response(JSON.stringify({ success: true }), { headers }) } ``` ### Result [Section titled “Result”](#result-1) * `token` - The restored admin session JWT * `cookie` - Set-Cookie header for the restored session * `clearCookies` - Array of Set-Cookie headers to clear the stash cookie ## Best Practices [Section titled “Best Practices”](#best-practices) Caution It’s a good idea to always validate admin status in your route handlers before calling `startImpersonation`. * Log all impersonation events via the `onImpersonate` hook * Set a reasonable `maxTTL` * Display a clear visual indicator when impersonation is active # Xbox / Minecraft tokens > Capture Microsoft tokens, exchange for Xbox and Minecraft tokens, and gate linking by entitlement. This guide shows how to: * Request Microsoft scopes that allow Xbox sign-in * Capture the Microsoft tokens at callback * Exchange them for Xbox Live (XBL), XSTS, then Minecraft tokens * Optionally gate linking on ownership (entitlements) You will need a Microsoft Entra ID app with `XboxLive.signin` permission. ### Auth config [Section titled “Auth config”](#auth-config) Define a Microsoft `profile` with `XboxLive.signin` (and `offline_access` if you want refresh tokens). Profiles let you choose scopes safely from the client by name. Optionally, keep regular Microsoft auth (`User.Read`) in a separate profile. auth.ts ```ts import { createAuth, json } from '@rttnd/gau/core' import { Microsoft } from '@rttnd/gau/oauth' export const auth = createAuth({ providers: [ Microsoft({ clientId: process.env.MICROSOFT_CLIENT_ID!, clientSecret: process.env.MICROSOFT_CLIENT_SECRET!, }) ], profiles: { microsoft: { xbox: { scopes: ['XboxLive.signin', 'offline_access'], linkOnly: true }, graph: { scopes: ['User.Read'] }, }, }, onOAuthExchange: async (ctx) => { // Example: perform the full chain here and return tokens to the client const msAccess = ctx.tokens.accessToken() const xbl = await xblAuthenticate(msAccess) const xsts = await xstsAuthorize(xbl.Token) const mc = await minecraftLogin(xsts.Token, xsts.DisplayClaims?.xui?.[0]?.uhs) return { handled: true, response: json({ microsoft: { access: msAccess }, xbl, xsts, minecraft: mc, }) } }, }) ``` Tip Mark the `xbox` profile as `linkOnly: true` to ensure users can only link it to an existing account and not sign in directly with Microsoft using that profile. Note When you short-circuit with `onOAuthExchange`, gau clears temporary cookies (CSRF/PKCE/callbackUri) and returns your custom response. ### Server helpers (example) [Section titled “Server helpers (example)”](#server-helpers-example) These minimal helpers demonstrate the HTTP calls. You can inline them, move to a module, or adapt to your runtime. server/xbox.ts ```ts const XBL_AUTH = 'https://user.auth.xboxlive.com/user/authenticate' const XSTS_AUTH = 'https://xsts.auth.xboxlive.com/xsts/authorize' const MC_LOGIN = 'https://api.minecraftservices.com/authentication/login_with_xbox' const MC_ENTITLEMENTS = 'https://api.minecraftservices.com/entitlements/license' const MC_PROFILE = 'https://api.minecraftservices.com/minecraft/profile' export async function xblAuthenticate(msAccess: string) { const body = { Properties: { AuthMethod: 'RPS', SiteName: 'user.auth.xboxlive.com', RpsTicket: `d=${msAccess}` }, RelyingParty: 'http://auth.xboxlive.com', TokenType: 'JWT', } const res = await fetch(XBL_AUTH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) if (!res.ok) throw new Error('XBL auth failed') return res.json() } export async function xstsAuthorize(xblToken: string) { const body = { Properties: { SandboxId: 'RETAIL', UserTokens: [xblToken] }, RelyingParty: 'rp://api.minecraftservices.com/', TokenType: 'JWT', } const res = await fetch(XSTS_AUTH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) if (!res.ok) throw new Error('XSTS auth failed') return res.json() } export async function minecraftLogin(xstsToken: string, uhs?: string) { const body = { identityToken: `XBL3.0 x=${uhs};${xstsToken}` } const res = await fetch(MC_LOGIN, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) if (!res.ok) throw new Error('MC login failed') return res.json() } export async function minecraftEntitlements(mcAccess: string) { const url = `${MC_ENTITLEMENTS}` const res = await fetch(url, { headers: { Authorization: `Bearer ${mcAccess}` } }) if (!res.ok) throw new Error('MC entitlements failed') return res.json() } export async function minecraftProfile(mcAccess: string) { const res = await fetch(MC_PROFILE, { headers: { Authorization: `Bearer ${mcAccess}` } }) if (!res.ok) throw new Error('MC profile failed') return res.json() } ``` ### Gate linking by ownership [Section titled “Gate linking by ownership”](#gate-linking-by-ownership) If you prefer to keep gau’s default linking/session behavior, move the ownership check into `onBeforeLinkAccount`. auth.ts ```ts export const auth = createAuth({ // ... onBeforeLinkAccount: async ({ tokens }) => { const msAccess = tokens.accessToken() const xbl = await xblAuthenticate(msAccess) const xsts = await xstsAuthorize(xbl.Token) const mc = await minecraftLogin(xsts.Token, xsts.DisplayClaims?.xui?.[0]?.uhs) // Example: require at least one entitlement const ent = await minecraftEntitlements(mc.access_token) const owns = Array.isArray(ent.items) && ent.items.length > 0 if (!owns) return { allow: false, response: json({ error: 'No Minecraft entitlement' }, { status: 403 }) } return { allow: true } }, }) ``` ### Client side [Section titled “Client side”](#client-side) * For desktop/mobile redirects with different hosts, gau uses token-based return and ships a small HTML auto-close page. * For Tauri, set `redirectTo` to a custom scheme, e.g. `gau://oauth/callback`, and use the runtime helpers. #### Selecting the Xbox profile [Section titled “Selecting the Xbox profile”](#selecting-the-xbox-profile) * Client: `auth.signIn('microsoft', { profile: 'xbox' })` * URL: `GET /api/auth/login/microsoft?profile=xbox` # Account Linking > How gau can link multiple OAuth providers to a single user account, either automatically or manually. `gau` allows users to connect multiple OAuth providers (e.g., GitHub and Google) to a single user account. This provides a seamless experience, allowing them to sign in with any of their linked providers to access the same account. This can be done in two ways: * **Automatic Linking**: `gau` can automatically link accounts based on a shared, verified email address when a user signs in. * **Manual Linking**: Authenticated users can manually link or unlink providers from their account settings page. ## Automatic Account Linking [Section titled “Automatic Account Linking”](#automatic-account-linking) `gau` has an **automatic account linking** feature, configured via the `autoLink` option in `createAuth`. When an existing user signs in with a new provider, `gau` can automatically associate this new sign-in with their existing account. ### How It Works [Section titled “How It Works”](#how-it-works) 1. **Check for Existing Account**: When a user signs in, `gau` first checks if an account already exists for that user with the specific OAuth provider. If so, it logs them in. 2. **Check for Email to Link**: If no account is found for that provider, `gau` looks at the email address returned by the new OAuth provider. It then checks if any user in your database already has that same email address. 3. **Link or Create**: * If a user with that email already exists, `gau` will **link** the new OAuth sign-in to that existing user record. * If no user with that email is found, a **new user** is created. This means a user can sign in with GitHub, sign out, and then sign back in with Google, and they will be logged into the same account, as long as both services use the same verified email address. ### Configuration [Section titled “Configuration”](#configuration) You can control this behavior with the [`autoLink`](/guides/configuration#autolink) option in `createAuth`. * **`'verifiedEmail'` (Default)**: Only links if the new provider’s email is verified. * **`'always'`**: Links if emails match, even if not verified. * **`false`**: No auto-linking. Each new provider creates a new user. ## Manual Account Linking [Section titled “Manual Account Linking”](#manual-account-linking) In addition to automatic linking, `gau` provides functions for users to manually manage their linked accounts, for example from a profile or settings page. The `useAuth` hook (available in client integrations) exposes `linkAccount` and `unlinkAccount` functions. ### Configuration [Section titled “Configuration”](#configuration-1) You can control manual linking behavior with the [`allowDifferentEmails`](/guides/configuration#allowdifferentemails) and [`updateUserInfoOnLink`](/guides/configuration#updateuserinfoonlink) options in `createAuth`. ### `linkAccount` [Section titled “linkAccount”](#linkaccount) This function initiates the OAuth flow to connect an additional provider to the currently logged-in user’s account. * SvelteKit +page.svelte ```svelte ``` * SolidStart LinkButton.tsx ```tsx import { useAuth } from '@rttnd/gau/client/solid' export function LinkButton() { const { linkAccount } = useAuth() return ( ) } ``` ### `unlinkAccount` [Section titled “unlinkAccount”](#unlinkaccount) This function removes a linked provider from the user’s account. * SvelteKit +page.svelte ```svelte ``` * SolidStart UnlinkButton.tsx ```tsx import { useAuth } from '@rttnd/gau/client/solid' export function UnlinkButton() { const { unlinkAccount } = useAuth() return ( ) } ``` `gau` prevents users from unlinking their last remaining account. Caution #### Unlinking Behavior [Section titled “Unlinking Behavior”](#unlinking-behavior) By default, `gau` allows linking accounts even if they have different primary email addresses. You can enforce matching emails during manual linking with the `allowDifferentEmails: false` configuration. When an account is unlinked, `gau` will currently reset the user’s `email` to `null` and `emailVerified` to `false`. Because of this, it’s a good practice to prompt the user to sign in again after unlinking an account. A new sign-in will automatically re-populate the user’s primary email from one of the remaining linked accounts. This behavior will be better and configurable in a future update. # Configuration > A deep-dive into all the createAuth options. The core of `gau` is the `createAuth` function, which you use to configure your auth setup. This guide covers all the available options in detail. Here’s a quick overview of a typical configuration: auth.ts ```ts import { DrizzleAdapter } from '@rttnd/gau/adapters/drizzle' import { createAuth } from '@rttnd/gau/core' import { GitHub } from '@rttnd/gau/oauth' import { db } from './db' import { Accounts, Users } from './db/schema' export const auth = createAuth({ adapter: DrizzleAdapter(db, Users, Accounts), providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }), ], jwt: { secret: process.env.AUTH_SECRET, }, }); ``` ## adapter [Section titled “adapter”](#adapter) > **Type**: `Adapter`, **Required**: Yes The `adapter` is responsible for connecting `gau` to your database and handling db operations. See the [adapters](/adapters) guide. ## providers [Section titled “providers”](#providers) > **Type**: `OAuthProvider[]`, **Required**: Yes This option takes an array of configured OAuth providers that your application will support. Each provider typically needs a `clientId` and `clientSecret`. ```ts import { GitHub, Google } from '@rttnd/gau/oauth' // ... providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), ], ``` You can pass additional provider-specific options using `params`. Check out the [OAuth Providers](/providers) section for a list of available providers and their specific configuration. ## profiles [Section titled “profiles”](#profiles) > Type: `{ [providerId: string]: { [profileName: string]: { scopes?: string[], redirectUri?: string, params?: Record } & ProviderSpecificOverrides } }`\ > Default: `{}` Define server-controlled, per-provider `profiles` to group auth provider configs. A profile can: * Override `scopes` used when building the provider authorization URL * Override the provider `redirectUri` (useful for desktop/mobile custom schemes) * Include provider-specific, strongly typed overrides (e.g. `tenant`, `prompt` for Microsoft) * Add arbitrary authorization `params` (appended to the provider auth URL) Profiles are selected by name at login/link time, either via the client helpers or a `?profile=` query parameter. If an unknown profile is requested, `gau` returns `400`. Example configuration: auth.ts ```ts import { createAuth } from '@rttnd/gau/core' import { GitHub } from '@rttnd/gau/oauth' export const auth = createAuth({ providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET! }), ], profiles: { github: { // Minimal sign-in with user identity basic: { scopes: ['read:user', 'user:email'] }, // Elevated permissions for repo automation maintainer: { scopes: ['read:user', 'user:email', 'repo', 'read:org'] }, }, }, }) ``` Select a profile when starting a flow: * Client: `auth.signIn('github', { profile: 'maintainer' })` * URL: `GET /api/auth/login/github?profile=maintainer` Security notes: * Profiles are defined on the server; the client can only choose by name. Unknown names are rejected. * Use profiles to constrain allowed scope sets, this avoids exposing arbitrary scopes to the client. * If you use a custom `redirectUri` for desktop/mobile flows, set it in the profile and configure `trustHosts` appropriately for your return hosts. * Avoid putting secrets into `params` — they become part of the URL. * Precedence for settings (when supported by a provider like Microsoft): * profile override (typed) > query parameter (e.g. `?prompt=...`) > provider default ### Extra params [Section titled “Extra params”](#extra-params) Some providers accept extra authorization parameters via the provider config: ```ts GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, params: { allow_signup: 'false' }, }) ``` Profile `params` are merged on top of provider `params` for the selected profile. ## basePath [Section titled “basePath”](#basepath) > **Type**: `string`, **Default**: `'/api/auth'` Defines the relative path for all authentication API endpoints. You typically don’t need to change this. The resulting URL is `{baseURL}/{basePath}/{provider}`. If your frontend and backend are on different hosts, look for the `baseURL` config in the framework specific guides. ## session [Section titled “session”](#session) > **Type**: `object`, **Default**: `{}` This object allows you to configure session management. ### session.strategy [Section titled “session.strategy”](#sessionstrategy) > **Type**: `'auto' | 'cookie' | 'token'`, **Default**: `'auto'` Determines how sessions are handled. See the [Session Management](/guides/session-management#session-strategy) guide for a detailed explanation of the different strategies. ## hooks [Section titled “hooks”](#hooks) These hooks let you customize the OAuth callback lifecycle, see [Hooks](/guides/hooks/). ## onError [Section titled “onError”](#onerror) > **Type**: `(context: { error: GauError, request: Request }) => Response | Promise | undefined` **Required**: No A hook to intercept and handle errors. See [Error Handling](/guides/error-handling#onerror-hook). ## errorRedirect [Section titled “errorRedirect”](#errorredirect) > **Type**: `string` **Required**: No The URL to redirect to for user-facing errors. See [Error Handling](/guides/error-handling#errorredirect-option). ## jwt [Section titled “jwt”](#jwt) > **Type**: `object`, **Default**: `{}` This object allows you to configure JSON Web Token settings for session management. ### jwt.secret [Section titled “jwt.secret”](#jwtsecret) > **Type**: `string`, **Required**: Yes The secret used for signing and verifying JWTs. For `HS256`, this can be a simple string. For the default and recommended `ES256`, this must be a **base64url-encoded PKCS#8 private key**. You can generate a valid secret using the CLI: * npm ```sh npx gau secret ``` * pnpm ```sh pnpm dlx gau secret ``` * yarn ```sh yarn dlx gau secret ``` * bun ```sh bunx gau secret ``` * ni ```sh nlx gau secret ``` ### jwt.algorithm [Section titled “jwt.algorithm”](#jwtalgorithm) > **Type**: `'ES256' | 'HS256'`, **Default**: `'ES256'` The signing algorithm to use. `ES256` is the default and recommended. It uses a private/public key pair, which is more secure: only your server can sign tokens, but anyone with the public key can verify them. `HS256` uses a single shared secret and is slightly faster, but both the signer and verifier need the same key. ### jwt.privateKey [Section titled “jwt.privateKey”](#jwtprivatekey) > **Type**: `CryptoKey`, **Required**: No The private key to use for signing JWTs, only with `ES256`. By default, this is derived from `jwt.secret`. ### jwt.ttl [Section titled “jwt.ttl”](#jwtttl) > **Type**: `number`, **Default**: `3600 * 24 * 7` (7 days) The time-to-live for session JWTs, in seconds. This determines how long a user stays logged in before needing to re-authenticate. Session Duration The default is 7 days, which works well for most apps. For apps where you want users to stay logged in longer, you can safely increase this to 30 days or more. ### jwt.iss, jwt.aud, and jwt.sub [Section titled “jwt.iss, jwt.aud, and jwt.sub”](#jwtiss-jwtaud-and-jwtsub) > **Type**: `string | string[]` Standard JWT claims for “issuer” (`iss`) and “audience” (`aud`). These are useful in multi-tenant applications or when your API needs to serve different clients. `gau` allows you to set default values for them here, and will automatically validate these claims when verifying a token. ## cookies [Section titled “cookies”](#cookies) > **Type**: `cookie.SerializeOptions`, **Default**: `{ path: '/', sameSite: 'lax', secure: true, httpOnly: true }` Allows you to customize the options for the session cookie. You can override the defaults. For example: ```ts cookies: { sameSite: 'strict', }, ``` ## trustHosts [Section titled “trustHosts”](#trusthosts) > **Type**: `'all' | string[]`, **Default**: `[]` This controls which domains are allowed to make requests to your auth endpoints and where users can be redirected after login. 1. **CSRF Protection**: Only origins in this list can make `POST` requests (like signing out). 2. **Redirect Validation**: The `redirectTo` parameter is only honored if it points to a trusted host. The `Origin` header of incoming requests and the host of redirect URLs are checked against this list. * **`[]` (Default)**: Only requests and redirects from/to the same origin as the app are allowed. Note In development, `localhost` and `127.0.0.1` are automatically trusted. * **`['app.example.com', 'tauri.localhost']`**: For multi-domain setups or Tauri apps, you must add the allowed hostnames. * **`'all'`**: This is a convenient shortcut for development that disables these checks, but it is **not recommended for production**. See the [Security](/guides/security) guide for more details. ## cors [Section titled “cors”](#cors) > **Type**: `boolean | CORSOptions`, **Default**: `true` Configures Cross-Origin Resource Sharing (CORS) for all auth endpoints. * `true` (default): Enables CORS, defaults: * `allowedOrigins: 'all'` (all origins allowed) * `allowCredentials: true` (credentials reflected, origin echoed — never `*`) * `allowedHeaders: ['Content-Type', 'Authorization', 'Cookie']` * `allowedMethods: ['GET', 'POST', 'OPTIONS']` * `false`: Disables CORS completely (no CORS headers are added). * `CORSOptions`: Manually configure CORS headers. Options: * `allowedOrigins: 'all'`: Allow any origin. When `allowCredentials: false`, the response uses `Access-Control-Allow-Origin: *`. When credentials are enabled, the requesting origin is echoed. * `allowedOrigins: 'trust'`: Reuses your `trustHosts` list. If `trustHosts: 'all'`, every origin is allowed (origin echoed). * `allowedOrigins: string[]`: Provide an explicit allow-list. Items can be full origins (`https://app.example.com`), bare hostnames (`app.example.com`), host:port, or `*` (which allows any origin but still echoes the caller instead of using `*`). * `allowCredentials`: Toggles the `Access-Control-Allow-Credentials` header. If `false` and `allowedOrigins === 'all'`, wildcard `*` is used; otherwise origin is echoed without the credentials header. * `allowedHeaders`: Array of request headers you want to allow (sent as a comma‑separated list). * `allowedMethods`: HTTP methods allowed for preflight / actual requests. * `exposeHeaders`: Extra response headers accessible to the browser (sets `Access-Control-Expose-Headers`). * `maxAge`: Seconds browsers may cache the preflight response. ## autoLink [Section titled “autoLink”](#autolink) > **Type**: `'verifiedEmail' | 'always' | false`, **Default**: `'verifiedEmail'` Controls how `gau` does automatic account linking. Read the [Account Linking](/guides/account-linking) guide for a full explanation. * **`'verifiedEmail'` (Default)**: On sign-in with a new provider, `gau` checks if the email is verified. If a user with that email already exists, the new account is linked to them. Otherwise, a new user is created. * **`'always'`**: Same as above, but links even if the provider doesn’t mark the email as verified. * **`false`**: Never auto-link. Every new provider sign-in creates a new user. ## allowDifferentEmails [Section titled “allowDifferentEmails”](#allowdifferentemails) > **Type**: `boolean`, **Default**: `true` Controls whether a user can manually link a provider whose primary email differs from the user’s current primary email. * `true` (default): Allow linking even if the emails differ. * `false`: Reject manual linking with a 400 error when the emails differ. This is only enforced during manual linking; it does not affect the auto-link checks above. ## updateUserInfoOnLink [Section titled “updateUserInfoOnLink”](#updateuserinfoonlink) > **Type**: `boolean`, **Default**: `false` When enabled, during manual linking gau will update the user’s profile fields from the new provider (e.g. `name`, `image`). If the provider’s email matches the user’s current email and is verified, gau will also set `emailVerified: true`. ## roles [Section titled “roles”](#roles) > **Type**: `object`, **Default**: `{}` This object allows you to configure role-based access control. When configured, gau derives `session.user.isAdmin` for every validated session using the rules below. ### roles.defaultRole [Section titled “roles.defaultRole”](#rolesdefaultrole) > **Type**: `string`, **Default**: `'user'` The default role assigned to newly created users. ### roles.resolveOnCreate [Section titled “roles.resolveOnCreate”](#rolesresolveoncreate) > **Type**: `(context: { providerId: string, profile: any, request: Request }) => string | undefined`, **Required**: No A function to dynamically resolve the role for a new user at creation time. Return `undefined` to fall back to `defaultRole`. ### roles.adminRoles [Section titled “roles.adminRoles”](#rolesadminroles) > **Type**: `string[]`, **Default**: `['admin']` An array of role names that are considered admin roles for helper predicates and for the derived `session.user.isAdmin` flag. ### roles.adminUserIds [Section titled “roles.adminUserIds”](#rolesadminuserids) > **Type**: `string[]`, **Default**: `[]` An array of user IDs that are always treated as admin for helper predicates and `session.user.isAdmin`. For a full guide, see [Role-Based Access Control](/guides/role-based-access-control). ## linkOnly [Section titled “linkOnly”](#linkonly) > **Type**: `boolean` **Default**: `false` When enabled the provider or the profile can only be used for linking to an existing user account, not for signing in or signing up. * Provider level: pass `linkOnly: true` to the provider config. * Profile level: set `linkOnly: true` on a specific profile. When a user completes an OAuth callback without an existing session for a link-only provider/profile, gau returns a 400 with a friendly message. # Error Handling > Understanding and customizing error handling in gau. This guide explains how to handle `gau` errors. ## GauError Class [Section titled “GauError Class”](#gauerror-class) All errors thrown by gau handlers are `GauError` instances with a specific error code. You can import and use these in your own code: ```ts import { GauError, ErrorCodes } from '@rttnd/gau' // Using default message and status throw new GauError(ErrorCodes.UNAUTHORIZED) // With options only throw new GauError(ErrorCodes.CSRF_INVALID, { redirectUrl: '/login' }) // With custom message throw new GauError(ErrorCodes.PROVIDER_NOT_FOUND, `Provider "${id}" not found`) ``` ## Configuring Error Handling [Section titled “Configuring Error Handling”](#configuring-error-handling) ### onError Hook [Section titled “onError Hook”](#onerror-hook) The `onError` hook lets you customize how errors are handled. You can log errors, modify responses, or redirect users to specific pages based on the error type. ```ts import { createAuth, ErrorCodes, redirect } from '@rttnd/gau' const auth = createAuth({ // ... other options onError: ({ error, request }) => { // Log all auth errors console.error('Auth error:', error.code, error.message) // Return a Response to override default behavior if (error.code === ErrorCodes.CSRF_INVALID) { return redirect('/login?error=session_expired') } // Return undefined to fall back to errorRedirect or default behavior }, }) ``` ### errorRedirect Option [Section titled “errorRedirect Option”](#errorredirect-option) For user-facing errors (OAuth flow), you can specify a URL to redirect to. Error details are passed as query parameters. ```ts const auth = createAuth({ // Redirect user-facing errors to your custom error page errorRedirect: '/auth/error', }) ``` Note For SvelteKit and SolidStart integrations, `errorRedirect` defaults to `/auth/error` automatically. ## Error Response Formats [Section titled “Error Response Formats”](#error-response-formats) Gau uses **route-based detection** to determine whether to return HTML or JSON: | Request Type | Example Routes | Default Response | | ------------ | ------------------------------------------- | ------------------------------------------------ | | User-facing | `GET /:provider`, `GET /callback/:provider` | HTML error page or redirect | | API | `GET /session`, `POST /signout` | JSON: `{ error: 'message', code: 'ERROR_CODE' }` | ### Response Priority [Section titled “Response Priority”](#response-priority) 1. If `onError` returns a `Response`, that’s used 2. For user-facing requests with `errorRedirect` set, redirects with query params 3. Otherwise, renders a styled HTML error page (user-facing) or returns JSON (API) ## Creating an Error Page [Section titled “Creating an Error Page”](#creating-an-error-page) When using `errorRedirect`, gau passes these query parameters: * `code` - The error code (e.g., `CSRF_INVALID`) * `message` - Human-readable error message * `status` - HTTP status code * `redirect` - Optional URL to redirect back to - SvelteKit src/routes/auth/error/+page.svelte ```svelte Authentication Error

Authentication Error

{message}

{#if code}

{code}

{/if} Go back
``` - SolidStart src/routes/auth/error.tsx ```tsx import { useSearchParams } from '@solidjs/router' import type { ErrorCode } from '@rttnd/gau' export default function AuthErrorPage() { const [params] = useSearchParams() const code = () => params.code as ErrorCode | undefined const message = () => params.message const redirect = () => params.redirect || '/' return (

Authentication Error

{message()}

{code() &&

{code()}

} Go back
) } ``` ## Handling Errors in Client Code [Section titled “Handling Errors in Client Code”](#handling-errors-in-client-code) When making API calls, errors are returned as JSON: ```ts const response = await fetch('/api/auth/session') const data = await response.json() if ('error' in data) { console.error('Error:', data.code, data.error) // data.code is the error code (e.g., 'UNAUTHORIZED') // data.error is the human-readable message } ``` ### Checking for Specific Errors [Section titled “Checking for Specific Errors”](#checking-for-specific-errors) ```ts import { ErrorCodes } from '@rttnd/gau' if (data.code === ErrorCodes.UNAUTHORIZED) { // Redirect to login } if (data.code === ErrorCodes.ACCOUNT_ALREADY_LINKED) { // Show "account already linked" message } ``` ## Error Codes Reference [Section titled “Error Codes Reference”](#error-codes-reference) ### OAuth Flow Errors [Section titled “OAuth Flow Errors”](#oauth-flow-errors) | Code | Default Message | Status | | -------------------------- | ---------------------------------- | ------ | | `CSRF_INVALID` | Invalid CSRF token | 403 | | `PKCE_MISSING` | Missing PKCE code verifier | 400 | | `PKCE_CHALLENGE_MISSING` | Missing PKCE challenge | 400 | | `OAUTH_CANCELLED` | Authentication was cancelled | 200 | | `PROVIDER_NOT_FOUND` | Provider not found | 400 | | `AUTHORIZATION_URL_FAILED` | Could not create authorization URL | 500 | ### Account/User Errors [Section titled “Account/User Errors”](#accountuser-errors) | Code | Default Message | Status | | ---------------------------- | ---------------------------------------------------- | ------ | | `USER_NOT_FOUND` | User not found | 404 | | `USER_CREATE_FAILED` | Failed to create user | 500 | | `ACCOUNT_ALREADY_LINKED` | Account already linked to another user | 409 | | `ACCOUNT_LINK_FAILED` | Failed to link account | 500 | | `ACCOUNT_NOT_LINKED` | Account not linked | 400 | | `CANNOT_UNLINK_LAST_ACCOUNT` | Cannot unlink the last account | 400 | | `EMAIL_ALREADY_EXISTS` | An account with this email already exists | 409 | | `EMAIL_MISMATCH` | Email mismatch between existing account and provider | 400 | | `LINKING_NOT_ALLOWED` | Linking not allowed | 403 | | `LINK_ONLY_PROVIDER` | Sign-in with this provider is disabled… | 400 | ### Session/Auth Errors [Section titled “Session/Auth Errors”](#sessionauth-errors) | Code | Default Message | Status | | --------------------------- | -------------------------- | ------ | | `UNAUTHORIZED` | Unauthorized | 401 | | `FORBIDDEN` | Forbidden | 403 | | `SESSION_INVALID` | Invalid session | 400 | | `SESSION_VALIDATION_FAILED` | Failed to validate session | 500 | ### Token Errors [Section titled “Token Errors”](#token-errors) | Code | Default Message | Status | | ----------------------- | --------------------- | ------ | | `TOKEN_INVALID` | Invalid token | 400 | | `TOKEN_EXPIRED` | Token expired | 400 | | `CODE_VERIFIER_INVALID` | Invalid code verifier | 400 | ### Request Errors [Section titled “Request Errors”](#request-errors) | Code | Default Message | Status | | ---------------------- | ----------------------- | ------ | | `NOT_FOUND` | Not found | 404 | | `METHOD_NOT_ALLOWED` | Method not allowed | 405 | | `INVALID_REQUEST` | Invalid request | 400 | | `INVALID_REDIRECT_URL` | Invalid redirect URL | 400 | | `UNTRUSTED_HOST` | Untrusted redirect host | 400 | | `UNKNOWN_PROFILE` | Unknown profile | 400 | ### Internal Errors [Section titled “Internal Errors”](#internal-errors) | Code | Default Message | Status | | ---------------- | ---------------------------- | ------ | | `INTERNAL_ERROR` | An unexpected error occurred | 500 | ## Disabling Error Redirect [Section titled “Disabling Error Redirect”](#disabling-error-redirect) If you prefer gau’s built-in styled error pages instead of redirecting, set `errorRedirect` to `undefined`: ```ts const auth = createAuth({ errorRedirect: undefined, // Use gau's built-in error pages }) ``` # Getting Started > How to install and set up gau for the first time. This guide will walk you through installing `@rttnd/gau` and setting it up for the first time. 1. ##### Install the package [Section titled “Install the package”](#install-the-package) * npm ```sh npm i @rttnd/gau ``` * pnpm ```sh pnpm add @rttnd/gau ``` * yarn ```sh yarn add @rttnd/gau ``` * bun ```sh bun add @rttnd/gau ``` * ni ```sh ni @rttnd/gau ``` 2. ##### Set up environment variables [Section titled “Set up environment variables”](#set-up-environment-variables) `gau` uses `AUTH_SECRET` for signing JWTs. You can generate a secure secret using the CLI: * npm ```sh npx gau secret ``` * pnpm ```sh pnpm dlx gau secret ``` * yarn ```sh yarn dlx gau secret ``` * bun ```sh bunx gau secret ``` * ni ```sh nlx gau secret ``` Then, add the output and your provider credentials to your `.env` file: .env ```txt AUTH_SECRET="your-generated-secret" GITHUB_CLIENT_ID="your-github-client-id" GITHUB_CLIENT_SECRET="your-github-client-secret" ``` 3. ##### Choose your database adapter [Section titled “Choose your database adapter”](#choose-your-database-adapter) `gau` uses a database adapter to connect to your database. [Drizzle](/adapters/drizzle/) 4. ##### Choose your integration [Section titled “Choose your integration”](#choose-your-integration) [SvelteKit](/integrations/sveltekit/) [SolidStart](/integrations/solidstart/) [Tauri](/runtimes/tauri/) 5. ##### Add OAuth Providers [Section titled “Add OAuth Providers”](#add-oauth-providers) [GitHub](/providers/github/) [Discord](/providers/discord/) [Google](/providers/google/) [Microsoft](/providers/microsoft/) [Facebook](/providers/facebook/) # Hooks > Extend and customize gau's OAuth flow using hooks. `gau` exposes hooks to let you intercept and customize the OAuth flow. You can modify user profiles, block linking, or take over the response entirely. Hooks are configured in `createAuth` and receive context about the current request (provider, tokens, user info, etc.). Hook syntax: auth.ts ```ts import { createAuth } from '@rttnd/gau/core' import { GitHub } from '@rttnd/gau/oauth' export const auth = createAuth({ providers: [ GitHub({ clientId: process.env.AUTH_GITHUB_ID!, clientSecret: process.env.AUTH_GITHUB_SECRET! }), ], onHook: async (context) => { return { handled: false } }, }) ``` ### Auth flow [Section titled “Auth flow”](#auth-flow) Two common paths run through the callback handler: #### Sign-in [Section titled “Sign-in”](#sign-in) * User initiates sign-in (`GET /api/auth/:provider`) * Browser is redirected back with `?code=&state=` * `gau` validates state/CSRF + PKCE, finds provider * Provider validates `code` -> returns `{ tokens, user (provider profile) }` * [`onOAuthExchange`](#onoauthexchange) runs * If it returns `{ handled: true, response }`: `gau` clears temp cookies and returns your response (STOP) * Otherwise, continue * [`mapExternalProfile`](#mapexternalprofile) runs to adjust `providerUser` * If provider is link-only: `gau` returns 400 (STOP) * `gau` finds/creates a user and links account (first time) or updates tokens (existing) * [`onAfterLinkAccount`](#onafterlinkaccount) runs with action ‘link’ or ‘update’ * Session is created and response is returned #### Linking [Section titled “Linking”](#linking) * User initiates linking while signed in (`GET /api/auth/link/:provider`) * Browser is redirected back with `?code=&state=` * `gau` validates state/CSRF + PKCE, finds provider * Provider validates `code` → returns `{ tokens, user (provider profile) }` * [`onOAuthExchange`](#onoauthexchange) runs * If it returns `{ handled: true, response }`: `gau` clears temp cookies and returns your response (STOP) * Otherwise, continue * [`mapExternalProfile`](#mapexternalprofile) runs to adjust `providerUser` * [`onBeforeLinkAccount`](#onbeforelinkaccount) runs: may block linking (return `allow:false` or custom response) * `gau` links the account and persists tokens * [`onAfterLinkAccount`](#onafterlinkaccount) runs with action ‘link’ * Session is (re)created and response is returned ### Hooks [Section titled “Hooks”](#hooks) #### onOAuthExchange [Section titled “onOAuthExchange”](#onoauthexchange) > Receives `{ request, providerId, state, code, codeVerifier, callbackUri, redirectTo, cookies, providerUser, tokens, isLinking, sessionUserId }` Return `{ handled: true, response }` to stop; `{ handled: false }` to continue. When it runs: * **After** `provider.validateCallback` returns tokens and provider profile. * **Before** any profile mapping, link-only checks, user lookup/linking/creation, or token update. Use it to: * Capture raw provider tokens and short-circuit with a custom Response. * Perform a custom token exchange and return results directly. Snippet: ```ts onOAuthExchange: async ({ tokens }) => { const access = tokens.accessToken() // optionally: const refresh = tokens.refreshToken?.() return { handled: false } } ``` #### mapExternalProfile [Section titled “mapExternalProfile”](#mapexternalprofile) When it runs (timeline): * After onOAuthExchange (if not short-circuited) * Before link-only enforcement and any persistence. Use it to: * Normalize or augment providerUser (name/email/avatar fields). ```ts mapExternalProfile: async ({ providerUser }) => ({ name: providerUser.name?.trim(), avatar: providerUser.avatar ?? undefined, }) ``` #### onBeforeLinkAccount [Section titled “onBeforeLinkAccount”](#onbeforelinkaccount) When it runs (timeline): * Only when an account is about to be linked (new link for a user). * After mapExternalProfile and (for sign-in) after user lookup/creation. Use it to: * Gate account linking with custom business logic (entitlements, org membership, etc.). ```ts onBeforeLinkAccount: async ({ userId, providerId }) => { const allowed = await checkEntitlements(userId, providerId) if (!allowed) return { allow: false } return { allow: true } } ``` You can also return a custom Response: ```ts return { allow: false, response: new Response('blocked', { status: 403 }) } ``` #### onAfterLinkAccount [Section titled “onAfterLinkAccount”](#onafterlinkaccount) When it runs (timeline): * After the account has been linked for the first time (action: ‘link’). * After an existing linked account’s tokens were updated on sign-in (action: ‘update’). Use it to: * Audit/log, notify, enqueue background work, or sync data. ```ts onAfterLinkAccount: async ({ action, providerId, userId }) => { // audit/log, enqueue jobs, etc. } ``` Tip See the Minecraft/Xbox cookbook for a real-world example that uses these hooks. # Middleware > Composable server middleware for sessions in SvelteKit and SolidStart. `gau` provides composable middleware helpers for framework integrations. Use what you need. ## Auth middleware [Section titled “Auth middleware”](#auth-middleware) The base middleware attaches two methods to your request context: * **`getSession()`** - Returns client-safe session data. * **`getServerSession()`** - Returns the full session including OAuth tokens. - SvelteKit `SvelteKitAuth` returns a `handle` hook that sets up `locals.getSession()`: hooks.server.ts ```ts export { handle } from './routes/api/auth/[...gau]/+server' ``` Now you can use `locals.getSession()` in load functions, endpoints, and remote functions. - SolidStart Use `authMiddleware` to attach `locals.getSession`. The first argument controls preloading: middleware.ts ```ts import { authMiddleware } from '@rttnd/gau/solidstart' import { createMiddleware } from '@solidjs/start/middleware' import { auth } from './server/auth' export default createMiddleware({ onRequest: [ authMiddleware(true, auth), ], }) ``` Options for the first argument: * `true` - Preload session on all routes * `false` - Lazy load (resolve on first `getSession()` call) * `['/dashboard', '/settings']` - Preload only on specific paths ### getSession vs getServerSession [Section titled “getSession vs getServerSession”](#getsession-vs-getserversession) Use `getSession()` for data that will be serialized to the browser: src/api/session.ts ```ts const session = await locals.getSession() return { session } ``` Use `getServerSession()` only on the server, like when you need OAuth tokens: src/api/github.ts ```ts const { accounts } = await locals.getServerSession() const githubAccount = accounts?.find(a => a.provider === 'github') if (githubAccount?.accessToken) { const res = await fetch('https://api.github.com/user/repos', { headers: { Authorization: `Bearer ${githubAccount.accessToken}` } }) } ``` ### Eager vs Lazy Loading [Section titled “Eager vs Lazy Loading”](#eager-vs-lazy-loading) * SvelteKit SvelteKit is lazy by default. * SolidStart The `pathsToPreLoad` parameter controls **when** the session is validated: **Eager (preload):** Session is validated immediately during middleware, before your route runs. ```ts // Session already resolved when getSession() is called const session = await event.locals.getSession() // instant, cached ``` **Lazy:** Session validation is deferred until `getSession()` is first called. ```ts // Validation happens here, on first call const session = await event.locals.getSession() // Subsequent calls return the cached result const again = await event.locals.getSession() // instant, same promise ``` Note Both modes memoize the result, so calling `getSession()` multiple times (e.g., in layout + page) only validates once. **When to use each:** | Scenario | Recommendation | | -------------------------------------------- | ---------------------------------------------------- | | Protected routes (`/dashboard`, `/settings`) | **Eager** - you’ll need the session anyway | | Public routes (`/`, `/about`, `/pricing`) | **Lazy** - skip validation if page never checks auth | | API routes that always check auth | **Eager** - avoid latency in the handler | ## Refresh middleware [Section titled “Refresh middleware”](#refresh-middleware) The Refresh middleware adds automatic session refresh: * Reads the token from **Cookie** first, then **Authorization: Bearer** * Refreshes if past the threshold, or every request * Responds appropriately: * **Cookie**: `Set-Cookie` header * **Bearer**: `X-Refreshed-Token` header (for Tauri/mobile) - SvelteKit hooks.server.ts ```ts import { sequence } from '@sveltejs/kit/hooks' import { handle as authHandle } from './routes/api/auth/[...gau]/+server' import { createRefreshHandle } from '@rttnd/gau/sveltekit' import { auth } from '$lib/server/auth' export const handle = sequence( authHandle, createRefreshHandle(auth, { threshold: 0.5 }), ) ``` - SolidStart middleware.ts ```ts import { authMiddleware, refreshMiddleware } from '@rttnd/gau/solidstart' import { createMiddleware } from '@solidjs/start/middleware' import { auth } from './server/auth' export default createMiddleware({ onRequest: [ authMiddleware(true, auth), refreshMiddleware(auth, { threshold: 0.5 }), ], }) ``` **Options:** * `threshold` - Only refresh if past this fraction of TTL (0-1). Example: `0.5` = refresh after 50% of session lifetime. * `ttl` - Override the TTL for the new token. ### Token-based clients [Section titled “Token-based clients”](#token-based-clients) If your app uses bearer tokens instead of cookies, the server middleware sends refreshed tokens via the `X-Refreshed-Token` header. Use the `fetch` method from `useAuth()` to handle this automatically. See the [Integrations](/integrations#fetch) page for details. # Protected Routes > Keep routes behind authentication on both the client and the server. Protecting routes ensures that only authenticated users can access them. `gau` supports both client-side guards for better UX and server-side checks for security. **You should use both.** ## Client-Side [Section titled “Client-Side”](#client-side) Client-side guards keep the session reactive and prevent flickers when navigating within your app. Note Use layouts to protect a group of routes. * SvelteKit Wrap the page in the `` component. src/routes/dashboard/+page.svelte ```svelte

Dashboard

Welcome, {auth.session?.user?.name}!

``` `` props: * `redirectTo` (optional): where to send unauthenticated visitors. Defaults to `'/'`. * `fallback` (optional): a Svelte snippet rendered while loading or when blocked. ```svelte
Loading...
}>
``` * SolidStart Use the `Protected` helper to create a wrapper component per route. src/routes/dashboard.tsx ```tsx import { Protected } from '@rttnd/gau/client/solid' import { useAuth } from '~/lib/auth' export default Protected(Page, '/') function Page() { const auth = useAuth() return (

Dashboard

Welcome, {session().user?.name}!

) } ``` Provide a fallback component to render a custom loading or blocked state instead of redirecting: ```tsx export default Protected( () => , () =>
Loading...
) ``` ## Server-Side [Section titled “Server-Side”](#server-side) Check session on the server to avoid rendering protected pages at all when the user is not signed in. * SvelteKit Example: src/routes/dashboard/+page.server.ts ```ts import type { PageServerLoad } from './$types' import { redirect } from '@sveltejs/kit' export const load: PageServerLoad = async ({ locals }) => { const session = await locals.getSession() if (!session?.user) throw redirect(302, '/login') return { session } } ``` * SolidStart Example: src/routes/dashboard.tsx ```tsx import type { RouteDefinition } from '@solidjs/router' import { query, createAsync, redirect } from '@solidjs/router' import { getRequestEvent } from 'solid-js/web' const getSession = query(async () => { 'use server' const event = getRequestEvent() const session = await event.locals.getSession() if (!session?.user) throw redirect('/login') return session }, 'session') export const route = { preload: () => getSession(), } satisfies RouteDefinition export default function Dashboard() { const session = createAsync(() => getSession()) return
Welcome, {session()?.user?.name}!
} ``` # Refresh Tokens > Understanding provider refresh tokens and session duration in gau. This guide covers two distinct concepts: **provider refresh tokens** (for calling external APIs) and **session refresh tokens** (for extending user sessions). ## Provider Refresh Tokens [Section titled “Provider Refresh Tokens”](#provider-refresh-tokens) `gau` uses its own JWT for sessions and does not require OAuth refresh tokens to keep a user signed in. However, when you need to call a provider API (Google, Microsoft), `gau` can store provider access/refresh tokens and automatically refresh/rotate them for you. ### What are provider refresh tokens? [Section titled “What are provider refresh tokens?”](#what-are-provider-refresh-tokens) Provider refresh tokens let your app get new access tokens without asking the user to sign in again. When you want to call a provider’s API (like querying Google Calendar or fetching Microsoft emails), you need an access token, but those expire quickly. The refresh token lets you get a fresh one behind the scenes. Note Some providers (e.g., GitHub) often do not issue refresh tokens. In this case, the `refreshToken` column will remain `null`. ### How gau stores tokens [Section titled “How gau stores tokens”](#how-gau-stores-tokens) During the OAuth callback, `gau` saves provider tokens into the `accounts` table via your adapter: * `accessToken`, `refreshToken`, `expiresAt`, `tokenType`, `scope`, `idToken` (when available) * On subsequent sign-ins, these values are updated for already-linked accounts. ### Automatic Refresh & Rotation [Section titled “Automatic Refresh & Rotation”](#automatic-refresh--rotation) `gau` exposes a server-side helper to retrieve a valid provider access token, refreshing and rotating as needed: server/usage.ts ```ts const result = await auth.getAccessToken(userId, 'google') if (!result) throw new Error('No access token available') const { accessToken } = result // Use accessToken to call Google APIs... ``` Behavior: * If the stored access token is still valid, it’s returned as-is. * If it’s expired and a `refreshToken` exists (and the provider supports it), `gau` calls the provider’s token endpoint. * If a new refresh token is returned, `gau` rotates it and persists the change via your adapter. ## Session Refresh Tokens [Section titled “Session Refresh Tokens”](#session-refresh-tokens) `gau` does not implement classic **refresh token pairs**. Sessions are managed using expiring JWTs, and not stored in the database. You can optionally do **sliding expiration** by refreshing the session JWT as the user keeps using the app. See the [Middleware](/guides/middleware) guide, or the [`refreshSession`](/guides/advanced#the-refreshsession-method) method. **For longer sessions**, just increase [`jwt.ttl`](/guides/configuration#jwtttl) in your config. The default is 7 days; you can safely set it to 30 days or more for most apps. **Refresh token pairs** do have benefits: logout all devices, replay protection, instant revocation. If this is important to you, [open an issue](https://github.com/Rettend/gau/issues). # Role-Based Access Control > Add optional roles to your gau setup and guard routes. This guide shows how to add roles to your `gau` setup. * Roles are stored in the `User` table in the database. * On first sign-in, you can set a role via `roles.defaultRole` or compute one with `roles.resolveOnCreate`. * The session returned by `getSession()` includes `user.role` (if stored) and a derived `user.isAdmin` flag. ## Add `role` to your User table [Section titled “Add role to your User table”](#add-role-to-your-user-table) Add a `role` column. You can constrain values if you want. * SQLite schema.ts ```diff import { snakeCase, text } from 'drizzle-orm/sqlite-core' export const Users = snakeCase.table('users', { // ... role: text().$type<'admin' | 'user'>().default('user'), }) ``` * PostgreSQL schema.ts ```diff import { snakeCase, text } from 'drizzle-orm/pg-core' export const Users = snakeCase.table('users', { // ... role: text().$type<'admin' | 'user'>().default('user'), }) ``` ## Configuration [Section titled “Configuration”](#configuration) See [roles](/guides/configuration#roles). These are all optional, by default `user` is the default role, `adminRoles` is `['admin']`, and `adminUserIds` is `[]`. auth.ts ```ts import { createAuth } from '@rttnd/gau/core' export const auth = createAuth({ // ... roles: { defaultRole: 'user', resolveOnCreate({ providerId, profile }) { // Example: return profile.email === 'admin@example.com' ? 'admin' : 'user' }, adminRoles: ['admin'], adminUserIds: ['1234567890'], }, }) ``` On first sign-in, `resolveOnCreate` is called. If it returns a role, it’s used; otherwise `defaultRole`. Every validated session sets `user.isAdmin` based on `roles.adminRoles` and `roles.adminUserIds`. The flag defaults to `false` when no rule matches. ## Use roles on the server [Section titled “Use roles on the server”](#use-roles-on-the-server) Authorize in your route handlers/middleware using `session.user?.isAdmin`. * SvelteKit ###### Remote functions [Section titled “Remote functions”](#remote-functions) admin.remote.ts ```ts import { getRequestEvent, query } from '$app/server' import { error } from '@sveltejs/kit' export const getSession = query( async () => { const event = getRequestEvent() const session = await event.locals.getSession() if (!session.user?.isAdmin) error(403, 'Forbidden') return session }, ) ``` ###### Load functions [Section titled “Load functions”](#load-functions) +page.ts ```ts import type { PageLoad } from './$types' import { error } from '@sveltejs/kit' export const load: PageLoad = async (event) => { const session = await event.locals.getSession() if (!session.user?.isAdmin) error(403, 'Forbidden') return session } ``` ###### API routes [Section titled “API routes”](#api-routes) routes/api/+server.ts ```ts import type { RequestHandler } from './$types' import { error } from '@sveltejs/kit' export const GET: RequestHandler = async (event) => { const session = await event.locals.getSession() if (!session.user?.isAdmin) error(403, 'Forbidden') return session } ``` * SolidStart ###### Server functions [Section titled “Server functions”](#server-functions) lib/server/session.ts ```ts import { query } from '@solidjs/router' import { getRequestEvent } from 'solid-js/web' export const getSession = query(async () => { const event = getRequestEvent() const session = await event.locals.getSession() if (!session.user?.isAdmin) throw new Error('Forbidden') return session }) ``` ###### API routes [Section titled “API routes”](#api-routes-1) routes/api/session.ts ```ts import type { APIHandler } from '@solidjs/start/server' export const GET: APIHandler = async (event) => { const session = await event.locals.getSession() if (!session.user?.isAdmin) return new Response('Forbidden', { status: 403 }) return session } ``` # Security > Security best practices when using gau. This guide explains `gau`’s security features. ## Host Validation [Section titled “Host Validation”](#host-validation) To protect against common web vulnerabilities, `gau` uses the `trustHosts` option to validate where requests are coming from and where users are being sent. This is a critical defense against both CSRF and Open Redirect attacks. This is configured via the `trustHosts` option in `createAuth`. ### Open Redirect Prevention [Section titled “Open Redirect Prevention”](#open-redirect-prevention) After login, you might want to send users to a specific page using the `redirectTo` parameter. Without validation, an attacker could trick users into being redirected to a phishing site. `gau` checks that the `redirectTo` URL points to a hostname in your `trustHosts` list. If it doesn’t match, the redirect is blocked. Note Client-side integrations automatically set `redirectTo` to their URL. You just need to whitelist the hostnames on the server’s `createAuth` config. ### CSRF Protection [Section titled “CSRF Protection”](#csrf-protection) A malicious site could try to make requests to your auth endpoints on behalf of a logged-in user. `gau` prevents this by checking the `Origin` header on all `POST` requests (like `/api/auth/signout`). Only requests from hosts in your `trustHosts` list are allowed. ### Configuration [Section titled “Configuration”](#configuration) * **Default behavior**: By default, `trustHosts` is an empty array (`[]`), which means `gau` will only allow requests and redirects that originate from or point to the same host as your application. Note In development, `localhost` and `127.0.0.1` are automatically trusted. * **Configuring specific hosts**: When your frontend and backend are on different domains, or you need to redirect to a subdomain, you must add those hosts to the list. ```ts trustHosts: [ 'app.example.com', // Production frontend 'tauri.localhost', // Production Tauri app ] ``` * **Trust all hosts (`'all'`)**: This setting disables host validation and is **not recommended for production**. In addition to `trustHosts`, `gau` also uses a temporary state-based CSRF token (`__gau-csrf-token` cookie) during the OAuth flow itself. This ensures that the user who starts the authentication process is the same one who finishes it, providing an extra layer of security against CSRF during login. ## PKCE [Section titled “PKCE”](#pkce) OAuth 2.1 added PKCE to ensure that the client asking for the token is the same client that initiated the login flow. `gau` implements this in two layers, for both web and native apps. ### Server-to-Provider PKCE [Section titled “Server-to-Provider PKCE”](#server-to-provider-pkce) `gau` automatically uses PKCE when communicating with OAuth providers. 1. The `gau` server generates a `code_verifier` and `code_challenge`. 2. It sends the `code_challenge` to the provider during the authorization request. 3. When exchanging the provider’s authorization code for tokens, it sends the `code_verifier`. ### Client-to-Server PKCE (Native Apps) [Section titled “Client-to-Server PKCE (Native Apps)”](#client-to-server-pkce-native-apps) Deep links (like `my-app://callback`) can potentially be intercepted by other apps on the device. To prevent this, `gau` adds another PKCE layer between your app and the server: 1. **Initiation**: The Tauri app generates a `code_verifier` and `code_challenge`. It sends the `code_challenge` to the `gau` server when starting the sign-in. 2. **Callback**: After authenticating with the provider, the `gau` server issues a short-lived **Auth Code** (instead of a session token) and redirects back to the app via deep link. 3. **Exchange**: The app intercepts the deep link, extracts the Auth Code, and sends it along with the original `code_verifier` to the server’s `/token` endpoint. 4. **Validation**: The server verifies the Auth Code and checks that the `code_verifier` matches the initial challenge. Only then is a session token issued. This way, even if another app intercepts the deep link, it can’t get a session token because it doesn’t have the secret `code_verifier`. ## Secure Cookie Defaults [Section titled “Secure Cookie Defaults”](#secure-cookie-defaults) `gau` applies safe defaults to the session cookie (`__gau-session-token`): * **`httpOnly: true`**: JavaScript can’t read the cookie, protecting against XSS attacks. * **`secure: true`**: The cookie is only sent over HTTPS. * **`sameSite: 'lax'`**: Protects against most CSRF attacks while still working with normal navigation. You can customize these settings via the [`cookies`](/guides/configuration#cookies) option, for example, to enforce `'strict'` same-site policies if your application’s flow allows for it. # Session Management > How gau handles user sessions in different environments. `gau`’s flexibility is achieved by supporting two primary session management strategies: **cookie-based** and **token-based** sessions. ## Cookie-Based Sessions (Web Apps) [Section titled “Cookie-Based Sessions (Web Apps)”](#cookie-based-sessions-web-apps) This is the most common method for full-stack web apps. ### How It Works [Section titled “How It Works”](#how-it-works) 1. **Sign-In**: The user is redirected to the OAuth provider. 2. **Callback**: After successful authentication, the provider redirects the user back to your apps’s callback URL (e.g., `/api/auth/callback/github`). 3. **Session Cookie**: `gau` validates the callback, creates a user session, and stores the session information in a `secure`, `HttpOnly` cookie. 4. **Authenticated Requests**: The browser automatically includes this cookie with every subsequent request to your backend. The server can then validate the cookie to identify the user. This flow is handled automatically by the framework helpers, like `@rttnd/gau/sveltekit`. The session cookie is named `__gau-session-token` by default and its attributes can be configured via the [`cookies`](/guides/configuration#cookies) option in `createAuth`. *** ## Token-Based Sessions (Native Apps & SPAs) [Section titled “Token-Based Sessions (Native Apps & SPAs)”](#token-based-sessions-native-apps--spas) Cookies don’t work well for desktop apps, mobile apps, or frontends hosted separately from the backend. For these cases, `gau` uses tokens instead. ### How It Works [Section titled “How It Works”](#how-it-works-1) The flow uses PKCE to securely pass the session token from your backend to the app, ensuring only the app that started the login can receive the token. 1. **Sign-In**: The client-side `signIn` function generates a PKCE `code_verifier` and `code_challenge`. It opens the system’s web browser to the OAuth provider’s authorization URL, including the challenge. 2. **Callback & Redirect**: After the user approves, the provider redirects to your backend. The backend validates the request and issues a short-lived **Auth Code** via a deep link redirect (e.g., `my-app://oauth/callback?code=...`). 3. **Exchange**: The operating system captures this custom URL and re-opens your native app. The app extracts the auth code and sends it, along with the original `code_verifier`, to the backend’s `/token` endpoint. 4. **Token Storage**: Upon successful validation, the backend returns a session token. The client library stores it in: * **`localStorage`**: For the client to attach it to subsequent API requests in the `Authorization: Bearer ` header. * **A non-`HttpOnly` cookie**: This allows server-side rendering logic to access the session on the initial page load. 5. **Authenticated Requests**: The client now includes this token in the `Authorization` header for all subsequent API requests. This is entirely managed by `gau`. The client libraries automatically handle storing the token and including it in requests, while the backend is designed to render the necessary redirect page. See the [Tauri integration guide](/integrations/tauri) for a practical example. ## Session Strategy [Section titled “Session Strategy”](#session-strategy) You can control this behavior using the `session.strategy` option in `createAuth`. * `'auto'` (Default): Uses cookies unless the client supplies a PKCE challenge or redirects to a different host or custom URL scheme. Browser clients with `credentials: 'omit'` supply a challenge, including for same-origin sign-in. * `'cookie'`: Forces cookie-based sessions, even for cross-origin redirects (as long as the host is trusted). * `'token'`: Forces token-based sessions, even for same-origin redirects. # Framework Integrations > How to integrate gau with your favorite framework. `gau` is designed to be framework-agnostic. It can run anywhere, but official support makes it easier. It provides client-side components and hooks for frameworks. This section covers the common features and options available across all client integrations. `gau` has support for full-stack frameworks: [SvelteKit](/integrations/sveltekit/) [SolidStart](/integrations/solidstart/) [Marko](/integrations/marko/) [Astro](/integrations/astro/) And backend-only frameworks: [Bun.serve](/integrations/bun-serve/) [Elysia](/integrations/elysia/) For backend-only setups, you can use the vanilla client on the frontend: [Vanilla](/integrations/vanilla/) ## AuthProvider [Section titled “AuthProvider”](#authprovider) In Solid and Svelte, wrap the components that need client-side auth state in an `AuthProvider`. Marko uses an explicit client tag. Server-rendered pages can read sessions without a client provider. ### Props [Section titled “Props”](#props) You can configure the `AuthProvider` with the following props: #### `baseUrl` [Section titled “baseUrl”](#baseurl) > **Type**: `string` **Default**: `'/api/auth'` The base URL for the authentication API endpoints. This should match the `basePath` configured in your `createAuth` setup on the server. Note If your frontend and backend are on different hosts, you’ll need to set this to the full URL of your backend’s auth API. #### `scheme` [Section titled “scheme”](#scheme) > **Type**: `string` **Default**: `'gau'` The URL scheme used for deep linking in desktop applications (e.g., Tauri). This is used to redirect the user back to your app after they have authenticated with an OAuth provider. #### `redirectTo` [Section titled “redirectTo”](#redirectto) > **Type**: `string` **Default**: The current host URL A default URL to redirect to after a successful sign-in. This can be overridden by passing a `redirectTo` option to the `signIn` function. #### `session` [Section titled “session”](#session) Supply the safe session from your server to show the signed-in state immediately. Later changes to this prop update the client session too. Solid uses a session accessor; Solid 2 accepts an accessor or a value, and Svelte accepts a value. #### `client` [Section titled “client”](#client) Pass a [vanilla client](/integrations/vanilla) to share session updates between separate component roots, such as Astro islands. `baseUrl` and `scheme` are taken from that client. Create shared clients in browser code, not at server module scope. ## useAuth [Section titled “useAuth”](#useauth) The `useAuth` hook is the primary way to access authentication state and methods in your components. ### Return Values [Section titled “Return Values”](#return-values) The `useAuth` hook returns an object with the following properties: #### `session` [Section titled “session”](#session-1) > **Type**: `GauSession` An object containing the current authentication state. Its `user` and `session` properties will be `null` if the user is not authenticated. * `user`: The user object from the database, or `null`. * `session`: The session object containing the raw JWT payload, or `null`. * `accounts`: An array of OAuth accounts linked to the user, or `null`. * `providers`: An array of available provider IDs. #### `isLoading` [Section titled “isLoading”](#isloading) > **Type**: `boolean` True when the session is loading for client side apps. #### `signIn` [Section titled “signIn”](#signin) > **Type**: `(provider: string, options?: { redirectTo?: string }) => Promise` A function to initiate the sign-in process with a specific OAuth provider. * `provider`: The ID of the provider to use (e.g., `'github'`, `'google'`). * `options.redirectTo`: A URL to redirect to after a successful sign-in, overriding the default `redirectTo` prop on the `AuthProvider`. Note You can get full TypeScript support for your provider IDs by creating a typed `useAuth` hook. See the framework integration guides for examples. You can also extract the union type of your configured providers using the `ProviderIds` utility: ```ts import type { ProviderIds } from '@rttnd/gau' import type { Auth } from './server/auth' export type Provider = ProviderIds // 'github' | 'google' ``` #### `signOut` [Section titled “signOut”](#signout) > **Type**: `() => Promise` A function to sign the user out, clearing their session. #### `linkAccount` [Section titled “linkAccount”](#linkaccount) > **Type**: `(provider: string, options?: { redirectTo?: string }) => Promise` A function to initiate the OAuth flow to manually link an additional provider to the currently authenticated user’s account. * `provider`: The ID of the provider to link (e.g., `'github'`, `'google'`). * `options.redirectTo`: A URL to redirect to after a successful link, overriding the default `redirectTo` prop on the `AuthProvider`. #### `unlinkAccount` [Section titled “unlinkAccount”](#unlinkaccount) > **Type**: `(provider: string) => Promise` A function to unlink an OAuth provider from the user’s account. * `provider`: The ID of the provider to unlink. Caution A user cannot unlink their last remaining account, as this would leave them with no way to sign in. `gau` will prevent this action and return an error. #### `refresh` [Section titled “refresh”](#refresh) > **Type**: `() => Promise` A function to manually refresh the session from the server. This fetches the latest session data and updates the local state. This might be useful when: * You’ve updated user data on the server and want to reflect changes immediately * You want to check if the session is still valid * You’ve linked or unlinked accounts and want to update the accounts list #### `fetch` [Section titled “fetch”](#fetch) > **Type**: `(input: RequestInfo | URL, init?: RequestInit) => Promise` A fetch wrapper for making authenticated API calls to your backend. ```ts const { fetch } = useAuth() const res = await fetch('/api/my-endpoint', { method: 'POST', body: JSON.stringify({ data: 'value' }), }) ``` **What it handles:** * Adds `Authorization: Bearer ` if a token is stored (Tauri/mobile) * Falls back to `credentials: 'include'` for cookie-based auth (web) * Auto-stores refreshed tokens from the `X-Refreshed-Token` header Tip For pure web apps using cookies, you don’t need `fetch` from useAuth, regular `fetch` with `credentials: 'include'` works fine. This is mainly useful for Tauri/mobile apps that use bearer tokens. See the [Middleware](/guides/middleware) guide for setting up automatic session refresh on the server. # Astro > How to integrate gau with Astro 7. This guide shows how to integrate `gau` with Astro 7, using GitHub as an example. 1. ##### Follow the Basic Setup [Section titled “Follow the Basic Setup”](#follow-the-basic-setup) Complete the [Getting Started](/guides/getting-started) guide. Create your auth instance in `src/server/auth.ts`. For a persistent database, follow the [Drizzle](/adapters/drizzle) guide. For GitHub, set the callback URL to `http://localhost:4321/api/auth/callback/github` during development. 2. ##### Configure Astro [Section titled “Configure Astro”](#configure-astro) Install an [Astro server adapter](https://docs.astro.build/en/guides/on-demand-rendering/) so auth routes can run on each request. For example, with the Node adapter: astro.config.mjs ```js import { defineConfig } from 'astro/config' import node from '@astrojs/node' export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), session: false, }) ``` Gau does not require Astro’s session storage. Omit `session: false` if your app otherwise uses `Astro.session`. 3. ##### Set up the API Route [Section titled “Set up the API Route”](#set-up-the-api-route) Export the Astro helpers alongside your auth instance: src/server/auth.ts ```ts import { AstroAuth } from '@rttnd/gau/astro' // After creating your auth instance: export const gau = AstroAuth(auth) ``` Create a catch-all route for the auth endpoints. src/pages/api/auth/\[...gau].ts ```ts import { gau } from '../../../server/auth' export const prerender = false export const { GET, POST, OPTIONS } = gau ``` 4. ##### Add Middleware [Section titled “Add Middleware”](#add-middleware) Add the Gau middleware to make sessions available in `Astro.locals` and refresh the session cookie when needed. src/middleware.ts ```ts import { gau } from './server/auth' export const onRequest = gau.onRequest ``` Server code can now call `Astro.locals.getSession()`. Use `getServerSession()` only when you need account tokens on the server. Add the locals to `src/env.d.ts` for full type safety: src/env.d.ts ```ts import type { GauAstroLocals } from '@rttnd/gau/astro' import type { auth } from './server/auth' declare global { namespace App { interface Locals extends GauAstroLocals {} } } ``` 5. ##### Load the Session [Section titled “Load the Session”](#load-the-session) Read the session in your page. Use a link to sign in and a form to sign out: src/pages/index.astro ```astro --- const { user } = await Astro.locals.getSession() --- {user ? ( <>

Welcome, {user.name}!

) : ( Sign in with GitHub )} ``` Note `data-astro-reload` keeps auth links and forms as full-page navigations when using Astro’s `ClientRouter`. The sign-out redirect must stay on the same origin. 6. ##### Protect a Page [Section titled “Protect a Page”](#protect-a-page) Check the session before rendering the page or loading private data. src/pages/account.astro ```astro --- export const prerender = false const { user } = await Astro.locals.getSession() if (!user) return Astro.redirect('/') ---

Welcome, {user.name}!

``` Check the session in each endpoint or action that accesses private data too. Keep personalized pages out of shared route caches. 7. ##### File Structure [Section titled “File Structure”](#file-structure) * src * server * auth.ts * pages * api * auth * \[…gau].ts * account.astro * index.astro * env.d.ts * middleware.ts * package.json * astro.config.mjs 8. ##### Next Steps [Section titled “Next Steps”](#next-steps) * See the [Astro example](https://github.com/Rettend/gau/tree/main/packages/example-astro) for a complete app. * Explore other [OAuth Providers](/providers). * Learn about [Protected Routes](/guides/protected-routes). ## Set up the Client [Section titled “Set up the Client”](#set-up-the-client) A browser client is optional. Use the [vanilla client](/integrations/vanilla) in browser scripts, or a Gau [Solid](/integrations/solidstart) or [Svelte](/integrations/sveltekit) provider inside a hydrated island. Pass the session from `Astro.locals.getSession()` to the island to show the signed-in state immediately. Separate islands do not share component context. To share updates, create one vanilla client in browser code and pass it through the providers’ `client` prop. Never share a mutable session client between server requests. For a retained island, pass fresh server session props after navigation. A client-side sign-out only updates subscribed components; navigate afterward to update other server-rendered content. ## Static Pages [Section titled “Static Pages”](#static-pages) A prerendered page cannot read a visitor’s session at build time. Use an on-demand page for private content, or a [server island](https://docs.astro.build/en/guides/server-islands/) for a personalized section of a public page. Read the session inside the island’s own request. # Bun.serve > How to use gau with a plain Bun.serve HTTP server. This guide shows how to use `gau` with a backend-only Bun server using `Bun.serve`. Bun’s docs: [Bun.serve](https://bun.com/docs/api/http). 1. ##### Follow the basic setup [Section titled “Follow the basic setup”](#follow-the-basic-setup) Complete the [Getting Started](/guides/getting-started) guide for installation, environment variables, and adding at least one OAuth provider. 2. ##### Create your auth configuration [Section titled “Create your auth configuration”](#create-your-auth-configuration) Define your `createAuth` configuration. src/auth.ts ```ts import process from 'node:process' import { MemoryAdapter } from '@rttnd/gau/adapters/memory' import { createAuth } from '@rttnd/gau/core' import { GitHub } from '@rttnd/gau/oauth' export const auth = createAuth({ adapter: MemoryAdapter(), providers: [ GitHub({ clientId: process.env.AUTH_GITHUB_ID!, clientSecret: process.env.AUTH_GITHUB_SECRET!, }), ], jwt: { secret: process.env.AUTH_SECRET!, }, // When frontend runs on a different origin, set the trusted hosts, example: // trustHosts: ['http://localhost:5173'], }) export type Auth = typeof auth ``` Note `gau` handles CORS and CSRF. For cross-origin setups, configure [`trustHosts`](/guides/configuration#trusthosts) in `createAuth`. 3. ##### Start a Bun.serve HTTP server [Section titled “Start a Bun.serve HTTP server”](#start-a-bunserve-http-server) `createHandler(auth)` returns a standard Fetch handler. You can use it directly inside `Bun.serve`. server.ts ```ts import { auth } from './src/auth' import { createHandler } from '@rttnd/gau/core' const handler = createHandler(auth) const server = Bun.serve({ routes: { '/api/auth/*': req => handler(req), '/': new Response('OK'), }, }) console.log(`Server listening on ${server.url.hostname}:${server.url.port}`) ``` 4. ##### Try it [Section titled “Try it”](#try-it) Look at the [Bun Example](https://github.com/Rettend/gau/tree/main/packages/example-bun). 5. ##### File Structure (example) [Section titled “File Structure (example)”](#file-structure-example) * src * auth.ts * index.ts * package.json #### Notes [Section titled “Notes”](#notes) * By default, sessions use cookies when possible. If `redirectTo` points to a different origin (or a custom scheme for native apps), `gau` automatically returns the session token in the URL hash. * You can force strategies via `session.strategy: 'cookie' | 'token'` in `createAuth`. # Elysia > How to integrate gau with Elysia. This guide shows how to integrate `gau` with an Elysia server. 1. ##### Follow the basic setup [Section titled “Follow the basic setup”](#follow-the-basic-setup) Complete the [Getting Started](/guides/getting-started) guide for installation, environment variables, and adding at least one OAuth provider. 2. ##### Create your auth configuration [Section titled “Create your auth configuration”](#create-your-auth-configuration) src/auth.ts ```ts import process from 'node:process' import { MemoryAdapter } from '@rttnd/gau/adapters/memory' import { createAuth } from '@rttnd/gau/core' import { GitHub } from '@rttnd/gau/oauth' export const auth = createAuth({ adapter: MemoryAdapter(), providers: [ GitHub({ clientId: process.env.AUTH_GITHUB_ID!, clientSecret: process.env.AUTH_GITHUB_SECRET!, }), ], jwt: { secret: process.env.AUTH_SECRET!, }, // When frontend runs on a different origin, set the trusted hosts, example: // trustHosts: ['http://localhost:5173'], }) export type Auth = typeof auth ``` Note `gau` handles CORS and CSRF. For cross-origin setups, configure [`trustHosts`](/guides/configuration#trusthosts) in `createAuth`. 3. ##### Mount the fetch handler [Section titled “Mount the fetch handler”](#mount-the-fetch-handler) `createHandler(auth)` returns a standard Fetch handler `(req: Request) => Promise`. You can mount it directly on an Elysia app using `.mount()`. src/index.ts ```ts import { createHandler } from '@rttnd/gau/core' import { Elysia } from 'elysia' import { auth } from './auth' const handler = createHandler(auth) const app = new Elysia() .mount(handler) .get('/', () => 'OK') .listen(3000) console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) ``` 4. ##### Configure CORS [Section titled “Configure CORS”](#configure-cors) You are probably not using Elysia exclusively for auth. In that case, disable `gau`’s CORS handling by setting `cors: false` in `createAuth`. Then, configure CORS for your whole app using Elysia’s [CORS plugin](https://elysiajs.com/plugins/cors.html#cors-plugin). src/index.ts ```diff +import { cors } from '@elysiajs/cors' import { createHandler } from '@rttnd/gau/core' import { Elysia } from 'elysia' import { auth } from './auth' const handler = createHandler(auth) const app = new Elysia() .mount(handler) +.use(cors()) .get('/', () => 'OK') .listen(3000) console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) ``` 5. ##### Try it [Section titled “Try it”](#try-it) Look at the [Elysia Example](https://github.com/Rettend/gau/tree/main/packages/example-elysia). 6. ##### File Structure (example) [Section titled “File Structure (example)”](#file-structure-example) * src * auth.ts * index.ts * package.json # Marko > How to integrate gau with Marko 6 and Marko Run. This guide shows how to integrate `gau` with Marko 6 and Marko Run, using GitHub as an example. 1. ##### Follow the Basic Setup [Section titled “Follow the Basic Setup”](#follow-the-basic-setup) Complete the [Getting Started](/guides/getting-started) guide. Create your auth instance in `src/server/auth.ts`. For a persistent database, follow the [Drizzle](/adapters/drizzle) guide. For GitHub, set the callback URL to `http://localhost:3000/api/auth/callback/github` during development. 2. ##### Set up the API Route [Section titled “Set up the API Route”](#set-up-the-api-route) Export the Marko Run helpers alongside your auth instance: src/server/auth.ts ```ts import { MarkoRunAuth } from '@rttnd/gau/marko-run' // After creating your auth instance: export const gau = MarkoRunAuth(auth) ``` Create a catch-all route for the auth endpoints. src/routes/api/auth/$$gau/+handler.ts ```ts import { gau } from '../../../../server/auth' export const GET = Run.GET((ctx) => gau.handle(ctx.request)) export const POST = Run.POST((ctx) => gau.handle(ctx.request)) export const OPTIONS = Run.OPTIONS((ctx) => gau.handle(ctx.request)) ``` 3. ##### Add Middleware [Section titled “Add Middleware”](#add-middleware) Add middleware to refresh the session cookie when needed. src/routes/+middleware.ts ```ts import { gau } from '../server/auth' export default Run.ALL((ctx, next) => gau.refresh(ctx.request, () => next())) ``` 4. ##### Load the Session [Section titled “Load the Session”](#load-the-session) Pass the session from your page handler: src/routes/+handler.ts ```ts import { gau } from '../server/auth' export const GET = Run.GET((ctx, next) => next({ session: gau.getSession(ctx.request) })) ``` Use `` to read the session in your page. Use a link to sign in and a form to sign out: src/routes/+page.marko ```marko

Welcome, ${session.user.name}!

Sign in with GitHub
``` `getSession(request)` returns data that can be sent to the browser. Use `getServerSession(request)` only when you need account tokens on the server. Note The sign-out form redirects to a fresh page. Its `redirectTo` must stay on the same origin. 5. ##### Set up the Client [Section titled “Set up the Client”](#set-up-the-client) Add the client tag when you need reactive session data. This is optional for server-rendered pages. src/routes/+page.marko ```marko import GauAuth from "@rttnd/gau/client/marko/Auth.marko";

${auth.session.user?.name}

``` Pass the returned `auth` value to components that need it. No context provider is required. The tag starts with the supplied session and accepts later session updates. Without a session, it loads one in the browser. Different origins If your frontend and backend use different origins, set `baseUrl` to the full URL of the auth API. ```marko ``` After signing out through JavaScript, navigate to a fresh page to update content rendered by the server. Use normal browser navigation for auth changes; Marko’s persisted pages are still under development. 6. ##### Protect a Page [Section titled “Protect a Page”](#protect-a-page) Check the session before rendering the page or loading private data. src/routes/account/+handler.ts ```ts import { gau } from '../../server/auth' export const GET = Run.GET(async (ctx, next) => { const session = await gau.getSession(ctx.request) if (!session.user) return ctx.redirect('/') return next({ session }) }) ``` 7. ##### File Structure [Section titled “File Structure”](#file-structure) * src * server * auth.ts * routes * api * auth * $$gau * +handler.ts * account * +handler.ts * +handler.ts * +middleware.ts * +page.marko * package.json * vite.config.ts 8. ##### Next Steps [Section titled “Next Steps”](#next-steps) * See the [Marko example](https://github.com/Rettend/gau/tree/main/packages/example-marko-run) for a complete app. * Explore other [OAuth Providers](/providers). * Learn about [Protected Routes](/guides/protected-routes). # Solid > How to integrate gau with Solid. Solid 2 RC is out. Read [the announcement](https://github.com/solidjs/solid/releases/tag/v2.0.0-rc.0). Solid 2 uses start mode from `@solidjs/vite-plugin` instead of SolidStart. This guide uses Solid 2. If your app uses Solid 1, see [Solid 1](#solid-1). ## Solid 2 [Section titled “Solid 2”](#solid-2) 1. ##### Follow the Basic Setup [Section titled “Follow the Basic Setup”](#follow-the-basic-setup) Complete the [Getting Started](/guides/getting-started) guide. Complete the [Drizzle](/adapters/drizzle) guide: 1. Set up your schema in `src/server/db/schema.ts` 2. Set up your database in `src/server/db/index.ts` 2. ##### Configure Solid [Section titled “Configure Solid”](#configure-solid) Enable start mode and file-system routing: vite.config.ts ```ts import solid from '@solidjs/vite-plugin' import { fileRoutes } from 'filesystem-routing/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ solid({ start: { middleware: './src/middleware.ts' }, ssr: true, serverFunctions: true, }), fileRoutes({ httpMethods: true }), ], }) ``` src/router.ts ```ts import { createRouter } from '@solidjs/router' import { fileRoutes } from '@solidjs/router/fs' import { pageRoutes } from 'virtual:file-routes' export const Router = createRouter({ routes: fileRoutes(pageRoutes), }) ``` 3. ##### Set up the API Route [Section titled “Set up the API Route”](#set-up-the-api-route) Create a catch-all route for the auth endpoints. src/routes/api/auth/\[...gau].ts ```ts import { SolidAuth } from '@rttnd/gau/solid2' import { auth } from '~/server/auth' export const { GET, POST, OPTIONS } = SolidAuth(auth) ``` 4. ##### Add Middleware [Section titled “Add Middleware”](#add-middleware) Add the Gau middleware before the file-system router. src/middleware.ts ```ts import { authMiddleware, refreshMiddleware } from '@rttnd/gau/solid2' import { createAPIHandler } from 'filesystem-routing/api' import routes from 'virtual:file-routes' import { auth } from './server/auth' export default [ authMiddleware(auth), refreshMiddleware(auth, { threshold: 0.5 }), createAPIHandler(routes), ] ``` Server code can now call `event.locals.getSession()`. Use `getServerSession()` only when you need account tokens on the server. Add the locals to `src/global.d.ts` for full type safety: src/global.d.ts ```ts /// import type { GauSolid2Locals } from '@rttnd/gau/solid2' import type { Auth } from './server/auth' declare module '@solidjs/web' { interface RequestEventLocals extends GauSolid2Locals {} } export {} ``` 5. ##### Set up the Client [Section titled “Set up the Client”](#set-up-the-client) Wrap your routes in `AuthProvider`. src/App.tsx ```tsx import { AuthProvider } from '@rttnd/gau/client/solid2' import { Router } from './router' export default function App() { return ( {props => ( {props.children} )} ) } ``` Different origins If your frontend and backend use different origins, set `baseUrl` to the full URL of the auth API. ```tsx {/* app */} ``` To include the session in the server-rendered page, pass it to `AuthProvider`. See the [Solid 2 example](https://github.com/Rettend/gau/tree/main/packages/example-solidstart2) for the full setup. 6. ##### Use the Client [Section titled “Use the Client”](#use-the-client) Create a typed `useAuth` hook: src/lib/auth.ts ```ts import type { Auth } from '~/server/auth' import { useAuth as useAuthCore } from '@rttnd/gau/client/solid2' export const useAuth = () => useAuthCore() ``` Use it in your components: src/routes/index.tsx ```tsx import { Show } from 'solid-js' import { useAuth } from '~/lib/auth' export default function Home() { const auth = useAuth() return ( auth.signIn('github')}>Sign in with GitHub} > ) } ``` ### Cross-origin Server Functions [Section titled “Cross-origin Server Functions”](#cross-origin-server-functions) To call server functions from Tauri or another origin, configure the remote endpoint once: src/App.tsx ```ts import { configureServerFunctions } from '@rttnd/gau/client/solid2' configureServerFunctions({ endpoint: import.meta.env.VITE_SERVER_FUNCTIONS_URL, }) ``` Restrict the frontend origins in `createAuth`: src/server/auth.ts ```ts createAuth({ // ... trustHosts: ['tauri.localhost'], cors: { allowedOrigins: 'trust' }, }) ``` Then add it before your other server middleware: src/middleware.ts ```ts import { serverFunctionsMiddleware } from '@rttnd/gau/solid2' export default [ serverFunctionsMiddleware(auth), // ... ] ``` It handles CORS. Keep `refreshMiddleware` enabled to refresh sessions; the client stores refreshed bearer tokens. ## Solid 1 [Section titled “Solid 1”](#solid-1) Solid 1 apps continue to use SolidStart. Use `@rttnd/gau/solidstart` for routes and middleware, and `@rttnd/gau/client/solid` for the client. See the [Solid 1 example](https://github.com/Rettend/gau/tree/main/packages/example-solidstart) for a complete setup. # SvelteKit > How to integrate gau with SvelteKit. This guide shows how to integrate `gau` with SvelteKit, using Drizzle (SQLite or PostgreSQL) and GitHub as examples. 1. ##### Follow the basic setup [Section titled “Follow the basic setup”](#follow-the-basic-setup) Complete the [Getting Started](/guides/getting-started) guide. Complete the [Drizzle](/adapters/drizzle) guide: 1. Set up your schema in `src/lib/server/db/schema.ts` 2. Set up your database in `src/lib/server/db/index.ts` 2. ##### Set up API Route [Section titled “Set up API Route”](#set-up-api-route) Create a catch-all route for auth endpoints on the server. src/routes/api/auth/\[...gau]/+server.ts ```ts import { SvelteKitAuth } from '@rttnd/gau/sveltekit' import { auth } from '$lib/server/auth' export const { GET, POST, OPTIONS } = SvelteKitAuth(auth) ``` 3. ##### Add Server Hook [Section titled “Add Server Hook”](#add-server-hook) Add the SvelteKitAuth handle to make sessions available in `event.locals`. src/hooks.server.ts ```ts import { SvelteKitAuth } from '@rttnd/gau/sveltekit' import { auth } from '$lib/server/auth' export const { handle } = SvelteKitAuth(auth) ``` Now, in server routes, you can access `event.locals.getSession()` which returns `{ user, session, accounts, providers }`. For full type-safety, update your `src/app.d.ts`: src/app.d.ts ```ts import type { GauSession } from '@rttnd/gau' declare global { namespace App { interface Locals { getSession: () => Promise } } } export {} ``` 4. ##### Set up the Client [Section titled “Set up the Client”](#set-up-the-client) Wrap your app in an `AuthProvider` and use the `useAuth` hook to access session data. * SSR Pass the session from a server load function to avoid a flash of logged-out state. src/routes/+layout.server.ts ```ts export async function load({ locals }) { return { session: await locals.getSession() } } ``` src/routes/+layout.svelte ```svelte {@render children()} ``` * CSR For CSR-only apps, the session will be fetched client-side after hydration. Use `isLoading` to handle the loading state. src/routes/+layout.svelte ```svelte {@render children()} ``` Note If your frontend and backend are on different hosts, set `baseUrl` to the full URL of your backend’s auth API. Use envs to set it for development and production. ```svelte ``` Next, to get full TypeScript support for your provider IDs, create a typed `useAuth` hook. This is a great practice as you only have to type it once. src/lib/auth.ts ```ts import type { Auth } from '$lib/server/auth' import { useAuth as useAuthCore } from '@rttnd/gau/client/svelte' export const useAuth = () => useAuthCore() ``` Note Exporting and importing the `Auth` type is optional, but it gives better types for the `auth.signIn` method. By default, the `provider` param is `string`, but with the `Auth` type, it will be a union of string literals of the configured providers, like `'github' | 'google'`. Now you can use your custom `useAuth` hook in any component to access the session and authentication methods. src/routes/+page.svelte ```svelte {#if auth.session?.user}

Welcome, {auth.session.user.name}!

{:else} {/if} ``` For protecting routes on the client side, you can use the `` component: src/routes/dashboard/+page.svelte ```svelte

Dashboard

Welcome, {auth.session?.user?.name}!

``` Note For better security, consider [server-side protection](#server-side-protection) using `+page.server.ts`. See the [Protected Routes](/integrations/#protected-routes) section for more details. 5. ##### File Structure [Section titled “File Structure”](#file-structure) * src * lib * server * auth.ts * auth.ts * routes * api * auth * \[…gau] * +server.ts * +layout.server.ts * +layout.svelte * +page.svelte * app.d.ts * hooks.server.ts * package.json * svelte.config.js 6. ##### Next Steps [Section titled “Next Steps”](#next-steps) * Explore other [OAuth Providers](/oauth-providers). * See [Deployment](/guides/deployment) for production tips. * Learn about how `gau` works. # Vanilla Client > How to use gau's vanilla JavaScript client for any frontend framework or vanilla JS. The vanilla client can be used with any frontend setup. It’s perfect for: * Backend-only auth setups (Bun.serve, Elysia, etc.) * Frameworks not yet supported by `gau` * Frameworkless apps 1. ##### Set up your backend [Section titled “Set up your backend”](#set-up-your-backend) Follow one of the backend-only integration guides: [Bun.serve](/integrations/bun-serve/) [Elysia](/integrations/elysia/) Or use `createHandler` from `@rttnd/gau/core` with any framework that supports standard `Request`/`Response` objects. 2. ##### Create the client [Section titled “Create the client”](#create-the-client) Import `createAuthClient` and initialize it with your auth API base URL: src/auth.ts ```ts import process from 'node:process' import { createAuthClient } from '@rttnd/gau/client/vanilla' import type { Auth } from './server/auth' export const auth = createAuthClient({ baseUrl: process.env.AUTH_BASE_URL, // http://localhost:3000/api/auth in dev }) ``` Note The `Auth` type import is optional but provides better types for your configured providers. 3. ##### Use the client [Section titled “Use the client”](#use-the-client) The client provides methods to manage authentication: ###### Fetch Session [Section titled “Fetch Session”](#fetch-session) ```ts const session = await auth.fetchSession() console.log(session.user) // User | null ``` ###### Sign In [Section titled “Sign In”](#sign-in) Redirects to OAuth provider ```ts window.location.href = await auth.signIn('github', { redirectTo: '/dashboard', // Optional }) ``` ###### Sign Out [Section titled “Sign Out”](#sign-out) ```ts await auth.signOut() ``` ###### Link Account [Section titled “Link Account”](#link-account) ```ts window.location.href = await auth.linkAccount('google', { redirectTo: '/profile', }) ``` ###### Unlink Account [Section titled “Unlink Account”](#unlink-account) ```ts await auth.unlinkAccount('google') ``` ###### Refresh Session [Section titled “Refresh Session”](#refresh-session) ```ts await auth.refreshSession() ``` ###### Use a Server Session [Section titled “Use a Server Session”](#use-a-server-session) Start with the safe session returned by your server helper: ```ts const auth = createAuthClient({ baseUrl: '/api/auth', session }) ``` After navigation, apply fresh server data with `auth.setSession(session)`. This updates subscribers without making a request. To fetch current data from the server instead, use `auth.refreshSession()`. Create shared clients only in browser code. On the server, keep each visitor’s session separate. ###### Listen to Session Changes [Section titled “Listen to Session Changes”](#listen-to-session-changes) ```ts const unsubscribe = auth.onSessionChange((session) => { console.log('Session updated:', session) // Update UI, store in state management, etc. }) // Later: clean up listener unsubscribe() ``` ###### Handle Redirect Callbacks [Section titled “Handle Redirect Callbacks”](#handle-redirect-callbacks) After OAuth redirects back to your app, handle the callback: ```ts const handled = await auth.handleRedirectCallback((url) => { window.history.replaceState({}, '', url) // Example }) if (handled) { console.log('OAuth callback handled, session updated') } ``` 4. ##### Next Steps [Section titled “Next Steps”](#next-steps) * Learn about [Session Management](/guides/session-management) * Explore [OAuth Providers](/providers/) * See [Security best practices](/guides/security) #### Session Token Storage [Section titled “Session Token Storage”](#session-token-storage) For browser apps that use bearer tokens instead of session cookies, set `credentials: 'omit'`: ```ts const auth = createAuthClient({ baseUrl: 'https://auth.example.com/api/auth', credentials: 'omit', }) ``` Use the server’s default `session.strategy: 'auto'` or `'token'`. Add the frontend host to `trustHosts` and allow its origin in CORS. Use an absolute frontend URL for `redirectTo` when the backend is on another origin. Browser sign-in uses PKCE and stores the returned bearer token in `localStorage` under `__gau-session-token`. Call `handleRedirectCallback()` after returning to the app; framework providers do this on mount. Bearer-authenticated requests omit cookies. Bearer account linking requires the adapter’s atomic `verification` store, supported by the built-in memory and Drizzle adapters. Navigate to the URL returned by `linkAccount()` promptly: it is single-use and expires after one minute. #### API Reference [Section titled “API Reference”](#api-reference) ##### `createAuthClient(options)` [Section titled “createAuthClient\(options)”](#createauthclienttauthoptions) Creates a new auth client instance. **Options:** * `baseUrl` (required): Base URL of your auth API * `credentials`: Fetch credentials mode; defaults to `'include'` in browsers and `'omit'` in Tauri **Returns:** * `fetchSession()`: Fetches the current session from the server * `refreshSession()`: Fetches the session, updates client state, and notifies subscribers * `applySessionToken(token)`: Applies a session token manually * `onSessionChange(listener)`: Subscribes to session changes, returns unsubscribe function * `handleRedirectCallback(replaceUrl?)`: Handles OAuth redirect callbacks * `signIn(provider, options?)`: Initiates sign-in flow * `signOut()`: Signs out the user * `linkAccount(provider, options?)`: Links an additional OAuth account * `unlinkAccount(provider)`: Unlinks an OAuth account # Providers > Sign in with email or an OAuth provider. `gau` uses [ArcticJS](https://arcticjs.dev/) and can work with any OAuth provider, it’s just a little extra work sometimes to get the user’s verified email. For email codes and magic links, use the [Email provider](/providers/email/). You can combine email and social logins in the same provider list. [Email](/providers/email/) [GitHub](/providers/github/) [Discord](/providers/discord/) [Google](/providers/google/) [ChatGPT](/providers/chatgpt/) [Microsoft](/providers/microsoft/) [Facebook](/providers/facebook/) # ChatGPT > Use a ChatGPT plan in a local app or add ChatGPT sign-in to a website. ChatGPT supports two flows: * **[Local apps](#local-apps)** can use an eligible user’s ChatGPT plan with their permission, without a preregistered client ID or secret. * **[Web apps](#web-apps)** can sign users in with a registered OpenAI OAuth client. This flow is currently limited to selected commercial partners in OpenAI’s limited trial. ## Local apps [Section titled “Local apps”](#local-apps) For eligible open-source and locally hosted apps, users can grant access to their ChatGPT plan without a preregistered client ID or secret. Local support currently targets Tauri desktop apps. See [OpenAI’s availability guidance](https://developers.openai.com/siwc/token-sharing-open-source) for eligibility and plan access. ### Tauri desktop apps [Section titled “Tauri desktop apps”](#tauri-desktop-apps) The native plugin handles browser sign-in, credential storage, refresh, and requests to OpenAI. Tokens stay in Rust, outside the webview. This connection is separate from a gau app session. #### Install [Section titled “Install”](#install) Add gau to your frontend: ```sh bun add @rttnd/gau @tauri-apps/api@2 ``` Add the Rust plugin from your app’s `src-tauri` directory: ```sh cargo add tauri-plugin-gau ``` Register the plugin on your existing Tauri builder: src-tauri/src/lib.rs ```rust tauri::Builder::default() .plugin(tauri_plugin_gau::init()) .run(tauri::generate_context!()) .expect("error while running tauri application"); ``` Add `gau:default` to the capability for the window that uses ChatGPT: src-tauri/capabilities/default.json ```json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "windows": ["main"], "permissions": [ "core:default", "gau:default" ] } ``` The plugin uses `productName` and `identifier` from your Tauri config and saves data in Tauri’s app-local data directory. Set your app’s name and keep its identifier stable. App registration and local identity are saved automatically. Credentials are stored in an encrypted file with its key in the operating system’s credential store. On Linux, a Secret Service keyring, such as GNOME Keyring or KWallet with Secret Service enabled, must be running and unlocked. For app sessions through a hosted backend instead, follow the shared [Tauri guide](/runtimes/tauri/). #### Sign in and make requests [Section titled “Sign in and make requests”](#sign-in-and-make-requests) src/chatgpt.ts ```ts import { createChatGPT } from '@rttnd/gau/runtimes/tauri' export const chatgpt = createChatGPT() const account = await chatgpt.signIn() if (account.status !== 'ready') throw new Error('ChatGPT plan access is needed') const fetch = chatgpt.createFetch(account.id) const catalog = await fetch('https://api.openai.com/v1/models') if (!catalog.ok) throw new Error(`Could not load models (${catalog.status})`) const { models } = await catalog.json() const model = models.find((model: { visibility: string }) => model.visibility === 'list') if (!model) throw new Error('No models are available') const response = await fetch('https://api.openai.com/v1/responses', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model.slug, input: [{ role: 'user', content: 'Hello!' }], store: false, stream: true, }), }) if (!response.ok) throw new Error(`Request failed (${response.status})`) // Read response.body as a stream of server-sent events. ``` Use the selected account’s catalog for your model picker: display `display_name` and send `slug` as the model. Reload it when the user switches accounts. `createFetch(account.id)` returns a fetch-compatible function for plain requests or a JavaScript inference SDK’s custom fetch option. It adds credentials and refreshes them in Rust; you do not need an API key. HTTP errors return a standard `Response`, so check `response.ok` and `response.status`. Use the OpenAI JavaScript SDK Install `openai`, then pass the native fetch function to the SDK: ```ts import OpenAI from 'openai' const openai = new OpenAI({ apiKey: 'native-managed', dangerouslyAllowBrowser: true, fetch: chatgpt.createFetch(account.id), maxRetries: 0, }) ``` The SDK requires an `apiKey` value and permission to run in the webview. This value is a placeholder; Rust supplies the real credential. Use `openai.responses.create()` with the same streaming options shown above. The fetch function supports only `GET /v1/models`, `GET /v1/models/{id}`, and `POST /v1/responses` at `https://api.openai.com`. Responses requests must use `store: false`, `stream: true`, and an input array. Read the stream through its terminal event; success means receiving `response.completed`, not just an HTTP success status. See [OpenAI’s models and inference guide](https://developers.openai.com/siwc/token-sharing-open-source/models-and-inference) for request limits and stream handling. ### Manage saved accounts [Section titled “Manage saved accounts”](#manage-saved-accounts) `chatgpt.listAccounts()` returns saved account metadata without tokens. Let users choose an account, and pass its ID to `createFetch` or `signOut`; there is no default account. Use `chatgpt.signIn()` to add one, or `chatgpt.signIn({ accountId: account.id })` to sign in again for a saved account. Check `account.status` before making requests: * `ready`: the account has ChatGPT plan permission. * `identity-only`: the user signed in without granting plan access. * `signed-out`: local credentials were removed. * `reauth-required`: the user needs to sign in again. When the user chooses to grant plan access, ask for consent again: ```ts await chatgpt.signIn({ accountId: account.id, prompt: 'consent' }) ``` To disconnect an account: ```ts const result = await chatgpt.signOut(account.id) ``` This removes its local credentials and keeps the saved registration. If `result.revocation` is `failed`, local sign-out succeeded but remote revocation was not confirmed. The user can disconnect the app in ChatGPT settings. Pass an `AbortSignal` as `signIn({ signal })` or in the fetch options to cancel work. `chatgpt.close()` cancels this client’s pending work without signing out saved accounts. ## Web apps [Section titled “Web apps”](#web-apps) The `ChatGPT` provider signs users in to your app. It does not grant access to their ChatGPT plan or conversations. This web flow requires a registered OpenAI OAuth client and is currently available only to selected commercial partners through OpenAI’s limited trial. ### Resources [Section titled “Resources”](#resources) * [OpenAI website sign-in guide](https://developers.openai.com/siwc/website) * [Request an OAuth client](https://developers.openai.com/siwc/request-client-id) ### Environment variables [Section titled “Environment variables”](#environment-variables) .env ```txt OPENAI_CLIENT_ID= ``` Set `OPENAI_CLIENT_SECRET` only if OpenAI issued one for your client. Keep it on the server. ### Callback URL [Section titled “Callback URL”](#callback-url) Register the full callback URL with OpenAI, including your app’s origin: ```txt https://example.com/api/auth/callback/chatgpt ``` The URL must match exactly. Register the callback for each environment you use. ### Config [Section titled “Config”](#config) Set up [verification storage](/adapters/drizzle/#verification-storage) before enabling this provider. The `openid`, `profile`, and `email` identity scopes are requested by default. auth.ts ```ts import process from 'node:process' import { DrizzleAdapter } from '@rttnd/gau/adapters/drizzle' import { createAuth } from '@rttnd/gau/core' import { ChatGPT } from '@rttnd/gau/oauth' import { db } from './db' import { Accounts, Users, Verification } from './schema' export const auth = createAuth({ adapter: DrizzleAdapter(db, Users, Accounts, Verification), providers: [ ChatGPT({ clientId: process.env.OPENAI_CLIENT_ID!, }), ], }) ``` If OpenAI issued a client secret, add it to the `ChatGPT` config: ```ts clientSecret: process.env.OPENAI_CLIENT_SECRET!, ``` ### Sign in [Section titled “Sign in”](#sign-in) Use your [client integration](/integrations/) with the provider ID `chatgpt`: ```ts await auth.signIn('chatgpt') ``` ChatGPT accounts are not automatically linked by email. A signed-in user can link ChatGPT with `auth.linkAccount('chatgpt')`; see [Account Linking](/guides/account-linking/). # Discord > How to set up Discord as an OAuth provider with gau. ### Resources [Section titled “Resources”](#resources) * [Arctic Discord Provider](https://arcticjs.dev/providers/discord) * [Discord Developer Portal](https://discord.com/developers/applications) ### Environment variables [Section titled “Environment variables”](#environment-variables) .env ```txt DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= ``` ### Callback URL [Section titled “Callback URL”](#callback-url) Add the redirect URI in your Discord application under “OAuth2” → “General” → “Redirects”. ```plaintext /api/auth/callback/discord ``` ### Config [Section titled “Config”](#config) The `identify` and `email` scopes are requested by default. auth.ts ```ts import process from 'node:process' import { createAuth } from '@rttnd/gau/core' import { Discord } from '@rttnd/gau/oauth' export const auth = createAuth({ providers: [ Discord({ clientId: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET, }), ], }) ``` ### Provider params [Section titled “Provider params”](#provider-params) #### params [Section titled “params”](#params) > Type: `Record` > > Default: — Extra query params appended to the authorization URL. # Email > Sign in with an email code or magic link. The email provider sends a six-digit code, a sign-in link, or both. Add it to your provider list alongside social logins, or use it on its own. ### Environment variables [Section titled “Environment variables”](#environment-variables) .env ```txt AUTH_SECRET= RESEND_TOKEN= EMAIL_FROM=My App ``` Verify your sending domain in [Resend](https://resend.com/domains). For development, `onboarding@resend.dev` can send to the email address on your Resend account. ### Config [Section titled “Config”](#config) auth.ts ```ts import process from 'node:process' import { createAuth } from '@rttnd/gau' import { DrizzleAdapter } from '@rttnd/gau/adapters/drizzle' import { Email } from '@rttnd/gau/email' import { Resend } from '@rttnd/gau/email/resend' import { db } from './db' import { Accounts, Users, Verification } from './schema' export const auth = createAuth({ adapter: DrizzleAdapter(db, Users, Accounts, Verification), providers: [ Email({ from: process.env.EMAIL_FROM!, send: Resend({ apiKey: process.env.RESEND_TOKEN! }), mode: 'both', }), ], jwt: { secret: process.env.AUTH_SECRET! }, }) ``` Use your framework’s usual Gau handler and session setup. See [Getting Started](/guides/getting-started/) if this is your first provider. ### Verification storage [Section titled “Verification storage”](#verification-storage) Follow the shared [verification storage setup](/adapters/drizzle/#verification-storage) for the schema and adapter configuration. Email uses this storage for short-lived challenges and sending limits. Gau removes expired records when it starts a new email request. ### Sign in [Section titled “Sign in”](#sign-in) Use `signIn` from the vanilla client or your framework’s `useAuth()` helper: ```ts const result = await client.signIn('email', { email: 'you@example.com', }) // Save result.challengeId while the user enters their code. await client.signIn('email', { challengeId: result.challengeId, code: '123456', }) ``` The first call returns `status: 'verification-required'`, `challengeId`, `expiresAt` in milliseconds, and `retryAfter` in seconds. The second returns `status: 'authenticated'` and refreshes the client’s session. Errors reject the promise with a message, `code`, and HTTP `status`. Request another email by calling `signIn('email', { email })` again after `retryAfter`. Keep the new challenge ID. Earlier emails remain valid until they expire or are used. The client keeps verification proof in the current tab’s session storage. Enter the code in the tab or app where you requested it; you can read the email on another device. After a reload, your UI also needs to restore the challenge ID, or ask for another email. ### Magic links [Section titled “Magic links”](#magic-links) Set `mode: 'link'` to send only a link, or `mode: 'both'` to include a code too. The default is `'code'`. Open the link in the browser where you requested it, then press **Continue**. Visiting the link alone does not use it, so ordinary email previews cannot consume it. Opening it in another browser shows a message asking you to return to the original browser or enter the code there. Pass `redirectTo` when requesting the email to choose where the link sends the user after sign-in. It must be a URL on the same origin as the auth handler. Code verification updates the session without navigating; your app can navigate afterward. ```ts await client.signIn('email', { email: 'you@example.com', redirectTo: '/dashboard', }) ``` Codes and links expire after 10 minutes by default. Each challenge can be used once. When an email contains both, using either invalidates the other. Tauri uses codes and receives a session token after verification. Use `'code'` or `'both'`; token sessions omit the link. ### Link an email [Section titled “Link an email”](#link-an-email) While signed in, use `linkAccount` for both steps: ```ts const result = await client.linkAccount('email', { email: 'you@example.com' }) await client.linkAccount('email', { challengeId: result.challengeId, code: '123456', }) ``` The challenge belongs to the signed-in user. Linking does not replace their session. An email already owned by another user cannot be linked. Use `unlinkAccount('email')` to remove it; Gau prevents removing the last account. Email sign-in follows `autoLink`: by default, a verified email can sign in to an existing user with that primary email. With `autoLink: false`, that user must sign in another way and link their email first. This means enabling email sign-in also makes mailbox access a way to access existing accounts. `allowDifferentEmails` and `linkOnly` work with email too. OAuth token hooks such as `onOAuthExchange` do not run for email. ### Email senders [Section titled “Email senders”](#email-senders) #### Resend [Section titled “Resend”](#resend) `Resend({ apiKey })` uses the HTTP API. No extra package is needed. #### Cloudflare [Section titled “Cloudflare”](#cloudflare) Use [Cloudflare Email Service](https://developers.cloudflare.com/email-service/) with its REST API: ```ts import { Cloudflare } from '@rttnd/gau/email/cloudflare' Email({ from: 'login@example.com', send: Cloudflare({ accountId: env.CLOUDFLARE_ACCOUNT_ID, apiToken: env.CLOUDFLARE_API_TOKEN }), }) ``` In a Worker, you can pass an email sending binding instead: `Cloudflare({ binding: env.EMAIL })`. Cloudflare currently requires Workers Paid and a domain using Cloudflare DNS for sending to arbitrary recipients. Check its docs for current availability and pricing. #### SMTP and self-hosted email [Section titled “SMTP and self-hosted email”](#smtp-and-self-hosted-email) Install the optional SMTP dependency: ```sh bun add nodemailer ``` ```ts import { SMTP } from '@rttnd/gau/email/smtp' Email({ from: 'login@example.com', send: SMTP({ host: 'mail.example.com', port: 465, secure: true, auth: { user: env.SMTP_USER, pass: env.SMTP_PASSWORD }, }), }) ``` SMTP runs on Node and Bun. It works with self-hosted servers and services such as Amazon Simple Email Service (SES), using that service’s SMTP credentials. Use an HTTP sender in Workers. #### Custom sender [Section titled “Custom sender”](#custom-sender) `send` receives `{ from, to, subject, text, html }`. Resolve when your email service accepts the message, and throw if it fails. Keep credentials and senders on the server. ```ts Email({ from: 'login@example.com', send: async (message) => { await mailer.send(message) }, }) ``` ### Email content [Section titled “Email content”](#email-content) Gau includes a default template. Use `render` to change the subject, plain text, and HTML without changing your sender: ```ts Email({ from: 'login@example.com', send: Resend({ apiKey: env.RESEND_TOKEN }), mode: 'code', render: ({ code }) => ({ subject: 'Your sign-in code', text: `Your code is ${code}. It expires in 10 minutes.`, html: `

Your code is ${code}. It expires in 10 minutes.

`, }), }) ``` The callback also receives `email`, `url`, `expiresAt` as a `Date`, and `purpose` (`'signin'` or `'link'`). `code` and `url` depend on the mode. Escape any user-provided values you include in HTML. ### Limits [Section titled “Limits”](#limits) `expiresIn` sets the lifetime in seconds, and `maxAttempts` sets how many code guesses a challenge allows. Their defaults are `600` and `5`. Sending limits use the verification store and are shared across server instances: ```ts Email({ from: 'login@example.com', send: Resend({ apiKey: env.RESEND_TOKEN }), rateLimit: { resendAfter: 60, perAddress: 5, perIp: 20, total: 100, getClientAddress: request => getTrustedClientAddress(request), }, }) ``` These are the defaults: a 60-second resend delay, 5 sends per address per hour, 20 per IP, and 100 total. Supply `getClientAddress` from your trusted server runtime to enable the IP limit. Gau does not trust forwarded headers automatically. Adjust the total for your app’s traffic. All limits must be positive integers. Email addresses are trimmed and lowercased. Gau does not remove dots or `+` tags. Codes and link tokens are stored as keyed hashes; do not log them in your sender or template callback. # Facebook > How to set up Facebook as an OAuth provider with gau. ### Resources [Section titled “Resources”](#resources) * [Arctic Facebook Provider](https://arcticjs.dev/providers/facebook) * [Meta for Developers - Apps](https://developers.facebook.com/apps) ### Environment variables [Section titled “Environment variables”](#environment-variables) .env ```txt FACEBOOK_CLIENT_ID= FACEBOOK_CLIENT_SECRET= ``` ### Callback URL [Section titled “Callback URL”](#callback-url) Add the callback URL in your Facebook App under “Facebook Login” → “Settings” → “Valid OAuth Redirect URIs”. Localhost URLs are automatically allowed. ```plaintext /api/auth/callback/facebook ``` ### Config [Section titled “Config”](#config) The `email` and `public_profile` scopes are requested by default. Note Make sure to enabled the `email` and `public_profile` permissions for your Facebook App! auth.ts ```ts import process from 'node:process' import { createAuth } from '@rttnd/gau/core' import { Facebook } from '@rttnd/gau/oauth' export const auth = createAuth({ providers: [ Facebook({ clientId: process.env.FACEBOOK_CLIENT_ID, clientSecret: process.env.FACEBOOK_CLIENT_SECRET, }), ], }) ``` ### Provider params [Section titled “Provider params”](#provider-params) #### params [Section titled “params”](#params) > Type: `Record` > > Default: — Extra query params appended to the authorization URL. # GitHub > How to set up GitHub as an OAuth provider with gau. ### Resources [Section titled “Resources”](#resources) * [GitHub OAuth App Docs](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) * [GitHub OAuth App Settings](https://github.com/settings/developers) ### Environment variables [Section titled “Environment variables”](#environment-variables) .env ```txt GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= ``` ### Callback URL [Section titled “Callback URL”](#callback-url) GitHub doesn’t allow configuring multiple callback URLs for an OAuth App, so create multiple OAuth Apps for development and production. ```plaintext /api/auth/callback/github ``` ### Config [Section titled “Config”](#config) The `user:email` scope is requested by default to retrieve the user’s email address. auth.ts ```ts import process from 'node:process' import { createAuth } from '@rttnd/gau/core' import { GitHub } from '@rttnd/gau/oauth' export const auth = createAuth({ providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }), ], }) ``` ### Provider params [Section titled “Provider params”](#provider-params) #### params [Section titled “params”](#params) > Type: `Record` > > Default: — Extra query params appended to the authorization URL. # Google > How to set up Google as an OAuth provider with gau. ### Resources [Section titled “Resources”](#resources) * [Google OAuth 2.0 Docs](https://developers.google.com/identity/protocols/oauth2/web-server) * [Google Cloud Console](https://console.cloud.google.com/apis/credentials) ### Environment variables [Section titled “Environment variables”](#environment-variables) .env ```txt GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= ``` ### Callback URL [Section titled “Callback URL”](#callback-url) In your Google Cloud project, under “Authorized redirect URIs”, you need to add the callback URL. ```plaintext /api/auth/callback/google ``` Google allows adding multiple redirect URIs, so add both your development and production URLs. ### Config [Section titled “Config”](#config) The `openid`, `email`, and `profile` scopes are requested by default. auth.ts ```ts import process from 'node:process' import { createAuth } from '@rttnd/gau/core' import { Google } from '@rttnd/gau/oauth' export const auth = createAuth({ providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), ], }) ``` ### Provider params [Section titled “Provider params”](#provider-params) #### params [Section titled “params”](#params) > Type: `Record` > > Default: — Extra query params appended to the authorization URL. # Microsoft Entra ID > How to set up Microsoft Entra ID as an OAuth provider with gau. ### Resources [Section titled “Resources”](#resources) * [Microsoft Entra ID Docs](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc) * [Microsoft Entra admin center](https://entra.microsoft.com/) ### Environment variables [Section titled “Environment variables”](#environment-variables) .env ```text MICROSOFT_CLIENT_ID= MICROSOFT_CLIENT_SECRET= ``` ### Callback URL [Section titled “Callback URL”](#callback-url) Under your app registration’s “Authentication” settings, you need to add a “Web” platform and configure the redirect URI. ```plaintext /api/auth/callback/microsoft ``` Microsoft allows adding multiple redirect URIs, so add both your development and production URLs. ### Config [Section titled “Config”](#config) The `openid`, `profile`, `email`, and `User.Read` scopes are requested by default. Note If you plan to accept work and school accounts too, make sure to request the `verified_primary_email` optional claim. It’s under your app registration’s “Token configuration” settings. auth.ts ```ts import process from 'node:process' import { createAuth } from '@rttnd/gau/core' import { Microsoft } from '@rttnd/gau/oauth' export const auth = createAuth({ providers: [ Microsoft({ clientId: process.env.MICROSOFT_CLIENT_ID, clientSecret: process.env.MICROSOFT_CLIENT_SECRET, }), ], }) ``` ### Provider params [Section titled “Provider params”](#provider-params) #### tenant [Section titled “tenant”](#tenant) > Type: `'common' | 'organizations' | 'consumers' | (string & {})` > > Default: `'common'` The tenant segment used in the authorize and token endpoints. You can also pass a specific tenant ID. * `"consumers"` is personal accounts only. * `"organizations"` is work/school accounts only. * `"common"` works with both. #### prompt [Section titled “prompt”](#prompt) > Type: `'login' | 'none' | 'consent' | 'select_account' | (string & {})` Optional prompt behavior to request at Microsoft. #### params [Section titled “params”](#params) > Type: `Record` Extra query params appended to the authorization URL. # CLI > CLI reference for gau. Coming soon. # Core API > Core API reference for gau. Coming soon. # Runtimes > How to integrate gau with different runtimes. `gau` has support for these runtimes: [Tauri](/runtimes/tauri/) # Tauri > How to integrate gau with Tauri for desktop and mobile applications. `gau` has first-class Tauri support. This guide explains how to build desktop and mobile apps with OAuth, and shows how to set up deep linking to handle OAuth redirects. This guide assumes you have already completed the setup for your web framework (e.g., [SvelteKit](/integrations/sveltekit/)). For ChatGPT plan use without a hosted auth backend, follow the [ChatGPT Tauri setup](/providers/chatgpt/#tauri-desktop-apps) instead. It uses the native `tauri-plugin-gau` plugin. 1. ##### Initialize Tauri [Section titled “Initialize Tauri”](#initialize-tauri) Initialize Tauri in your project. This will create a `src-tauri` directory. * npm ```sh npx tauri init ``` * pnpm ```sh pnpm tauri init ``` * yarn ```sh yarn tauri init ``` * bun ```sh bun tauri init ``` * ni ```sh nlx tauri init ``` Make sure you have these installed: * npm ```sh npm i @tauri-apps/api npm i -D @tauri-apps/cli ``` * pnpm ```sh pnpm add @tauri-apps/api pnpm add -D @tauri-apps/cli ``` * yarn ```sh yarn add @tauri-apps/api yarn add -D @tauri-apps/cli ``` * bun ```sh bun add @tauri-apps/api bun add -D @tauri-apps/cli ``` * ni ```sh ni @tauri-apps/api ni -D @tauri-apps/cli ``` Also set up the `build` config in `tauri.conf.json`, and follow other Tauri setup steps specific to your frontend framework. 2. ##### Install Tauri Plugins [Section titled “Install Tauri Plugins”](#install-tauri-plugins) `gau` requires the following plugins: * [Single Instance](https://v2.tauri.app/plugin/single-instance) (recommended on desktop) * [Opener](https://v2.tauri.app/plugin/opener) * [Deep Link](https://v2.tauri.app/plugin/deep-linking) - npm ```sh npm run tauri add opener npm run tauri add deep-link ``` - pnpm ```sh pnpm tauri add opener pnpm tauri add deep-link ``` - yarn ```sh yarn run tauri add opener yarn run tauri add deep-link ``` - bun ```sh bun tauri add opener bun tauri add deep-link ``` - ni ```sh nlx tauri add opener nlx tauri add deep-link ``` And add the `single-instance` plugin to your `src-tauri/Cargo.toml`. src-tauri/Cargo.toml ```diff [dependencies] # ... +[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies] +tauri-plugin-single-instance = { version = "2.0.0", features = [ "deep-link" ] } ``` 3. ##### Configure Deep Linking [Section titled “Configure Deep Linking”](#configure-deep-linking) To handle OAuth callbacks, you need to configure a custom URL scheme. After the auth flow is done, the browser will open your app again with the URL you set (`gau://...`). In `src-tauri/tauri.conf.json`, add the `deep-link` plugin and define a scheme. **Set this to something unique to your app.** src-tauri/tauri.conf.json ```diff { // ... +"plugins": { +"deep-link": { +"desktop": { +"schemes": [ +"gau" + ] + } + } + } } ``` You need to configure `gau` to use this scheme: * SvelteKit src/routes/+layout.svelte ```diff - + {@render children()} ``` * Solid 2 src/app.tsx ```diff import { AuthProvider } from '@rttnd/gau/client/solid2' export default function App() { return ( - + ... ) } ``` * Vanilla src/auth/client.ts ```diff import process from 'node:process' import { createAuthClient } from '@rttnd/gau/client/vanilla' export const authClient = createAuthClient({ baseUrl: process.env.AUTH_BASE_URL, // http://localhost:3000/api/auth in dev scheme: 'gau', }) ``` Add required permissions to your Tauri capabilities: * Desktop (default.json) src-tauri/capabilities/default.json ```diff { // ... "permissions": [ "core:default", +"core:event:default", "deep-link:default", "opener:default" ] } ``` * Mobile (mobile.json) src-tauri/capabilities/mobile.json ```json { "$schema": "../gen/schemas/mobile-schema.json", "identifier": "mobile-capability", "windows": [ "main" ], "platforms": [ "iOS", "android" ], "permissions": [ "core:event:default", "deep-link:default", "opener:default", { "identifier": "opener:allow-open-url", "allow": [ { "url": "*" } ] } ] } ``` 4. ##### Set up Rust App Backend [Section titled “Set up Rust App Backend”](#set-up-rust-app-backend) Here is a working `src-tauri/src/lib.rs` to initialize the deep link plugin and listen for incoming URLs. The listener forwards the URL to the frontend via an event. src-tauri/src/lib.rs ```rust use tauri::{Manager, Emitter}; use tauri_plugin_deep_link::DeepLinkExt; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let mut builder = tauri::Builder::default(); #[cfg(desktop)] { builder = builder.plugin(tauri_plugin_single_instance::init( |app, _argv, _cwd| { if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); let _ = window.set_focus(); } }, )); } builder .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { #[cfg(any(windows, target_os = "linux"))] #[cfg(debug_assertions)] { use tauri_plugin_deep_link::DeepLinkExt; let _ = app.deep_link().register_all(); } let handle = app.handle().clone(); let handle_for_closure = handle.clone(); handle.deep_link().on_open_url(move |event| { if let Some(window) = handle_for_closure.get_webview_window("main") { if let Some(url) = event.urls().first() { let _ = window.emit("deep-link", url.to_string()); } } }); Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` 5. ##### Configure the Frontend [Section titled “Configure the Frontend”](#configure-the-frontend) The frontend needs to be configured to build as a static single-page app for Tauri, listen for the deep link event, and handle the sign-in flow. If you are building a full-stack app, you can still host your backend, and the Tauri app will communicate with it as well. * SvelteKit First, update `svelte.config.js` to use `@sveltejs/adapter-static` when building for Tauri. svelte.config.js ```js import process from 'node:process' import Auto from '@sveltejs/adapter-auto' import Static from '@sveltejs/adapter-static' import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' const isTauri = !!process.env.TAURI_ENV_PLATFORM /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: { adapter: isTauri ? Static({ fallback: 'index.html' }) : Auto(), }, } export default config ``` Configure Vite for Tauri. On mobile, Tauri v2 sets `TAURI_DEV_HOST` to the host it exposes to the device and tunnels traffic. Use it for `server.host` and HMR; on desktop you can use the usual local ports. * Desktop vite.config.ts ```ts import process from 'node:process' import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' const isTauri = !!process.env.TAURI_ENV_PLATFORM export default defineConfig({ plugins: [sveltekit()], server: { port: isTauri ? 4173 : 5173, strictPort: true, }, }) ``` * Android vite.config.ts ```ts import process from 'node:process' import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' const host = process.env.TAURI_DEV_HOST const isTauri = !!process.env.TAURI_ENV_PLATFORM const devPort = 1420 const hmrPort = 1430 export default defineConfig({ plugins: [sveltekit()], clearScreen: false, server: { port: isTauri ? devPort : 5173, strictPort: true, host: host || false, hmr: host ? { protocol: 'ws', host, port: hmrPort, } : undefined, }, }) ``` Your Tauri development URL in `tauri.conf.json` should match this port (e.g., `http://localhost:4173`). Create a root layout that disables SSR, as Tauri apps are client-side rendered. src/routes/+layout.ts ```ts export const ssr = false ``` The `@rttnd/gau/client/svelte` package automatically handles the Tauri-specific logic. Remember that your Tauri app is a static frontend and needs to communicate with your deployed backend. Use envs to set the `baseUrl` for development (`http://localhost:5173/api/auth`) and production. src/routes/+layout.svelte ```diff + {@render children()} ``` Finally, you can use the `useAuth` hook in your components. The `signIn` method will automatically open the browser for the OAuth flow, and the app will receive the auth code via the deep link and exchange it for a session token. src/routes/+page.svelte ```svelte {#if auth.session?.user}

Welcome, {auth.session.user.name}!

{:else} {/if} ``` * Vanilla vite.config.ts ```ts import process from 'node:process' import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' const isTauri = !!process.env.TAURI_ENV_PLATFORM export default defineConfig({ plugins: [sveltekit()], server: { port: isTauri ? 4173 : 5173, strictPort: true, }, }) ``` * Solid 2 vite.config.ts ```ts import process from 'node:process' import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' const host = process.env.TAURI_DEV_HOST const isTauri = !!process.env.TAURI_ENV_PLATFORM const devPort = 1420 const hmrPort = 1430 export default defineConfig({ plugins: [sveltekit()], clearScreen: false, server: { port: isTauri ? devPort : 5173, strictPort: true, host: host || false, hmr: host ? { protocol: 'ws', host, port: hmrPort, } : undefined, }, }) ``` * Desktop The vanilla client ships with helpers tailored for Tauri. Listen for tokens delivered via the Tauri deep link bridge with `startTauriBridge`. src/main.ts ```diff import { createAuthClient } from '@rttnd/gau/client/vanilla' const authClient = createAuthClient({ baseUrl: import.meta.env.VITE_API_URL, scheme: 'gau', +}) +await authClient.startTauriBridge() +document.getElementById('sign-in-github')?.addEventListener('click', async () => { +await authClient.signIn('github') +}) +document.getElementById('sign-out')?.addEventListener('click', async () => { await authClient.signOut() }) ``` * Android Build the Tauri frontend without SSR or server functions. vite.config.ts ```ts import solid from '@solidjs/vite-plugin' import { defineConfig } from 'vite' export default defineConfig({ plugins: [solid({ start: true, ssr: false })], }) ``` Set `frontendDist` to `../dist/client` in `tauri.conf.json`. The Solid client handles the Tauri OAuth callback automatically. src/App.tsx ```tsx import { AuthProvider } from '@rttnd/gau/client/solid2' {/* app */} ``` To call remote server functions, enable `serverFunctions` and follow the [cross-origin setup](/integrations/solidstart#cross-origin-server-functions). 6. ##### Configure the Backend [Section titled “Configure the Backend”](#configure-the-backend) When using Tauri, the request to your authentication backend might not have a standard `Origin` header. `gau`’s CSRF protection checks this header for `POST` requests. You should configure `trustHosts` in your `createAuth` options to allow requests from your Tauri app. src/lib/server/auth.ts ```diff import { createAuth } from '@rttnd/gau' export const auth = createAuth({ // ... other options trustHosts: ['tauri.localhost'], cors: { allowedOrigins: 'trust' }, }) ``` 7. ##### Android Development Notes [Section titled “Android Development Notes”](#android-development-notes) See the [README](https://github.com/Rettend/gau/blob/main/packages/example-sveltekit-tauri-mobile/README.md). Your application is now configured for desktop authentication with Tauri! When a user clicks a sign-in button, `gau` will open the system browser for the OAuth flow. After authorization, the provider redirects to a page that triggers the `gau://` deep link, sending an authorization code back to your Tauri app. The app then exchanges this code for a session token using PKCE, ensuring a secure handoff. This setup allows you to deploy the app on the web and build a desktop app too, with the same codebase.