Impersonation
Impersonation allows superadmins to act as another user within a specific organization. Every session is logged for auditing.
Session Shape
When impersonating, session.impersonation is populated:
interface ImpersonationContext {
superadminId: string
superadminName: string
sessionId: string
startedAt: Date
}
// Check if currently impersonating
if (session.impersonation) {
// Acting as another user
}
Gates
Two built-in gates control impersonation access:
import { isSuperadminGate, notImpersonatingGate } from '@repo/auth'
// Only superadmins can access
await validateSession([isSuperadminGate])
// Superadmins who are NOT currently impersonating (e.g. for starting a new session)
await validateSession([isSuperadminGate, notImpersonatingGate])
How It Works
- Superadmin calls a
startImpersonationCommandwithuserIdandorganizationId - An
impersonation_sessionrow is created with a 1-hour expiration - A httpOnly secure cookie (
impersonation_session_id) is set - On each request,
hooks.server.tschecks the cookie, loads the impersonation session, and replacesevent.locals.sessionwith the impersonated user's session (includingsession.impersonationcontext) - The session ends when: the superadmin calls
stopImpersonationCommand, or the session expires
Database Schema
The impersonation_session table in the global database:
import { impersonationSessionsTable } from '@repo/auth/global'
// Columns
id // uuid, PK
superadminId // text, FK → globalUsersTable
impersonatedUserId // text, FK → globalUsersTable
impersonatedOrganizationId // uuid, FK → globalOrganizationsTable
createdAt // timestamp, default now()
expiresAt // timestamp, required
endedAt // timestamp, nullable
endReason // text, nullable (e.g. 'stopped', 'expired')
Relations: superadmin, impersonatedUser, impersonatedOrganization.
Logging
Every impersonation session is automatically recorded in the impersonation_session table. There is no built-in UI for viewing logs — apps should implement their own view if needed.
Query logs with Drizzle:
const { db, impersonationSessionsTable } = await getGlobalDb()
const logs = await db.query.impersonationSessionsTable.findMany({
orderBy: desc(impersonationSessionsTable.createdAt),
with: {
superadmin: { columns: { id: true, name: true, email: true } },
impersonatedUser: { columns: { id: true, name: true, email: true } },
impersonatedOrganization: { columns: { id: true, name: true } },
},
})
For paginated views, use the overviewToQuery pattern from @repo/db with the DataGrid component from @repo/table.