Permissions
Permissions are written with action-specific require[...] blocks. A post action is allowed only when its requirement expression resolves to a truthy value.
post articles {
create require[ $user.id ]
read require[ $access.read || $post.status == "published" ]
edit require[ $access.edit || $user.id == $prev.authorId ]
delete require[ $access.delete ]
}
If an action does not have a require[...] block, the action is denied.
Actions
| Action | When it runs |
|---|---|
create | A new post is being created. |
read | A post is being read. |
edit | An existing post is being updated. |
delete | A post is being deleted. |
Each action has a separate requirement because safe read access is often different from safe write access.
Runtime Values for Permissions
Permission expressions commonly use these variables:
| Variable | Use |
|---|---|
$post | The post involved in the action. During edit, this is the new final post after processing. |
$prev | The stored post before the action. Useful for ownership checks and immutable comparisons. |
$user | Current signed-in user. |
$access | Platform access flags such as read, edit, and delete. |
$secret | Secret credentials referenced by FRL. fb-admin automatically creates Secret Keys fields for referenced keys. |
$private | Private server-side settings referenced by FRL. fb-admin automatically creates Private Settings fields for referenced keys. |
$page | Page settings from the site's page settings. |
$currency | Platform currency config. $currency.value is the selected code and $currency.mode is currently fixed. |
$context | Scratch values created during the current rule run, usually in process or helper functions. |
$resolved | Webhook payload data during on resolved handlers. |
Public Submissions
For public forms, require the fields that prove the request is complete enough to accept.
post contacts {
schema {
name: { type: "string", min: 1, max: 120, required: true }
email: { type: "string", min: 3, max: 180, required: true }
message: { type: "string", min: 1, max: 2000, required: true }
acceptedTerms: { type: "boolean", required: true }
}
create require[
$post.email && $post.message && $post.acceptedTerms
]
read require[ $access.read ]
edit require[ $access.edit ]
delete require[ $access.delete ]
}
The schema enforces exact field shape. The create requirement adds an authorization rule for the public action.
Owner-Only Updates
For user-generated content, compare $user.id with $prev.authorId.
post comments {
schema {
id: { type: "string", required: true, immutable: true }
authorId: { type: "string", required: true, immutable: true }
articleId: { type: "string", required: true, immutable: true }
text: { type: "string", min: 1, max: 1000, required: true }
}
create require[
$user.id && $post.authorId == $user.id
]
read require[
$access.read
]
edit require[
$user.id == $prev.authorId
]
delete require[
$user.id == $prev.authorId || $access.delete
]
}
Pair this with immutable schema fields so an update cannot move a comment to another author or article.
Admin or Site Editor Actions
Use $access flags when the platform already knows the current user has permission to manage the site.
post products {
create require[ $access.edit ]
read require[ $access.read ]
edit require[ $access.edit ]
delete require[ $access.delete ]
}
This is the common pattern for site admin dashboards, catalog management, settings pages, and theme-owned content.
Mixed Public and Private Reads
A production theme often needs public reads for published content and private reads for drafts.
read require[
$post.status == "published" || $access.read || $user.id == $prev.authorId
]
Use $post or $prev depending on the action and what Frontbacked passes for that path. For stored ownership checks, $prev.authorId is the safer value during edit/delete because it cannot be changed by the incoming request.
Protecting Update Merge Behavior
When the frontend calls Frontbacked.updatePost, Frontbacked may merge the submitted patch into the previous post before validating the final result. That is convenient, but it means your rule must protect fields that should not change.
Use all three together:
- Strict schemas reject fields that do not belong to the post type.
immutableprotects fields that must never change.immutableUnlessprotects fields that can change only under an explicit guard.
schema {
id: { type: "string", required: true, immutable: true }
authorId: { type: "string", required: true, immutable: true }
siteId: { type: "string", required: true, immutable: true }
status: {
type: "string",
enum: ["draft", "published"],
required: true,
immutableUnless: $access.edit
}
}
edit require[
$access.edit || $user.id == $prev.authorId
]
This prevents a site from using a partial update to change ownership, site identity, or protected workflow state.
Common Mistakes
Avoid using frontend-only assumptions as security. Hidden inputs, disabled buttons, and JavaScript checks improve UX, but FRL is what protects saved data.
Avoid trusting mutable fields for ownership. During edit and delete, prefer $prev.authorId over $post.authorId.
Avoid broad edit requirements on public content. If public users can edit, tie the rule to a signed-in user and immutable owner fields.