You’re building on Payload, and several people and systems need to sign in to it. Your team needs the admin panel, your customers need accounts on the website, a mobile app needs to call the API, and some outside system needs to read data without a person at the keyboard. Each of these signs in differently, and the choice you make for each one decides how sessions end, what a leaked credential can do and what you change before launch.
As of October 2026, the latest stable release of Payload is 3.92.1, and one line of config turns any collection into a set of users with login, logout, password resets and sessions built in (Payload’s authentication docs). This guide maps each caller to a sign-in method, then covers the setup, Google login, SSO and two-factor authentication, and the settings to change before launch.
- How authentication works in Payload
- Which sign-in method fits each caller
- What happens when someone logs in
- How to set up sign-in for your app’s users
- How a mobile app signs in to Payload
- API keys for integrations and services
- Google login, SSO and two-factor authentication
- What a signed-in user can do: access control
- Before you go live: a production checklist
- Frequently asked questions
- What to do next
How authentication works in Payload
Payload’s authentication lives on collections. Set auth: true on any collection, and every document in it becomes a user who can log in, log out and reset a password (Payload’s authentication docs). Payload adds the email, hash and salt fields for you, generates the login screen for the admin panel, and exposes the auth operations on its REST, GraphQL and Local APIs (Payload’s auth operations docs).
You can have several auth-enabled collections, but only one of them can open the admin panel. The admin.user setting in your Payload config names it, and it’s the users collection unless you say otherwise (Payload’s admin panel docs). Every other auth collection signs in to your own front end and API only.
For example, take a fitness club with three branches. Its staff manage classes, trainers and timetables in the admin panel, so they live in users. Its members book classes on the website and in a mobile app, so they live in a separate members collection and can never reach the admin panel. The club’s door-entry system also checks memberships through the API, and its staff sign in with the club’s Google Workspace accounts. We’ll use this club throughout. If you’re new to Payload’s collections and fields, our guide to Payload covers the content model first.
Which sign-in method fits each caller
Payload ships three ways for a request to prove who it is, HTTP-only cookies, JSON Web Tokens (JWTs) and API keys, and lets you add your own as a custom strategy. They all end the same way, with a user attached to the request as req.user, so the same access rules apply whichever method was used.
| Caller | Method | How the credential travels | At the fitness club |
|---|---|---|---|
| Staff in the admin panel | Cookie | Set by the login screen, sent by the browser | Branch managers editing the timetable |
| Website on the same domain or a subdomain | Cookie | Sent by the browser, with credentials: 'include' on cross-origin requests | Members booking on the club’s site |
| Website on a different domain | Cookie with sameSite: 'None', or a JWT | Cross-site cookie, or an Authorization header | A booking site on a separate domain |
| Mobile app | JWT | Authorization: Bearer <token>, kept in the device’s secure storage | The members’ app |
| Integration or background service | API key | Authorization: <collection> API-Key <key> | The door-entry system checking a membership |
| AI assistant over MCP | The MCP plugin’s own API keys | Bearer key per assistant | A manager asking an assistant for this week’s bookings |
| Outside identity provider | Custom strategy, SSO or a plugin | The provider’s session or token | Staff signing in with Google Workspace |
Cookies are the default for browsers. When someone logs in through the REST or GraphQL API, Payload sets an HTTP-only cookie called payload-token, which JavaScript in the page can’t read, and the browser sends it back on every request to the same site (Payload’s cookie strategy docs). Once your front end sits on a different domain, the cookie becomes a third-party cookie, and you either move to a subdomain or configure the cookie and Payload’s csrf and cors lists to allow it. Our guide to Payload and Next.js walks through that two-app setup.
JWTs suit anything that isn’t a browser. The login response also returns the token in its JSON body, and a client can send it back in an Authorization header, as JWT <token> or Bearer <token> (Payload’s JWT strategy docs).
API keys belong to one user each and never expire on their own, which makes them the simplest fit for a service that calls Payload on a schedule (Payload’s API key docs). AI assistants are the exception. They connect through Payload’s MCP plugin, which keeps a separate API Keys collection with per-collection permissions, and our guide to Payload’s MCP plugin covers how to scope them.
What happens when someone logs in
A successful login creates a token, sets the cookie and opens a session. Both the token and the cookie last two hours by default, set by tokenExpiration in seconds on the collection (Payload’s auth defaults in 3.92.1).
Sessions are on by default. Each login adds a session to the user’s document, with its own expiry, and the token carries that session’s ID (Payload’s auth operations docs). That’s what lets you end access before a token runs out:
- Logout ends the session the request came from. Pass
allSessions: trueto end every session the user has. - A password change by the user ends their other sessions and keeps the one they changed it from.
- A password change by an admin, or a password reset, ends all of the user’s sessions.
For example, a club member is logged in on a phone and a laptop and loses the phone. Changing the password on the laptop ends the phone’s session at once, without waiting two hours for its token to expire.
You can switch sessions off with useSessions: false. The tokens are then stateless, which saves a database write per login, but they can’t be revoked and stay valid until they expire, even after a password change (Payload’s authentication docs). Keep sessions on for anything with customer accounts.
To keep someone signed in past two hours, a client calls the refresh-token operation before the token expires and gets a new one. A token that has already expired can’t be refreshed, so the user logs in again. The admin panel can do this for you with admin.autoRefresh: true, which keeps staff logged in while the panel is open.
Payload also decides what goes inside the token. Fields marked saveToJWT: true, such as a role, travel in the token and are available on req.user without a database read (Payload’s token data docs). Keep it to small fields that access rules need, because everything in a JWT can be read by whoever holds it.
How to set up sign-in for your app’s users
Your customers get their own auth collection, separate from staff. The steps below build the fitness club’s members collection.
- Create the collection
- Decide who can sign up
- Verify email addresses
- Send reset links to your own front end
- Read the signed-in member
- Scope members to their own data
1. Create the collection
Add a collection with auth switched on and the options your members need. The lockout and expiry settings can differ from your staff’s, and loginWithUsername lets members log in with a username.
export const Members: CollectionConfig = {
slug: 'members',
auth: {
tokenExpiration: 60 * 60 * 24 * 7, // a week, in seconds
verify: true,
maxLoginAttempts: 5,
lockTime: 10 * 60 * 1000, // ten minutes, in milliseconds
},
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'homeBranch', type: 'relationship', relationTo: 'branches' },
],
}2. Decide who can sign up
Signing up is a create operation on the collection, so the collection’s create access rule decides who may do it. Return true to let anyone register from your website, or check something first. In our monorepo template, the users collection is invite-only by default, so a sign-up needs a valid invite token. The same rule switches to public, admin-only or closed sign-up for a client project.
3. Verify email addresses
With verify: true, a new member gets an email with a verification link and can’t log in until they’ve used it (Payload’s authentication email docs).
4. Send reset links to your own front end
The forgot-password email links to the admin panel by default. For members, override forgotPassword.generateEmailHTML so the link opens a reset page on your website, which then calls the reset-password operation with the token (Payload’s authentication email docs). The reset token expires after one hour by default, and Payload sends at most one reset email per user every 15 seconds (Payload’s forgotPassword operation in 3.92.1).
5. Read the signed-in member
A browser client calls GET /api/members/me, which returns the member and their token expiry, or null when nobody is signed in. On the server, payload.auth({ headers }) reads the same cookie or header through the Local API (Payload’s auth operations docs). Our Next.js guide shows the Server Component version.
6. Scope members to their own data
What a member can see comes from access rules, so the club’s bookings collection returns a query that limits a member to their own bookings:
read: ({ req: { user } }) => {
if (user?.collection === 'users') return true // staff see every booking
if (user?.collection === 'members') return { member: { equals: user.id } }
return false
},How a mobile app signs in to Payload
A mobile app signs in with a JWT in the Authorization header, because React Native has no browser cookie jar to hold Payload’s cookie. Payload accepts Bearer tokens out of the box, so the server needs no changes.
Our monorepo template ships an Expo mobile app that works this way, and its sign-in follows five steps:
- The app posts the email and password to
/api/users/loginand gets back the user, the token and its expiry. - It stores the token with
expo-secure-store, in the iOS Keychain or the Android Keystore, set toWHEN_UNLOCKED_THIS_DEVICE_ONLY, so the token can’t be read while the phone is locked and doesn’t sync to other devices. - A shared API client adds
Authorization: Bearer <token>to every request and sends no cookies. - The shared client carries a refresh call that posts to
/api/users/refresh-tokenand stores the new token. The template doesn’t refresh on its own, so each project decides when to call it, before a sensitive action or on a timer. - The client exposes one hook for a
401response, which a project wires to clear its cached data and send the user back to the login screen.
The token never goes into AsyncStorage, localStorage or a JavaScript global. For the fitness club, that’s the members’ app, pointed at the members collection instead of users. Keep the app’s token lifetime in step with how often members open it, because a member who comes back after the token has expired has to log in again.
API keys for integrations and services
An API key lets a service act as one Payload user without logging in. Switch it on with useAPIKey: true on an auth collection, and staff who have admin access can generate, regenerate or revoke a key on each user’s document (Payload’s API key docs). The service then sends the collection’s slug, the words API-Key and the key:
const response = await fetch(`https://club.example.com/api/members/${memberId}`, {
headers: {
Authorization: `integrations API-Key ${process.env.PAYLOAD_API_KEY}`,
},
})The key behaves differently from a password in four ways:
- Payload shows a new key once, then only a masked value. Copy it when you generate it.
- The key doesn’t expire, and changing the user’s password doesn’t disable it. Revoke or regenerate it when the service’s access should end.
- Changing
PAYLOAD_SECRETinvalidates every API key, because the secret encrypts them. - Access control applies exactly as it does to a person, so the key can do only what its user can do.
Give each integration its own user, in its own collection, with a role that grants exactly what it needs. For example, the club’s door-entry system gets an integrations collection with useAPIKey: true and disableLocalStrategy: true, so it can’t log in with a password at all, and a rule that lets it read members’ membership status only. If its key leaks, the holder can check whether a membership is active and nothing else.
Google login, SSO and two-factor authentication
Payload’s built-in strategy signs people in with an email or username and a password. Google login, other social logins, single sign-on (SSO) and two-factor authentication (2FA) aren’t part of the core package as of 3.92.1. You add them through one of four routes:
| Route | What you get | What you maintain | Fits |
|---|---|---|---|
| Payload Enterprise SSO | Sign-in through SAML or OAuth 2.0 identity providers, including Microsoft, Okta and Google, with profiles created on first login | An Enterprise licence | Staff who already sign in through a company identity provider |
payload-auth (Better Auth) | Social providers, passkeys, 2FA and organisations, for the admin panel and your app | A community plugin and Better Auth’s releases | Social login and 2FA for customers and staff |
payload-oauth2 | An OAuth 2.0 sign-in for any provider, with Google and Zitadel tested | A community plugin | One provider, such as Google, on the login screen |
| Custom strategy | Your provider’s session or token mapped to a Payload user | The strategy and the provider integration | Teams already on Auth0, Clerk, Auth.js or a company identity provider |
Payload Enterprise SSO is one of Payload’s Enterprise features, sold under an Enterprise licence (Payload’s SSO page).
payload-auth swaps Payload’s authentication for Better Auth, so one session covers the admin panel and your front end, and the admin’s login, sign-up, password reset and 2FA screens are replaced with Better Auth’s (payload-auth). As of October 2026, it supports Payload 3.79.1 and later 3.x releases (payload-auth’s package file).
payload-oauth2 adds an authorisation route and a callback route to an auth collection. A link to /api/users/oauth/google starts the Google sign-in, and the callback finds or creates the matching user, sets Payload’s cookie and redirects back to your app (payload-oauth2 on GitHub).
A custom strategy is a function on the collection that reads the incoming request, checks it against your provider and returns a Payload user or null (Payload’s custom strategies docs).
For example, the fitness club’s staff already use Google Workspace, and the club’s IT lead requires 2-Step Verification on those accounts. Signing staff in with Google, through payload-oauth2 or a custom strategy, gives the admin panel that second factor without Payload storing one. The club’s members sign in with an email and a password, so 2FA for them would come from payload-auth and Better Auth’s two-factor plugin.
What a signed-in user can do: access control
Authentication tells Payload who sent a request. Access control decides what that user may do, through functions on each collection, field and operation that receive req.user and return true, false or a query (Payload’s access control docs). Role-based access control (RBAC) is one field and a set of those functions. Give each user a roles field, check it in every rule, and use access.admin to decide which roles may open the admin panel at all (Payload’s admin panel docs).
Our monorepo template’s users collection adds two guards around sign-in. A beforeLogin hook rejects any user whose active checkbox is off, with a 403, and new users start inactive. A field hook on roles means only an admin can set roles, so a user editing their own profile can’t promote themselves, and the roles are saved to the token for the access rules to read.
Roles get more involved when one Payload serves several businesses or brands, because each user’s access is then scoped to their tenant as well. Our guide to multi-tenancy in Payload covers how tenancy and roles combine.
Before you go live: a production checklist
Several of Payload’s defaults suit local development and need a decision before launch. The defaults below are as of Payload 3.92.1.
- Set a long, random
PAYLOAD_SECRET, different in every environment. It signs every token and encrypts every API key. Our template’s checklist asks for at least 32 characters. - Set
cookies.secure: truein production. The default isfalse, and Payload adds theSecureflag on its own only whensameSiteis'None'(Payload’s cookie code in 3.92.1). Leave itfalseonly forhttp://localhost. - List your front-end domains in
csrf, and setserverURL. With an empty list, Payload accepts the cookie from any origin. With a list, a cookie sent from any other origin is treated as signed out (Payload’s cookie strategy docs). If a front end sends cookies across domains, list its origin incorstoo, without a wildcard. - Review the lockout. The default locks an account for 10 minutes after 5 failed logins. Set
maxLoginAttemptsandlockTimeper collection, and don’t set the attempts to0, which switches lockout off. - Set
tokenExpirationfor each collection, so staff and customers don’t share the two-hour default by accident. - Turn on
verifyfor any collection with public sign-up. - Rate-limit at your host or proxy. Payload 3.92.1 has per-account lockout and the 15-second gap between reset emails, but no limit on requests per IP address, so put one in front of
/api/*/loginand/api/*/forgot-password. - Disable GraphQL if you don’t use it, with
graphQL.disable: true, and setmaxComplexityif you do (Payload’s docs on preventing API abuse). - Keep
admin.autoLoginbehind a development check, so a test login never ships. - Log sign-ins if you need an audit trail. Payload doesn’t log sign-ins by default. An
afterLoginhook that writes to a log collection records who signed in and when, andafterChangehooks can record what they changed. - Stay on the latest stable release. Payload publishes its security advisories on GitHub, and our review of Payload covers how often they arrive and how to keep up.
