Skip to main content

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

  1. Superadmin calls a startImpersonationCommand with userId and organizationId
  2. An impersonation_session row is created with a 1-hour expiration
  3. A httpOnly secure cookie (impersonation_session_id) is set
  4. On each request, hooks.server.ts checks the cookie, loads the impersonation session, and replaces event.locals.session with the impersonated user's session (including session.impersonation context)
  5. 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.