Dyrected Cloud
Use access control safely in Dyrected Cloud with Jexl rules, serializable named policies, and Cloud's built-in policy registry.
Use Dyrected Cloud access control when your content backend is hosted by Dyrected and your rules need to survive sync:schema. Cloud can enforce rules that are serializable: booleans, Jexl strings, serializable named policies, and Cloud's built-in policies.
If you only remember three things from this page, remember these:
- Jexl strings are the safest default for Cloud.
- Serializable named policies work when the policy value can sync as schema data.
- Function rules belong in self-hosted Dyrected, not Cloud schemas.
If you are new to access control itself, start with the Access Control overview. This page is the Cloud-specific guide.
The recommended path
For most Cloud projects, start here:
- use Jexl strings for collection, global, and field rules
- use a built-in Cloud policy when the policy name matches what you mean
- use a site-defined named policy only when its value is a string or boolean
That keeps your access rules portable. They can be stored with the site schema, synced to Dyrected Cloud, and enforced by the hosted content backend.
What works in Cloud
Dyrected Cloud can enforce these access rule shapes:
| Rule shape | Cloud support | Good default? | Notes |
|---|---|---|---|
true / false | Yes | Sometimes | Best for always-open or always-closed rules. |
| Jexl string | Yes | Yes | Best general-purpose Cloud rule. |
| Named policy reference | Yes, for built-in or serializable policies | Sometimes | Good when a reusable policy name makes the rule clearer. |
Here is a Cloud-safe rule that lets signed-in users see everything while anonymous readers only see published content:
access: {
read: "user != null ? true : { status: { equals: 'published' } }",
}This works in Cloud because the rule is a Jexl string. Cloud can store it with the synced schema and evaluate it in the hosted backend.
What sync:schema sends to Cloud
When you run npx dyrected sync:schema, Dyrected sends your serializable schema to the Cloud backend. Collection rules, global rules, field rules, and string or boolean policy values can sync.
Cloud sync keeps access-control data that can be represented as schema:
| Policy form | Cloud sync result |
|---|---|
accessPolicies: { canRead: true } | Kept |
accessPolicies: { canRead: "user != null" } | Kept |
{ policy: 'isAuthenticated' } | Kept because it resolves to a Cloud built-in |
Function rules and function policies do not become Cloud behavior. If a rule needs local services, direct database checks, side effects, or arbitrary server code, put that behavior in your application backend or use self-hosted Dyrected.
Site-defined policies
Use site-defined policies when you want a reusable name for a rule that still stays serializable.
This policy works in Cloud because canManageOrders is a Jexl string:
export default defineConfig({
accessPolicies: {
canManageOrders: "'admin' in user.roles || 'editor' in user.roles",
},
collections: [
defineCollection({
slug: 'orders',
access: {
update: { policy: 'canManageOrders' },
},
fields: [
{ name: 'status', label: 'Status', type: 'text' },
],
}),
],
globals: [],
})After sync, Cloud can resolve update: { policy: 'canManageOrders' } because the policy value was sent as schema data.
Use this rule:
- string and boolean policies can be user-defined and synced
- built-in Cloud policies are available without defining them in your site config
- policies that require executable code are self-hosted-only
Built-in named policies in Dyrected Cloud
Dyrected Cloud currently ships with these built-in named policies in its backend registry:
| Policy | Use it when | Common params |
|---|---|---|
isAuthenticated | The caller must be signed in. | — |
hasRole | Access depends on one or more role strings. | role, roles |
isSelf | The record should match the current user's own ID. | field |
isOwner | The record should match a custom ownership field such as owner, ownerId, or customerId. | field |
isTenantMember | Content should stay inside the current workspace, site, or tenant boundary. | field, tenantId |
hasCapability | Access depends on one or more capability strings. | capability, capabilities |
isPublished | Public reads should only see published entries. | field, value |
canReadDrafts | Certain roles or capabilities may read drafts. | roles, capabilities |
fieldEquals | You want a reusable equals filter. | field, value |
fieldIn | You want a reusable in filter. | field, values |
createdByCurrentUser | The record should match the user who originally created it. | field |
notSelf | The record should match everyone except the current user. | field |
These built-ins sit alongside any site-defined string or boolean policies that reached Cloud through sync:schema.
The rest of this page focuses on when to use them, not only what they are called.
Choose the right rule first
When you are writing a Cloud rule, use this quick decision guide:
| You want to say | Best fit |
|---|---|
| "Anyone can read this." | read: true |
| "Only signed-in users can do this." | Jexl: "user != null" or policy: { policy: 'isAuthenticated' } |
| "Only admins or editors can do this." | Jexl role check, { policy: 'hasRole', params: { roles: [...] } }, or a synced string policy such as { policy: 'canManageOrders' } |
| "Users can only see their own rows." | { policy: 'isSelf' }, { policy: 'isOwner' }, or { policy: 'createdByCurrentUser' } depending on the field |
| "Users can only see rows for this workspace or tenant." | { policy: 'isTenantMember' } |
| "Anonymous readers only see published content." | Jexl ternary or { policy: 'isPublished' } on public reads |
If the rule needs to react live in the admin form for field-level access, prefer Jexl. A Jexl field rule can be reflected in the editor UI and enforced by the API.
createdByCurrentUser vs isOwner
This is the most important distinction on the page.
Both policies return the same kind of result: a filter that compares a field on the document to the current user's ID. The difference is not a hidden engine behavior. The difference is the field they target by default and the meaning you are expressing.
createdByCurrentUser
Use createdByCurrentUser when the rule means:
"The current user may act on records they originally created."
By default, this policy checks the built-in createdBy field:
access: {
read: { policy: 'createdByCurrentUser' },
}That is the named-policy version of:
access: {
read: "{ createdBy: { equals: user.sub } }",
}This is a good fit for:
- contact submissions a user opened themselves
- support tickets the user submitted
- personal notes or drafts where "creator" is the rule you care about
Dyrected adds createdBy automatically through its built-in system fields when timestamps are enabled, and timestamps default to true. So this policy is especially convenient when you want to lean on that built-in field instead of creating your own.
isOwner
Use isOwner when the rule means:
"The current user may act on records owned by them according to this schema."
By default, isOwner checks a field named owner:
access: {
read: { policy: 'isOwner' },
}That is the named-policy version of:
access: {
read: "{ owner: { equals: user.sub } }",
}In real projects, you will often pass a custom field because many schemas use names like ownerId, customerId, or accountId:
access: {
read: { policy: 'isOwner', params: { field: 'ownerId' } },
}This is a good fit for:
- a project owned by one account
- an order owned by a customer
- a workspace record assigned to an owner
- any document where ownership is a first-class business concept
The practical difference
Use this rule of thumb:
| If your question is... | Use |
|---|---|
| "Who created this record?" | createdByCurrentUser |
| "Who owns this record right now?" | isOwner |
That distinction matters because creator and owner are often not the same concept in a real app.
If you point both policies at the same field, they behave the same way. For example, createdByCurrentUser and isOwner with params: { field: 'createdBy' } both return the same kind of equality filter. The dedicated policy name exists to make the rule's intent obvious to the next person reading the schema.
For example:
- an admin could create an order on behalf of a customer
- a team member could create a project that belongs to a workspace owner
- a record could be reassigned later, while
createdBystill points to the original creator
Cloud does not infer those semantics for you. It only compares the current user ID to the field you chose. Your job is to pick the field that matches your business meaning.
Side-by-side example
Here is the same collection using both policies so the difference is obvious:
export const Orders = defineCollection({
slug: 'orders',
access: {
// "Users can read orders they created"
read: { policy: 'createdByCurrentUser' },
// "Users can update orders they own"
update: { policy: 'isOwner', params: { field: 'customerId' } },
},
fields: [
{ name: 'customerId', label: 'Customer', type: 'text', required: true },
{ name: 'status', label: 'Status', type: 'text' },
],
})In that example:
readfollows the built-increatedByupdatefollows the custom business fieldcustomerId
That is a valid setup when "who created it" and "who should control it" are different rules.
When to use isSelf instead
Use isSelf when the record itself is the user's own record.
For example, a profile collection usually wants:
access: {
read: { policy: 'isSelf' },
update: { policy: 'isSelf' },
}That is clearer than isOwner when the target document is the user's own account, profile, or member record.
Use this rule of thumb:
isSelf: "this row is me"isOwner: "this row belongs to me"createdByCurrentUser: "I created this row"
Practical Cloud examples
Public published content, private drafts
Use a Jexl ternary when public readers should only see published entries but signed-in staff should see everything:
access: {
read: "user != null ? true : { status: { equals: 'published' } }",
}This is often the cleanest Cloud-safe pattern because one rule covers both audiences.
Role-based writing access
Use hasRole when your collections follow a simple admin/editor model:
access: {
create: { policy: 'hasRole', params: { roles: ['admin', 'editor'] } },
update: { policy: 'hasRole', params: { roles: ['admin', 'editor'] } },
delete: { policy: 'hasRole', params: { role: 'admin' } },
}If the same logic must also drive field-level admin behavior, the equivalent Jexl rule is often easier:
access: {
update: "'admin' in user.roles || 'editor' in user.roles",
}If you prefer a project-local policy name, use a synced string policy:
export default defineConfig({
accessPolicies: {
canManageOrders: "'admin' in user.roles || 'editor' in user.roles",
},
collections: [
defineCollection({
slug: 'orders',
access: {
create: { policy: 'canManageOrders' },
update: { policy: 'canManageOrders' },
},
fields: [
{ name: 'status', label: 'Status', type: 'text' },
],
}),
],
globals: [],
})Tenant-scoped data
Use isTenantMember when documents should stay inside the current site or workspace boundary:
access: {
read: { policy: 'isTenantMember', params: { field: 'workspaceId' } },
update: { policy: 'isTenantMember', params: { field: 'workspaceId' } },
}This is a good fit for multi-tenant project, invoice, or asset collections.
Reusable equals and in filters
Use fieldEquals and fieldIn when you want small reusable filters without inventing new custom policy names:
access: {
read: { policy: 'fieldEquals', params: { field: 'status', value: 'published' } },
}access: {
read: { policy: 'fieldIn', params: { field: 'status', values: ['review', 'published'] } },
}These are especially useful when you want a named-policy shape but the logic is still simple.
Jexl vs built-in policy in Cloud
In Cloud, use this rule of thumb:
| Reach for | When |
|---|---|
| Jexl string | You want the most portable Cloud-safe rule, especially for field access and admin reactivity. |
| Named policy | You want a reusable Cloud-backed rule and the policy value is serializable or built in. |
| Self-hosted Dyrected | The rule needs application-server code, local services, or side effects. |
If you are unsure, start with Jexl. Move to a named policy when the rule becomes repetitive or the policy name makes the intent clearer.
Troubleshooting
"It worked locally, but Cloud denied it"
Check these first:
- Did the rule you tested locally depend on application-server code?
- Is the policy value a Jexl string, a boolean, or a Cloud built-in?
- Should this rule be an inline Jexl string instead?
"I am not sure whether to use createdByCurrentUser or isOwner"
Ask this question:
Do I care about the document's original creator, or the document's business owner?
Then map the answer like this:
- original creator:
createdByCurrentUser - business owner field such as
ownerIdorcustomerId:isOwner - the document is the user's own record:
isSelf
"The admin form is not hiding or locking fields the way I expected"
For field access, prefer inline Jexl when the admin form should react live in the same way as the API.
"I changed access rules, but Cloud still behaves the old way"
Run:
npx dyrected sync:schemaIf the updated schema did not reach Cloud, the old rules are still active there.