Github|...

Admin impersonation

Let an admin act as another user from the DevTools, with a warning bar, time-limited tokens and an audit log.

Impersonation lets an admin see and use the app exactly as one of its users does, which is the fastest way to reproduce “it looks wrong for me” reports. It is off unless a project enables it, it is started from the DevTools Access tab, and while it lasts the app is unmistakable: the page becomes a rounded sheet on an amber backdrop, with a bar above it naming who you are acting as and a Stop button.

The page is scaled, not pushed down: one factor for both axes, so the aspect ratio is untouched, 100vh layouts resolve to the framed area, and an app-shell layout keeps its bottom row on screen and its internal scrolling intact.

Warning

An impersonating admin has the target user’s full access, writes included. Grant spky admin add only to people you would trust with every account. Every session and every write is recorded.

Enable it

Add the impersonation block to sp00ky.yml and redeploy (or restart spky dev):

sp00ky.yml
impersonation:
  enabled: true              # default false
  tokenTtl: 15m              # lifetime of one token (1m to 1h); renewed automatically
  maxDuration: 2h            # hard cap on one impersonation session
  userTable: user            # the table whose records can be impersonated
  searchFields: [username, email]   # what the DevTools user picker searches

Then make sure the people who may impersonate are admins:

# Only admins can impersonate, and admins cannot be impersonated
spky admin add alice

With enabled absent or false, every deploy removes the impersonation access method and functions from the database, so no impersonation token can be issued or used, including tokens handed out before you turned it off. The SSP and scheduler also refuse to sign tokens unless SPKY_IMPERSONATION is on, and they never sign one without a non-empty SPKY_AUTH_SECRET.

Use it from the DevTools

  1. Sign in to the app as an admin and open the DevTools Access tab.
  2. Under Impersonate, search for a user. Admins are listed but cannot be picked.
  3. Enter a reason (it goes into the audit log), click Impersonate, and confirm.

The page switches to the user’s session: queries, permissions and live updates behave exactly as they do for that user. Their data lives in its own local bucket, which is deleted from your device when you stop. Stop impersonating in the warning bar or in the Access tab returns you to your own session.

How it is secured

  • Server-enforced. The client only asks. fn::_00_impersonate::start runs in the admin’s own SurrealDB session and is permitted only for _00_admin members. The backend then signs a token for a dedicated _00_impersonate access method, with a key derived from SPKY_AUTH_SECRET that never leaves the backend.
  • Checked on every authenticate. The access method re-validates the session row each time the token is used. The session must be open and unexpired, its admin must still be on the roster, and its target must exist and must not be an admin.
  • Short-lived. A token lives tokenTtl and the client renews it before it expires. Nothing is renewed past maxDuration.
  • Revocable. Stopping, ending the row server-side, removing the admin from the roster, disabling the feature, or rotating SPKY_AUTH_SECRET all end the session. An open connection notices within a minute.
  • No escalation. An impersonated session cannot start another one, cannot read the audit tables, and session rows cannot be edited except to end them.
  • Visible. The warning lives in a closed shadow root, and the framing is written as !important inline styles, so no app stylesheet can hide or undo either. Both are reverted exactly when impersonation ends. An app can restyle or replace the banner (see Make it yours); replacing it keeps a fallback, and only an explicit mode: 'none' turns the warning off.
Note

While impersonating, $access in SurrealDB is _00_impersonate, not the user’s usual access method. Rules written against $auth.id behave exactly as for the user, and so does the SSP’s view permission check. A table rule that compares $access directly (for example $access = "account") does not match an impersonated session.

Audit trail

-- Who impersonated whom, why, and until when
SELECT * FROM _00_impersonation ORDER BY started_at DESC;

-- Every write made while impersonating
SELECT * FROM _00_impersonation_write WHERE session = _00_impersonation:abc;

-- End a session from the server side (takes effect on the next check, within a minute)
UPDATE _00_impersonation:abc SET ended_at = time::now();

In your app

By default the warning is shown and the page is shifted and rounded for you; you do not have to render anything. The shift publishes its height as a CSS variable, which anything of yours that is fixed to the top of the viewport should use:

/* The framed page is laid out normally, so most apps need nothing. A header
   of your own that is fixed to the VIEWPORT (rather than inside the page)
   should clear the bar: */
.app-header {
  top: var(--sp00ky-impersonation-banner-height, 0px);
}

Make it yours

impersonationBanner in the client config covers three cases.

Restyle the built-in one. Pass a theme; anything you leave out keeps its default. Colours take any CSS value, so your own tokens work.

createClient({
  // ...
  impersonationBanner: {
    theme: {
      heightPx: 52,
      insetPx: 14,
      radiusPx: 20,
      background: 'linear-gradient(135deg, #a78bfa, #db2777)',
      text: '#3b0764',
      accent: '#3b0764',
      accentText: '#fff',
      stopLabel: 'End session',
      // `**…**` marks the emphasised part; the label is text, never markup.
      label: (info) => 'Support mode: **' + info.target + '**',
    },
  },
});
Prop Type Default Description
heightPx number 40 Height of the bar above the page.
insetPx number 10 Gutter left, right and below the page. `0` frames from the top only.
radiusPx number 14 The framed page's corners. `0` squares them off.
background string amber wash + rake Any CSS `background` for the backdrop behind the page.
text string dark brown Warning text colour on the backdrop.
accent string dark brown Stop button background.
accentText string off-white Stop button text colour.
stopLabel string 'Stop' Label of the button that ends the session.
label (info) => string Impersonating … The sentence. Wrap a part in `**` to emphasise it; the result is text, never markup.
noPageShift boolean false Keep the bar but leave the page layout untouched (do your own offset with the CSS variable).

Render your own. Set mode: 'custom' and build the bar yourself from useImpersonation(). Acknowledge it while it is on screen, so the client knows a warning is actually visible; the Solid hook does that for you with rendersBanner: true. If nothing acknowledges within about 2.5 seconds, the built-in banner appears, on the assumption that your banner failed to render rather than that nobody should be warned.

// 1. Tell the client you are rendering it.
createClient({ /* ... */ impersonationBanner: { mode: 'custom' } });

// 2. Render it, and acknowledge it while it is on screen.
function ImpersonationBar() {
  const { impersonation, stop } = useImpersonation({ rendersBanner: true });
  return (
    <Show when={impersonation()}>
      {(info) => (
        <div class="my-impersonation-bar">
          Acting as {info().target}
          <button onClick={() => void stop()}>Stop</button>
        </div>
      )}
    </Show>
  );
}

Show nothing. mode: 'none' renders nothing, ever, and does not fall back. It is your call, and then it is your job to make an impersonated session obvious: the page otherwise looks exactly like an ordinary session of someone else’s account. The client logs one console warning to say so.

To react to impersonation yourself, use the hook (Solid) or the auth service (any client):

import { Show } from 'solid-js';
import { useImpersonation } from '@spooky-sync/client-solid';

export function DangerZone() {
  const { isImpersonating } = useImpersonation();
  // The client already shows its own warning bar. Use the hook for extras,
  // such as hiding actions an admin should not take on someone's behalf.
  return (
    <Show when={!isImpersonating()}>
      <DeleteAccountButton />
    </Show>
  );
}
import { client } from './db'; // your Sp00kyClient instance

// Admin session required; the server refuses everyone else.
const users = await client.auth.searchImpersonationTargets('bob');
await client.auth.impersonate(users[0].id, 'Support ticket #4211');

client.auth.impersonation;              // { session, target, admin, tokenExpiresAt }
client.auth.subscribeImpersonation((info) => console.log(info));

await client.auth.stopImpersonating();  // back to the admin session
Prop Type Default Description
auth.impersonation ImpersonationInfo | null - The active impersonation, derived from the token in use.
auth.impersonate(targetId, reason) Promise<ImpersonationInfo> - Start. Admins only; `reason` needs at least 3 characters.
auth.stopImpersonating() Promise<void> - Return to the admin session. Always leaves the target, even offline.
auth.subscribeImpersonation(cb) () => void - Called immediately and on every change. Returns an unsubscribe.
auth.searchImpersonationTargets(search) Promise<ImpersonationCandidate[]> - The DevTools user search. Admins only.
auth.listActiveImpersonations() Promise<ActiveImpersonation[]> - Open sessions. Admins only.
useImpersonation() { impersonation, isImpersonating, stop } - Solid hook in `@spooky-sync/client-solid` and `client-solid2`.

Signing out while impersonating ends the impersonation and signs the admin out too.

Next steps