Skip to content

Authentication & Authorization

Generated: 2026-01-04

NexisChat implements a dual-auth strategy:

  • WorkOS AuthKit: Primary authentication for enterprise SSO features
  • Better Auth: Fallback/self-hosted authentication option

Primary authentication provider with:

  • Google OAuth
  • GitHub OAuth
  • Microsoft OAuth
  • SAML SSO (enterprise)
  • Magic Link (passwordless)
apps/server/src/auth/workos.ts
import { WorkOS } from '@workos-inc/node'
const workos = new WorkOS(process.env.WORKOS_API_KEY)
// Get authorization URL
const authUrl = workos.userManagement.getAuthorizationUrl({
provider: 'authkit',
redirectUri: process.env.WORKOS_REDIRECT_URI,
clientId: process.env.WORKOS_CLIENT_ID
})
// Handle callback
const { user, accessToken } = await workos.userManagement.authenticateWithCode({
code,
clientId: process.env.WORKOS_CLIENT_ID
})

Self-hosted auth with:

  • Email/password
  • OAuth providers
  • Session management
  • Password reset
apps/server/src/auth/better-auth.ts
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!
}
}
})
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
}

Sessions are stored in PostgreSQL (session table) with:

  • Token indexed for fast lookup
  • Automatic expiration via TTL
  • User cascade deletion
// Server-side cookie settings
const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' as const,
path: '/',
maxAge: 60 * 60 * 24 * 7 // 7 days
}

All resources are scoped to users:

-- Every resource query includes userId filter
SELECT * FROM accounts WHERE user_id = $userId;
SELECT * FROM folders WHERE account_id IN (
SELECT id FROM accounts WHERE user_id = $userId
);
apps/server/src/middleware/subscription-limits.ts
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
}
}
}
apps/server/src/middleware/auth.ts
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 check
export 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
}
})
})
apps/whatsapp-web-server/src/middleware/auth.ts
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')
}
})
// Ensure user owns the WhatsApp account
export 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] }
})
1. User clicks "Sign in with Google"
2. Client redirects to WorkOS auth URL
3. User authenticates with Google
4. Google redirects to WorkOS
5. WorkOS redirects to our callback URL with code
6. Server exchanges code for tokens
7. Server creates/updates user in database
8. Server creates session
9. Server sets HttpOnly cookie
10. Client receives success, loads dashboard
apps/client/src/lib/auth.ts
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.ts
app.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')
})
  1. Never expose tokens in URLs - Use POST for sensitive data
  2. HttpOnly cookies - Prevent XSS token theft
  3. Secure flag - HTTPS only in production
  4. SameSite=Lax - CSRF protection
  1. Token rotation - Rotate on sensitive actions
  2. Session invalidation - On password change, logout
  3. IP binding - Optional, store IP for validation
  4. User agent tracking - Detect session hijacking
  1. Rate limiting - Per-user request limits
  2. Input validation - Zod schemas on all inputs
  3. CORS - Strict origin allowlist
  4. Content Security Policy - XSS prevention
// Client
async function logout() {
await fetch('/api/auth/logout', { method: 'POST' })
window.location.href = '/login'
}
// Server
app.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 })
})
# WorkOS
WORKOS_CLIENT_ID=client_xxxxx
WORKOS_API_KEY=sk_xxxxx
WORKOS_REDIRECT_URI=http://localhost:8787/auth/callback
# Better Auth
BETTER_AUTH_SECRET=your-secret-key
BETTER_AUTH_URL=http://localhost:8787
# OAuth Providers (if using Better Auth)
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxxxx
GITHUB_CLIENT_ID=xxxxx
GITHUB_CLIENT_SECRET=xxxxx