Token Data
What a Dyrected session token actually carries — the identity claims sub, email, and collection — and how the full user object in your access rules and hooks is rebuilt from the database on each request.
A Dyrected session token is deliberately thin. It carries just enough to identify who is making a request, and nothing more. The rich user object you read in access rules and hooks is not pulled from the token — it is rebuilt from the database on each request. Understanding that split is the key to writing correct access control, so this page lays out exactly what is in the token and what is not.
For how the token is signed and verified, see JWT Strategy.
What the token carries
Every session token contains three identity claims plus the standard JWT timestamps:
| Claim | Meaning |
|---|---|
sub | The user's document id — the "subject" of the token |
email | The user's email address |
collection | The slug of the auth collection that issued the token |
iat | Issued-at time (added automatically) |
exp | Expiry time (added automatically) |
That is the whole payload for a normal session. Notably, the token does not include the user's roles, name, or any other profile field. It is an identity claim, not a snapshot of the document.
If you have seen older notes suggesting the token embeds id, roles, or a slug, those do not match current behavior. The payload is sub, email, and collection (plus iat/exp). Roles come from the database, not the token — see below.
A few tokens carry an extra marker for single-purpose flows. Password-reset and invitation tokens include a purpose of "reset" or "invite", and tokens minted by dashboard SSO carry providerId and authSource: "external". These are used internally by the matching endpoints; your application code works with the three identity claims.
Why roles still work in access rules
Here is the part that surprises people. This access rule works perfectly, even though roles is not in the token:
export const Orders = defineCollection({
slug: 'orders',
access: {
update: ({ user }) => user?.roles?.includes('admin'),
},
fields: [{ name: 'total', label: 'Total', type: 'number' }],
})It works because Dyrected re-hydrates the user on every request. The flow is:
- The request arrives with
Authorization: Bearer <token>. - The server verifies the token and reads
subandcollectionfrom it. - When a database is connected, it loads that user's current document, strips the
password, and overlays the token's identity claims on top. - The result is handed to your access functions and hooks as
user.
So user is the live database record, not a frozen copy from login time. If an admin changes someone's roles, the next request reflects it immediately — there is no need to log out and back in to pick up new permissions. The cost is one lookup per authenticated request, which is why the token stays thin: the source of truth is the database, and the token just points at it.
The shape of user
Inside access rules and hooks, user is the authenticated user's document with a few guaranteed fields overlaid from the token:
interface AuthenticatedUser {
sub: string // the user's document id (always present)
collection: string // the auth collection slug (always present)
email?: string // from the token, or the document
roles?: string[] // present when the collection has a roles field
[key: string]: unknown // every other field on the user's document
}Because the whole document (minus password) is included, any custom field you added to the auth collection is available too — user.name, user.plan, user.organizationId, and so on. Reach for user.sub when you need the id in a form that is always present, and user.roles for permission checks.
// Owner-only update, using a custom field plus the guaranteed id
access: {
update: ({ user, doc }) => user?.sub === doc?.ownerId,
}When there is no database
Dyrected degrades gracefully rather than failing closed in two cases:
- No database configured. For setups without a connected database, there is nothing to hydrate from, so
userfalls back to just the token's identity claims (sub,email,collection). Custom fields androleswill not be present, so write access rules that only depend on identity in that scenario. - A transient database error. If the lookup fails momentarily, the server logs it and falls back to the same identity claims rather than rejecting the request outright.
If the account has been deleted since the token was issued, hydration finds nothing and the request is treated as unauthenticated (401) — a deleted user is not a valid session.
Practical implications
- Keep permission data on the user document, not in the token. Roles and any attributes your access rules check should live as fields on the auth collection, where hydration will surface them. Do not try to stuff them into the token — there is no mechanism to add custom claims, and hydration already gives you the whole document.
- Permission changes take effect on the next request. Because
useris live, role and field edits apply immediately. - A separate service that only has the token sees only identity. If another system verifies a Dyrected token directly, it gets
sub,email, andcollection— to act on roles it must look the user up bysub. See JWT Strategy.
Where to go next
- JWT Strategy — signing, expiry, and external verification
- Access Control — turn the
userobject into permissions - Operations — the endpoints that issue these tokens
Cookie Strategy
Where to store a Dyrected session token between requests — httpOnly cookies, client-readable cookies, and storage — and what each choice means for security, since the server authenticates only from the Authorization header.
Plugins (Coming Soon)
What to expect from the Dyrected plugins section while the public plugin docs are still being written.