Permissions
Permissions are written with allow rules. A post action is allowed only when every matching permission expression resolves to a truthy value.
version 2;
post articles {
allow create: $actor.id
allow get: $post.current.data.status == "published" || $admin.canRead("$this")
allow list(limit=20, max=100): true
allow edit: $actor.id == $post.current.authorId || $admin.canEdit("$this")
allow delete: $admin.canDelete("$this")
}
If an action does not have an allow rule, the action is denied.
Actions
| Action | When it runs |
|---|---|
create | A new post is being created. |
get | One post is being fetched. |
list | A collection of posts is being fetched. |
read | A grouped permission that applies to both get and list. |
edit | An existing post is being updated. |
delete | A post is being deleted. |
write | A grouped permission that applies to create, edit, and delete. |
Each concrete action can have a separate rule because safe single-post access is often different from safe list access. Use allow read: expression when the same expression should protect get and list, allow write: expression when the same expression should protect create, edit, and delete, or comma-separated actions such as allow create, edit: expression when a smaller group shares one rule.
Use comma-separated grouped actions when the same broad condition should cover every read and write action:
allow read, write: $actor.isSignedIn || $actor.isAdmin
Matching rules intersect. If a rule file declares allow read, write: ..., allow read: ..., and allow get: ..., a get request must pass all three matching rules.
allow list: decides whether a visitor may request a list. Use FQL where or filter to choose which rows appear in that list. Because allow read: also applies to list requests, allow read: and allow list: cannot use post-scoped values such as $post, $row, or $rowMeta. This also applies inside helper functions called from list permissions. Put single-post conditions in allow get: instead.
Runtime Values for Permissions
Permission expressions commonly use these variables:
| Variable | Use |
|---|---|
$post.current.data | The saved post data before the action. Useful for published/draft and owner checks. |
$post.current | Backend metadata for the saved post, such as id and author id. |
$post.pending.data | The submitted or merged data that will be saved. before create and before edit hooks write here. |
$post.pending | Backend metadata for the pending write, including the generated id during create. |
$action | Metadata for the current operation. $action.where contains requested read filters, $action.payment names the current payment event, $action.isRead is true for get/list, and $action.isWrite is true for create/edit/delete. |
$actor | Current requester. Use $actor.id, $actor.email, $actor.name, $actor.data, $actor.wallet, $actor.isGuest, $actor.isSignedIn, $actor.isAdmin, $actor.isEndpoint, and $actor.endpoint when available. |
$admin | Content permission helpers for FRL-managed posts and profile data. |
$secret | Secret credentials referenced by FRL. fb-admin automatically creates Secret Keys fields for referenced keys. |
$private | Private rule settings referenced by FRL. fb-admin automatically creates Private Settings fields for referenced keys. |
$page | Page settings from the site's page settings. |
$currency | Site currency config, including value, baseCurrency, and the selected support mode when configured. |
$context | Scratch values for the current rule run. Write it in before hooks, after hooks, endpoint bodies, and event blocks; read it in allow rules and helper functions. |
$local | Scratch values inside one fn call. Use it when a helper needs temporary values before returning a result. |
Admin Permission Helpers
Use $admin.canRead(...), $admin.canCreate(...), $admin.canEdit(...), and $admin.canDelete(...) when the current user must have site-admin permission for FRL-managed content.
post products {
allow create: $admin.canCreate("$this")
allow get: $post.current.data.status == "published" || $admin.canRead("$this")
allow edit: $admin.canEdit("$this")
allow delete: $admin.canDelete("$this")
}
Pass a post type for post-level permission, or a field path for field-level permission:
$admin.canEdit("products")
$admin.canEdit("products.price")
$admin.canEdit("products.images.url")
$admin.canEdit("$this")
$admin.canEdit("$this.price")
Field checks inherit from broader permissions. For example, products.price.amount passes when the admin has permission for products.price or products. Array indexes are ignored, so products.images[0].url is checked like products.images.url.
Inside a post or profile rule, $this means the current post type. For example, inside post products, $admin.canEdit("$this.name") is checked as products.name.
Calling a helper with no argument checks all-post permission for that action across FRL-managed content:
$admin.canRead()
$admin.canEdit()
That no-argument form is useful for broad admin dashboards. Prefer resource-specific checks when a rule protects one post type or one sensitive field.
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: "email", required: true }
message: { type: "string", min: 1, max: 2000, required: true }
acceptedTerms: { type: "boolean", required: true }
}
allow create: $post.pending.data.email && $post.pending.data.message && $post.pending.data.acceptedTerms
allow get: $admin.canRead("$this")
allow list(limit=25, max=100): $admin.canRead("$this")
allow edit: $admin.canEdit("$this")
allow delete: $admin.canDelete("$this")
}
The schema enforces exact field shape. The create allow rule adds an authorization rule for the public action.
Use $action.where when a list should only be available for a specific requested target:
post videos {
schema {
creator: { type: "user", required: true }
title: { type: "string", required: true }
video: { type: "file", required: true }
}
allow list(limit=20, max=100):
$actor.id &&
$action.where.creator &&
posts.count({
type: "subscriptions",
where: {
subscriber: $actor.id,
creator: $action.where.creator,
expiresAt: { $gt: time.now() }
}
}) > 0
allow get: $post.current.data.creator == $actor.id || $admin.canRead("$this")
}
$action.where reflects the where values the theme requested. It is useful for checking access before Frontbacked loads list rows.
Owner-Only Updates
For user-generated content, compare the signed-in user with a trusted stored owner value. If ownership is stored inside post data, protect that field with immutable.
post comments {
schema {
authorId: { type: "user", required: true, immutable: true }
articleId: { type: "string", required: true, immutable: true }
text: { type: "string", min: 1, max: 1000, required: true }
}
allow create: $actor.id && $post.pending.data.authorId == $actor.id
allow get: $admin.canRead("$this") || $actor.id == $post.current.data.authorId
allow edit: $actor.id == $post.current.data.authorId
allow delete: $actor.id == $post.current.data.authorId || $admin.canDelete("$this")
}
Pair owner checks with immutable schema fields so an update cannot move content to another author or parent record.
Site Admin Actions
For site-admin dashboards, catalog management, settings-like content, and other theme-owned data, use $admin helpers scoped to the post type being managed.
post products {
allow create: $admin.canCreate("$this")
allow get: $admin.canRead("$this")
allow edit: $admin.canEdit("$this")
allow delete: $admin.canDelete("$this")
}
Mixed Public and Private Reads
A production theme often needs public reads for published content and private reads for drafts.
allow get: $post.current.data.status == "published" || $admin.canRead("$this") || $actor.id == $post.current.authorId
Use $post.current for trusted saved values and $post.pending for values being written. For stored ownership checks during edit and delete, $post.current.data or $post.current is the safe side 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.editableIfprotects fields that can change only under an explicit guard.
schema {
authorId: { type: "string", required: true, immutable: true }
siteId: { type: "string", required: true, immutable: true }
status: {
type: "enum",
enums: ["draft", "published"],
default: "draft",
required: true,
editableIf: $admin.canEdit("articles.status")
}
}
allow edit: $admin.canEdit("articles") || $actor.id == $post.current.data.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, use $post.current instead of $post.pending.
Avoid broad edit rules on public content. If public users can edit, tie the rule to a signed-in user and immutable owner fields.