Authentication Collections
How to add built-in authentication to any collection, and how the auth endpoints work.
Setting auth: true on a collection turns it into an auth collection — Dyrected adds hashed password storage, login/logout endpoints, JWT issuance, and a /me endpoint automatically.
Enabling Auth
export default defineConfig({
collections: [
{
slug: 'users',
auth: true,
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'role', type: 'select', options: ['admin', 'editor', 'viewer'], defaultValue: 'viewer' },
{ name: 'avatar', type: 'relationship', relationTo: 'media' },
],
},
],
})When auth: true, Dyrected automatically adds:
email— unique, required, indexedpassword— hashed with bcrypt before storage, never returned in API responses
You do not need to declare email or password in your fields array.
Auth Endpoints
All auth endpoints are prefixed with /api/collections/{slug}.
POST /api/collections/users/login
Authenticate with email and password. Returns a JWT and the user document.
Request:
{
"email": "user@example.com",
"password": "my-password"
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "abc123",
"email": "user@example.com",
"name": "Jane Doe",
"role": "editor"
}
}The JWT has a default TTL of 2 hours. You can configure this with auth.tokenExpiration (see below).
POST /api/collections/users/logout
Invalidates the current session. Requires Authorization: Bearer <token> header.
Response: 200 OK with no body.
GET /api/collections/users/me
Returns the currently authenticated user's document.
Headers: Authorization: Bearer <token>
Response:
{
"id": "abc123",
"email": "user@example.com",
"name": "Jane Doe",
"role": "editor"
}password is always stripped from this response regardless of field-level access config.
POST /api/collections/users/refresh-token
Exchange a valid (non-expired) JWT for a fresh one with a new expiry.
Headers: Authorization: Bearer <token>
Response:
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }POST /api/collections/users/invite
Send an invitation to an email address. Requires Authorization: Bearer <token>.
Request:
{ "email": "newuser@example.com" }Response: 200 OK
{ "success": true, "message": "Invite sent to newuser@example.com." }Returns 409 if an account with that email already exists.
POST /api/collections/users/accept-invite
Create an account from a valid invite token. The token comes from the invite email. Any extra fields beyond token and password are saved on the new user document.
Request:
{
"token": "<invite-token>",
"password": "my-new-password",
"name": "Jane Doe"
}Response: 201 Created — same shape as /login: { token, user }. The user is logged in immediately. A welcome email is sent to the new account.
POST /api/collections/users/forgot-password
Sends a password-reset email to the provided address. In development with no email config, the email is captured by Ethereal and a preview URL is logged to the console. Always responds with 200 regardless of whether the email exists (prevents email enumeration).
Request:
{ "email": "user@example.com" }POST /api/collections/users/reset-password
Resets the user's password using the token from the reset email. Sends a passwordChanged confirmation email to the user after a successful reset.
Request:
{
"token": "<reset-token>",
"password": "new-password"
}Auth Config Options
You can pass an object instead of true to auth to customise behaviour:
{
slug: 'users',
auth: {
tokenExpiration: 7200, // JWT TTL in seconds (default: 7200 = 2h)
maxLoginAttempts: 5, // Lock account after N failed logins (default: 5)
lockTime: 600, // Lock duration in seconds (default: 600 = 10m)
useAPIKey: false, // Allow API key auth for this collection (default: false)
depth: 1, // Depth for populating relationships in /me response
cookies: {
secure: true, // Set Secure flag on session cookies
sameSite: 'Lax',
domain: '.mysite.com', // Share cookies across subdomains
},
},
}| Option | Type | Default | Description |
|---|---|---|---|
tokenExpiration | number | 7200 | JWT lifetime in seconds |
maxLoginAttempts | number | 5 | Failed login attempts before account lock |
lockTime | number | 600 | Account lock duration in seconds |
depth | number | 0 | Population depth for the /me endpoint |
cookies.secure | boolean | true in production | Whether to set the Secure cookie flag |
cookies.sameSite | string | 'Lax' | Cookie SameSite policy |
Using the Authenticated User in Access Functions
When a request includes a valid JWT, Dyrected decodes it and passes the user to every access function and hook:
access: {
read: ({ user }) => !!user,
update: ({ user, doc }) => user?.role === 'admin' || user?.id === doc.authorId,
}The user object is the raw document from your auth collection (without password), populated to the depth configured in auth.depth.
Multiple Auth Collections
You can have more than one auth collection — for example, separate users and admins collections with different fields and access rules:
collections: [
{ slug: 'users', auth: true, fields: [...] },
{ slug: 'admins', auth: true, fields: [...] },
]Each collection gets its own independent set of login/logout/me endpoints.
Admin UI Auth Flow
When the Admin UI loads:
- It checks for a stored JWT in
localStorage. - It calls
/api/collections/{authSlug}/meto validate the token. - If valid, it stores the user in context and renders the dashboard.
- On 401, it clears the token and redirects to the login screen.
The Admin UI prioritizes the __admins collection for login. If no __admins collection is present, it falls back to the first collection with auth: true. See Separated Auth Model for the full isolation model.