Plugins
Impersonation

Configuration

Options for access, cookies, duration, UI, and session seams.

OptionTypeDefaultDescription
access.impersonate({ req, target, targetCollection }) => true | falserequiredStart gate. Only literal true allows.
access.filterTargets({ req, targetCollection }) => Where | booleanunsetWho the switcher lists. true = whole collection, false = hide it, Where = filter the list. Start re-checks this. Omit to show everyone the viewer can read.
access.terminate({ req }) => true | falsedenyRemote end of an open row. Return true to allow. That principal can end any open row by id. In swap mode this signs the impersonator out of the admin. See Limits.
access.readRecordsPayload AccessdenyRead the impersonation-sessions collection. Does not put the collection in the nav. Create, update, and delete stay denied.
disabledbooleanfalseReturn the incoming config unchanged.
collectionSlugstringimpersonation-sessionsRecord collection. Create, update, and delete are denied. Hidden until ui.sessionsCollection is true.
collection.overrides(collection) => collectionunsetLast pass over the built sessions collection. Whatever it returns is registered, including access. Do not rename fields the plugin reads, and do not change the slug here.
apiPathstring/impersonationRoot endpoint prefix. Must not collide with a collection slug.
hintCookieNamestringimpersonation-hintRow id hint. Must not start with cookiePrefix.
decorateRequestsbooleantruePaint req.user._impersonation on the target session, including both emails. false skips that field only. maxDuration still closes the session on the next authenticated request.
reason'off' | 'optional' | 'required''off'Note stored on the row.
maxDurationnumber (seconds)unsetAbsolute cap from startedAt. Unset means refresh can keep the session alive.
retention{ deleteAfterDays, cron?, queue? }offOpt-in job: close stale open rows, then delete closed rows older than N days. Host must run the queue (jobs.autoRun). Open rows are never deleted.
cookies.clearOnSwitchstring[]['payload-tenant']Extra cookies expired on swap start and exit. Parallel leaves them in place. See Multi-tenancy.
security.trustedOriginsstring[][]Extra Origins allowed for cookie POSTs.
targetsstring[]every local-strategy auth collectionCollections that can be impersonated.
session.issue / revoke / bindingfunctionsPayload local-authCustom auth seams.
uifalse or per-surface flagsall trueSee below.
translationsper-locale mapbuilt-insSee Translations.

UI flags

Each flag is independent. ui: false turns all of them off. The provider still mounts, so useImpersonation() works from your own components.

FlagDefaultWhat it does
ui.headerActiontrueImpersonate in the admin header. Opens the user drawer.
ui.bartrueBottom bar while a session is open. It sits beside the nav, not over it. Collapse keeps an Impersonation chip that snaps to the edges of the content area.
ui.documentActiontrueImpersonate next to Save on a target document (beforeDocumentControls, so it still shows when the dots menu is hidden). Hidden while a session is already open.
ui.recordActiontrueEnd session beside Save on an impersonation-sessions document (beforeDocumentControls). Hidden when the row already has endedAt. Create, update, and delete stay denied.
ui.sessionsCollectionfalsePut impersonation-sessions in the admin nav. access.readRecords still decides who can read a row.
ui.cardunsetReplacement for the default switcher row. Same props as ImpersonationUserCard: collectionSlug, doc, onSelect, onOpenDrawer, documentHref, title, email, openDocumentLabel, openDrawerLabel. Pass a component path, or { path, exportName } for a named export. Omit to keep the default card.
ui.cardEmailtrueShow email under useAsTitle on default switcher cards.
ui.userLabel'title'How the bar, collapsed chip, and frontend banner name users. 'title' is the collection's useAsTitle, 'email' is the email. Each falls back to the other.

Build your own control with useImpersonation from @10x-media/impersonation/client. It returns { status, setStatus, start, exit, end, reasonMode, targetFilters, apiPath, cardEmail, sessionCollection }. targetFilters is the access.filterTargets result (true or a Where per collection). status starts from the server render and updates after the bar checks the live session. StartConfirmModal is exported too.

collection.overrides receives the built sessions collection and whatever it returns is registered, including access. Do not rename fields the plugin reads, and do not change the slug here. Use collectionSlug for that.

getImpersonation

Server helper. Pass user when you already called payload.auth, so it does not authenticate again. Omit user to authenticate from headers. Pass user: null when the request is anonymous.

const { user } = await payload.auth({ headers })
const impersonation = await getImpersonation({ headers, payload, user })

side is target when this request is the impersonated session, and impersonator when it is the admin side of a parallel session.

See Frontend for the shipped banner.

access.filterTargets

The switcher cannot call access.impersonate (that needs a concrete target). Use filterTargets so the list does not show people start would 403.

filterTargets: ({ req, targetCollection }) => {
  if (req.user?.roles?.includes('admin')) {
    return targetCollection === 'users' ? { roles: { not_in: ['admin'] } } : true
  }
  if (req.user?.roles?.includes('editor')) {
    return targetCollection === 'members'
  }
  return false
}

This is UX. access.impersonate is still the start check. Start also re-runs the filter so list and start cannot disagree.

On this page