Authentication & Authorization
Authentication & Authorization
Section titled “Authentication & Authorization”Generated: 2026-01-04
Overview
Section titled “Overview”NexisChat implements a dual-auth strategy:
- WorkOS AuthKit: Primary authentication for enterprise SSO features
- Better Auth: Fallback/self-hosted authentication option
Authentication Providers
Section titled “Authentication Providers”WorkOS AuthKit
Section titled “WorkOS AuthKit”Primary authentication provider with:
- Google OAuth
- GitHub OAuth
- Microsoft OAuth
- SAML SSO (enterprise)
- Magic Link (passwordless)
import { WorkOS } from '@workos-inc/node'
const workos = new WorkOS(process.env.WORKOS_API_KEY)
// Get authorization URLconst authUrl = workos.userManagement.getAuthorizationUrl({ provider: 'authkit', redirectUri: process.env.WORKOS_REDIRECT_URI, clientId: process.env.WORKOS_CLIENT_ID})
// Handle callbackconst { user, accessToken } = await workos.userManagement.authenticateWithCode({ code, clientId: process.env.WORKOS_CLIENT_ID})Better Auth
Section titled “Better Auth”Self-hosted auth with:
- Email/password
- OAuth providers
- Session management
- Password reset
import { betterAuth } from 'better-auth'import { drizzleAdapter } from 'better-auth/adapters/drizzle'
export const auth = betterAuth({ database: drizzleAdapter(db, { provider: 'pg' }), emailAndPassword: { enabled: true }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! } }})Session Management
Section titled “Session Management”Token Structure
Section titled “Token Structure”interface Session { id: string // Session UUID userId: string // User reference token: string // Session token (HttpOnly cookie) expiresAt: Date // Expiration timestamp ipAddress?: string // Client IP userAgent?: string // Browser info createdAt: Date updatedAt: Date}Session Storage
Section titled “Session Storage”Sessions are stored in PostgreSQL (session table) with:
- Token indexed for fast lookup
- Automatic expiration via TTL
- User cascade deletion
Cookie Configuration
Section titled “Cookie Configuration”// Server-side cookie settingsconst cookieOptions = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, path: '/', maxAge: 60 * 60 * 24 * 7 // 7 days}Authorization Model
Section titled “Authorization Model”Resource Ownership
Section titled “Resource Ownership”All resources are scoped to users:
-- Every resource query includes userId filterSELECT * FROM accounts WHERE user_id = $userId;SELECT * FROM folders WHERE account_id IN ( SELECT id FROM accounts WHERE user_id = $userId);Subscription-Based Limits
Section titled “Subscription-Based Limits”interface SubscriptionLimits { maxAccounts: number maxTemplatesPerAccount: number features: { analytics: boolean automation: boolean prioritySupport: boolean }}
const PLAN_LIMITS: Record<string, SubscriptionLimits> = { free: { maxAccounts: 1, maxTemplatesPerAccount: 5, features: { analytics: false, automation: false, prioritySupport: false } }, pro_monthly: { maxAccounts: 5, maxTemplatesPerAccount: 50, features: { analytics: true, automation: true, prioritySupport: true } }, pro_yearly: { maxAccounts: 5, maxTemplatesPerAccount: 50, features: { analytics: true, automation: true, prioritySupport: true } }}Middleware Implementation
Section titled “Middleware Implementation”export const requireAuth = middleware(async ({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'You must be logged in' }) }
return next({ ctx: { ...ctx, user: ctx.session.user } })})
// With subscription checkexport const subscriptionLimits = middleware(async ({ ctx, next }) => { const subscription = await getSubscription(ctx.user.id) const limits = PLAN_LIMITS[subscription?.planType ?? 'free']
const accountCount = await countAccounts(ctx.user.id)
return next({ ctx: { ...ctx, subscription, limits, canAddAccount: accountCount < limits.maxAccounts } })})WhatsApp Server Auth
Section titled “WhatsApp Server Auth”WorkOS Middleware (Elysia)
Section titled “WorkOS Middleware (Elysia)”import { WorkOS } from '@workos-inc/node'
const workos = new WorkOS(process.env.WORKOS_API_KEY)
export const requireAuth = (app: Elysia) => app.derive(async ({ headers, set }) => { const token = headers.authorization?.replace('Bearer ', '')
if (!token) { set.status = 401 throw new Error('Unauthorized') }
try { const { user } = await workos.userManagement.getUser(token) return { user } } catch { set.status = 401 throw new Error('Invalid token') } })Account-Scoped Auth
Section titled “Account-Scoped Auth”// Ensure user owns the WhatsApp accountexport const requireSingleAccountProtected = (app: Elysia) => app.use(requireAuth).derive(async ({ user, params }) => { const account = await db .select() .from(accounts) .where(and(eq(accounts.phoneNumber, params.phoneNumber), eq(accounts.userId, user.id))) .limit(1)
if (!account.length) { throw new Error('Account not found') }
return { account: account[0] } })OAuth Flow
Section titled “OAuth Flow”Login Flow
Section titled “Login Flow”1. User clicks "Sign in with Google"2. Client redirects to WorkOS auth URL3. User authenticates with Google4. Google redirects to WorkOS5. WorkOS redirects to our callback URL with code6. Server exchanges code for tokens7. Server creates/updates user in database8. Server creates session9. Server sets HttpOnly cookie10. Client receives success, loads dashboardImplementation
Section titled “Implementation”export async function initiateLogin(provider: string) { const res = await fetch('/api/auth/url', { method: 'POST', body: JSON.stringify({ provider }) }) const { url } = await res.json() window.location.href = url}
// Server: apps/server/src/routes/auth.tsapp.post('/auth/url', async (c) => { const { provider } = await c.req.json() const url = workos.userManagement.getAuthorizationUrl({ provider, redirectUri: process.env.WORKOS_REDIRECT_URI, clientId: process.env.WORKOS_CLIENT_ID }) return c.json({ url })})
app.get('/auth/callback', async (c) => { const code = c.req.query('code')
const { user, accessToken } = await workos.userManagement.authenticateWithCode({ code, clientId: process.env.WORKOS_CLIENT_ID })
// Create/update user const dbUser = await upsertUser(user)
// Create session const session = await createSession(dbUser.id)
// Set cookie setCookie(c, 'session', session.token, cookieOptions)
return c.redirect('/app')})Security Considerations
Section titled “Security Considerations”Token Security
Section titled “Token Security”- Never expose tokens in URLs - Use POST for sensitive data
- HttpOnly cookies - Prevent XSS token theft
- Secure flag - HTTPS only in production
- SameSite=Lax - CSRF protection
Session Security
Section titled “Session Security”- Token rotation - Rotate on sensitive actions
- Session invalidation - On password change, logout
- IP binding - Optional, store IP for validation
- User agent tracking - Detect session hijacking
API Security
Section titled “API Security”- Rate limiting - Per-user request limits
- Input validation - Zod schemas on all inputs
- CORS - Strict origin allowlist
- Content Security Policy - XSS prevention
Logout Flow
Section titled “Logout Flow”// Clientasync function logout() { await fetch('/api/auth/logout', { method: 'POST' }) window.location.href = '/login'}
// Serverapp.post('/auth/logout', async (c) => { const session = getCookie(c, 'session')
if (session) { await db.delete(sessions).where(eq(sessions.token, session)) deleteCookie(c, 'session') }
return c.json({ success: true })})Environment Variables
Section titled “Environment Variables”# WorkOSWORKOS_CLIENT_ID=client_xxxxxWORKOS_API_KEY=sk_xxxxxWORKOS_REDIRECT_URI=http://localhost:8787/auth/callback
# Better AuthBETTER_AUTH_SECRET=your-secret-keyBETTER_AUTH_URL=http://localhost:8787
# OAuth Providers (if using Better Auth)GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.comGOOGLE_CLIENT_SECRET=xxxxxGITHUB_CLIENT_ID=xxxxxGITHUB_CLIENT_SECRET=xxxxx