When a paying customer says "the dashboard is broken for me," you have two choices.
You can ask for screenshots, walk them through troubleshooting steps, and spend 30 minutes on a problem you could solve in 90 seconds -- or you click "Login as User" and see exactly what they see.
User impersonation is one of those features every SaaS support team eventually needs. Most founders build it twice: once badly, then again after a security concern. Here is the safe way to do it from the start.
Your admin panel already lets you search for users and edit their data. That handles about half of support tickets.
The other half are "nothing works for my account" tickets -- a broken layout, a permission error, or an edge case in the user's specific data. You cannot reproduce it from the admin side. You need to see what they see.
Without impersonation, you are debugging blind. With it, you click a button, land in the user's session, reproduce the problem in 10 seconds, fix it, and close the ticket.
The wrong approach: use the user's password to log in normally. This breaks your audit trail and exposes you to liability.
The right approach uses a short-lived, single-use impersonation token:
This guarantees three things: the token expires in 5 minutes and is single-use; every impersonation is logged with who did it and when; the admin can read and act as the user -- not become them permanently.
You need one new table. It stores the token, the admin who created it, the user being impersonated, whether it has been used, and when it expires.
In Drizzle ORM this follows the same pattern as every other module in the Next.js SaaS Boilerplate: a schema file, a relations file, a types file, and a service that throws HttpError instead of returning error objects.
Your JWT payload gets one optional field: impersonatedBy. When that field is present, the session belongs to the target user but records which admin started it. Every write in that session goes to the user's data, and your audit log records the impersonation event alongside it.
POST /api/admin/impersonation -- admin-only, creates the token. Verify the caller is an admin, generate a cryptographically random 32-byte token, insert it with a 5-minute expiry, return the token. Never log the raw token value.
GET /api/impersonation/enter?token=xxx -- the token is the credential. Find it, verify it is not expired or already used, mark it used in the same transaction, create a short-lived JWT for the target user with impersonatedBy set, redirect to the app.
POST /api/impersonation/exit -- clears the impersonated JWT and sends the admin back to the admin panel.
Never let an impersonated session run without a visible indicator. Support staff occasionally forget they are viewing as a customer and take actions that affect real data. The banner is the safety net.
It reads the impersonatedBy field from the current JWT. If it is set, it renders at the top of every page and stays there:
{session?.impersonatedBy && (
<div className="bg-yellow-500 px-4 py-2 text-sm font-medium text-black">
Viewing as {session.email} --
<button onClick={exitImpersonation} className="ml-1 underline">
Exit impersonation
</button>
</div>
)}
Because the boilerplate already ships JWT auth with getUserFromRequest, Drizzle ORM, and an admin module, you are connecting three pieces that exist -- not building them from scratch.
Password changes. An admin impersonating a user must not be able to change that user's password and lock them out. Check for impersonatedBy in your password-change service and throw HttpError(403) if it is present.
Account deletion. Same reason. The user needs to be able to return after the session ends.
Everything else is allowed -- that is the point.
Decide who can impersonate whom before you write the first line of code.
The safest rule: only users with the admin role can impersonate, and they cannot impersonate other admins -- only regular members. This stops privilege escalation. An attacker who compromises a regular account cannot reach admin access through impersonation.
If your app uses the multi-tenancy module with orgRoleEnum, the check is one condition: callerRole === 'admin' && targetUser.role !== 'admin'.
User impersonation is a one-afternoon build on top of the auth and admin panel you already have. One table, three routes, one banner component.
The next time a customer says "something is wrong with my account," you open their session in 15 seconds and close the ticket in five minutes.
Get the Next.js SaaS Boilerplate and add impersonation before your first paying customer needs support.