Operations
Every authentication endpoint an auth collection exposes — login, logout, the current user, token refresh, password reset, and invitations — with exact request and response shapes and the matching SDK methods.
This page is the endpoint guide for auth collections. Use it when you already understand what auth is and need the exact route, request shape, return shape, or SDK helper for a specific operation.
Enabling auth: true on a collection mounts a fixed set of authentication endpoints under /api/collections/{slug}/. If you are new to the concept, start with the Overview first — this page assumes you already know what an auth collection is.
Throughout, {slug} is your collection's slug (for example users or __admins), and every path is prefixed with /api/collections/.
These endpoints are available for application-user auth collections on both Dyrected Cloud and self-hosted Dyrected.
The full list
| Method | Path | Auth | SDK method | Returns |
|---|---|---|---|---|
GET | /{slug}/init | Public | isInitialized() | { initialized } |
POST | /{slug}/first-user | Public | registerFirstUser(data) | { token, user } |
POST | /{slug}/login | Public | login(email, password) | { token, user } |
POST | /{slug}/logout | Public | logout() | { success, message } |
GET | /{slug}/me | Bearer | me() | the user document |
POST | /{slug}/refresh-token | Bearer | refreshToken() | { token } |
POST | /{slug}/forgot-password | Public | sendPasswordReset(email, options?) | { success, message, token?, resetUrl?, emailSent? } |
POST | /{slug}/reset-password | Public | resetPassword(token, password) | { success, message } |
POST | /{slug}/{id}/change-password | Bearer | changePassword(id, payload) | { success, message } |
POST | /{slug}/invite | Bearer | invite(email, options?) | { success, message, token, inviteUrl?, emailSent } |
POST | /{slug}/accept-invite | Public | acceptInvite(token, password, extra?) | { token, user } |
GET | /{slug}/tokens/verify | Public | verifyToken(token, type) | { valid, purpose, email, expired?, error? } |
"Bearer" means the request must include a valid Authorization: Bearer <token> header. The rest are public. The password field is stripped from every user object these endpoints return.
The examples below use the SDK, since that is how most apps call these. Every method maps 1:1 to the REST route in the table, so you can call the raw endpoint directly if you prefer.
Bootstrapping the first account
A brand-new auth collection has no users, and login needs one to exist. These two endpoints solve the chicken-and-egg problem.
init tells you whether any user exists yet, which is handy for deciding whether to show a "create first admin" screen or a normal login form:
const { initialized } = await client.collection("users").isInitialized();
// GET /api/collections/users/init → { "initialized": false }first-user creates that very first account. It only works while the collection is empty — once any user exists it returns 403. The first user is always created with roles: ["admin"], and you get back a signed-in session immediately:
const { token, user } = await client.collection("users").registerFirstUser({
email: "[email protected]",
password: "a-strong-password",
name: "Site Owner",
});
client.setToken(token);
// POST /api/collections/users/first-user → { token, user }Any extra fields you include (like name above) are saved onto the new document.
Logging in and out
login exchanges an email and password for a token and the user record. A wrong email and a wrong password return the same 401 with the message "Invalid email or password.", so the endpoint never reveals which accounts exist during normal failures:
const { token, user } = await client
.collection("users")
.login("[email protected]", "hunter2");
client.setToken(token);
// POST /api/collections/users/login → { token, user }If an account was invited or created without a password, attempting to log in before accepting the invitation returns a 401 with code: "PASSWORD_NOT_SET":
{
"error": true,
"message": "Password has not been set for this account. Please accept your invitation or set up your password.",
"code": "PASSWORD_NOT_SET"
}This explicit code lets your frontend immediately direct the user to accept their invite or request a setup link instead of showing a generic credential error.
If the collection's built-in lockout is enabled and someone keeps guessing wrong passwords, Dyrected temporarily locks that account and login starts returning 429 Too Many Requests until the lock window expires:
{
"error": true,
"message": "Too many login attempts. Try again later.",
"retryAfterSeconds": 600
}The response also includes a Retry-After header. By default Dyrected locks an account after 5 failed attempts for 10 minutes, and you can change both values on the collection with auth: { maxLoginAttempts, lockTime }.
logout now revokes the current server-tracked session, so the token stops working immediately even before it expires:
await client.collection("users").logout();
client.clearToken();
// POST /api/collections/users/logout
// → { "success": true, "message": "Logged out." }The normal SDK helper covers the current session only. If you want to sign the account out everywhere, call the same endpoint with allSessions=true:
await fetch("/api/collections/users/logout?allSessions=true", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});Calling clearToken() still matters because your app should stop sending the token it was holding, but the server now revokes the backing session too.
Reading the current user
me returns the signed-in user's current document. It requires a Bearer token and returns the user object directly — not wrapped in a { user } envelope:
const user = await client.collection("users").me();
// GET /api/collections/users/me
// → { "id": "...", "email": "[email protected]", "name": "Jane", "roles": [...] }If the token is missing or invalid you get a 401; if the account was deleted since the token was issued you get a 404.
Refreshing a session
Tokens expire (7 days by default). refresh-token issues a fresh token from a still-valid one, extending the current session without asking the user to log in again:
const { token } = await client.collection("users").refreshToken();
client.setToken(token);
// POST /api/collections/users/refresh-token → { token }This requires a valid Bearer token — an expired token cannot refresh itself, so refresh before it lapses. The refreshed token keeps the same underlying session instead of silently creating a second one.
Password recovery
This is a two-step flow. First, forgot-password generates a single-use reset token and dispatches an email:
await client.collection("users").sendPasswordReset("[email protected]", {
resetUrl: "https://mysite.com/reset-password", // optional
});
// POST /api/collections/users/forgot-password
// → { "success": true, "message": "If an account with that email exists, a reset link has been sent." }It always returns 200 with the same message whether or not the email belongs to a real account, so it can never be used to probe which emails are registered.
When you pass a resetUrl, Dyrected builds a clickable link by appending token=<token> to it, and the email links there. If omitted, Dyrected uses auth.urls.resetPassword from your collection config or the default admin reset URL.
Headless password reset tokens
If you send password reset links through your own transactional pipeline (such as SMS, WhatsApp, or an internal customer service tool), you can mint the reset token without triggering Dyrected's email dispatch:
const { token, resetUrl } = await client.collection("users").createPasswordResetToken(
"[email protected]",
{ resetUrl: "https://mysite.com/reset-password" },
);Passing sendEmail: false to sendPasswordReset achieves the same result:
const result = await client.collection("users").sendPasswordReset("[email protected]", {
sendEmail: false,
});
// result.token contains the minted token, result.emailSent is falseSecond, reset-password sets the new password using the token:
await client
.collection("users")
.resetPassword(tokenFromEmail, "a-new-password");
// POST /api/collections/users/reset-password
// → { "success": true, "message": "Password has been reset. You can now log in." }The reset token is single-purpose and time-limited — it defaults to one hour (configurable with auth.resetPasswordExpiration on the collection) and only works against the collection it was issued for. A successful reset also revokes that user's active sessions so older login tokens stop working.
Changing a known password
When the user is signed in and just wants to change their password, use change-password instead of the reset flow. It requires a Bearer token and targets the user's own id:
await client.collection("users").changePassword(user.id, {
oldPassword: "hunter2",
newPassword: "an-even-better-password",
confirmPassword: "an-even-better-password",
});
// POST /api/collections/users/{id}/change-password → { success, message }A successful password change revokes that user's active sessions too, so any older tokens need to be replaced with a fresh login.
Inviting users
Invitations let an existing, signed-in user bring someone new in without handing out a shared password. invite sends an invitation email to an address that does not yet have an active account — it requires a Bearer token, and returns 409 only if the email is already registered with an active user who has a usable password:
const inviteEntryUrl = "https://myapp.com/accept-invite";
await client.collection("users").invite("[email protected]", {
inviteUrl: inviteEntryUrl,
data: {
roles: ["editor"],
firstName: "Alex",
department: "Marketing",
},
});
// POST /api/collections/users/invite
// → { success: true, message, token, inviteUrl, emailSent: true }When you pass inviteUrl, Dyrected builds a clickable acceptance link with token=<token>. If omitted, it resolves auth.urls.invite on the collection config, falling back to the admin invite URL.
Pre-created and pending accounts
Calling invite creates a pending user record:
- Invited users appear in the admin list immediately with their pending status.
- Any initial fields passed in
data(such asroles,name, or custom attributes) are saved directly onto the pending record. - If a user record was already pre-created in the database (for example, imported from an external system with
password: null), callinginvite()does not fail with 409. Dyrected identifies the pending account, refreshesinvitedAt, saves any updated data, and dispatches a fresh invite token. - Re-inviting an unaccepted user refreshes their expiration window and supersedes older invite tokens.
Headless invitation tokens
If you need to deliver invitations out-of-band (via SMS, clipboard copy in an admin panel, or a third-party CRM), use createInviteToken:
const { token, inviteUrl } = await client.collection("users").createInviteToken({
email: "[email protected]",
inviteUrl: "https://myapp.com/onboarding",
data: { tier: "premium" },
});
// emailSent is false; no outbound email is triggeredThe invited person completes signup with accept-invite, which is public — the invite token is what authorizes it. Their email comes from the token, so they only choose a password (plus any extra fields you collect). On success the pending user is activated, signed in, and the response is 201:
const { token, user } = await client.collection("users").acceptInvite(
inviteTokenFromEmail,
"their-chosen-password",
{ name: "New Teammate" }, // optional extra fields
);
client.setToken(token);
// POST /api/collections/users/accept-invite → { token, user }An invite token defaults to 7 days (configurable via auth.inviteExpiration). Accepting an already-used or superseded invite returns 409.
Validating tokens on frontend mount
When building custom invite acceptance or password reset pages, verify the token immediately when the frontend view mounts. This avoids letting a user enter a password only to receive an error when submitting an expired or invalid link.
Use verifyToken:
const verification = await client.collection("users").verifyToken(tokenFromUrl, "invite");
if (!verification.valid) {
if (verification.expired) {
// Show "Your invitation has expired. Please contact an admin."
} else {
// Show "Invalid or already-used invitation link."
}
} else {
// Token is valid; display user email: verification.email
}This calls GET /api/collections/:slug/tokens/verify?token=...&type=invite|reset and returns:
{
"valid": true,
"purpose": "invite",
"email": "[email protected]"
}Or for an expired or invalid token:
{
"valid": false,
"error": "Invite token has expired. Please request a new invitation.",
"expired": true
}Error shapes
Errors come back with a matching HTTP status and a consistent body:
{ "error": true, "message": "Invalid email or password." }Through the SDK these surface as a DyrectedError with a statusCode and an errors array, so you can branch on the status:
import { DyrectedError } from "@dyrected/sdk";
try {
await client.collection("users").login(email, password);
} catch (err) {
if (err instanceof DyrectedError && err.statusCode === 401) {
// show "wrong email or password"
}
}Where to go next
- Token Data — what the returned token carries and how
useris built on the server - JWT Strategy — signing, expiry, and external verification
- Cookie Strategy — where to store the token these endpoints return
- Email — customize the reset and invite emails
Admin SSO
Let your team sign in to the Dyrected dashboard with an external identity provider — Okta, Azure AD, Google Workspace, or any OIDC provider — through the adminAuth config, with just-in-time provisioning and your own access rules.
JWT Strategy
How Dyrected signs session tokens — the DYRECTED_JWT_SECRET, HS256 signing, token expiry, the Authorization Bearer header, and how another service can verify a Dyrected token.