10x-media plugins
Dual Session

Dual Session

Give each Payload auth collection its own session cookie, so an admin session and a frontend session can coexist.

Payload signs every collection's token into one config-wide cookie, ${cookiePrefix}-token. With a single auth collection that is invisible. With two it is not: a customer logging in on your website overwrites the editor's admin session in the same browser, and logging into the admin panel logs them out of the website.

@10x-media/dual-session moves the collections you name onto cookies of their own, so both sessions live at once.

Experimental. This plugin changes how sessions are established, which is not a place where a subtle bug announces itself. It is covered by unit, integration and end-to-end tests against Payload's real routing on both Mongo and Postgres, but auth surfaces differ a lot between projects, and yours may take a path none of those cover. It ships under the beta dist-tag and the API can still change.

Try it in staging first, and check the thing it exists for: log in to the admin panel and to the website in one browser, and confirm both sessions survive a reload, a refresh and a logout of the other. Report anything that looks off: edge cases found in real projects are the point of the beta.

payload.config.ts
import { buildConfig } from 'payload'
import { dualSession } from '@10x-media/dual-session'

export default buildConfig({
  admin: { user: 'users' },
  collections: [users, customers],
  plugins: [dualSession({ collections: ['customers'] })],
})

A customer now signs in to payload-customers-token. users keeps payload-token and the admin panel never notices the plugin exists.

Already running another auth plugin? Check compatibility first. This plugin owns a collection's auth endpoints and decides where its cookie is written. Anything else doing either of those to the same collection (OAuth, magic links, passkeys, SSO, 2FA) collides, and usually collides silently. There is a two-minute check and the ways out in Other plugins that touch auth. Plugins that only read req.user or add a strategy are fine.

How it works

Two pieces:

  1. Shadowed endpoints. Payload appends a collection's built-in auth endpoints after the ones the collection declares, and routes to the first match. The plugin declares replacements for the six that touch the cookie, each delegating to the same core operation the built-in uses. Hooks, lockout, sessions and verification behave exactly as before; only the cookie name differs.

  2. An auth strategy registered on the collection, which authenticates from that cookie. It mirrors Payload's own JWT strategy step for step, including the CSRF gate, the verify check and the session sid check.

The endpoints it replaces are POST /login, POST /logout, POST /refresh-token, GET /me, POST /reset-password and POST /first-register. The rest are left alone because they never write a cookie: /forgot-password, /unlock, /verify/:id, /init.

Attributing requests

Payload's REST namespace is shared. /api/customers/me looks identical whether the admin panel asked or the website did, but the answer should differ: a browser holding both sessions has two valid users, and the request has to pick one.

The auth-scope proxy stamps each request with the session it is allowed to use, based on where it came from. Without it the plugin still works, falling back to a rule that keeps the admin panel reachable at the cost of precision on the website.

One collection with roles

If editors and website visitors are the same collection told apart by a roles field, the boundary is not between two collections. Give the entry a predicate and the cookie becomes a function of the user instead:

dualSession({
  collections: [{ slug: 'users', isolate: (user) => !checkRole(['admin', 'editor'], user) }],
})

Staff keep the shared cookie, byte for byte as core writes it; everyone else moves to payload-users-token. See One collection, two sessions.

Requirements

Payload ^3.83.0. Next is optional and only needed for the proxy; the plugin itself is framework-agnostic.

The collection backing the admin panel (admin.user) may only be listed together with an isolate predicate. It owns the shared cookie, and moving it wholesale would take the admin panel down. The plugin refuses to build a config that does.

Next

Quick start, then Scopes for the proxy. One collection, two sessions if your project has one users collection with roles, Frontends and clients if the website is not this Next app, Custom auth if you have your own SSO.

On this page