Frontbacked Docs

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

ActionWhen it runs
createA new post is being created.
getOne post is being fetched.
listA collection of posts is being fetched.
readA grouped permission that applies to both get and list.
editAn existing post is being updated.
deleteA post is being deleted.
writeA 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:

VariableUse
$post.current.dataThe saved post data before the action. Useful for published/draft and owner checks.
$post.currentBackend metadata for the saved post, such as id and author id.
$post.pending.dataThe submitted or merged data that will be saved. before create and before edit hooks write here.
$post.pendingBackend metadata for the pending write, including the generated id during create.
$actionMetadata 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.
$actorCurrent 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.
$adminContent permission helpers for FRL-managed posts and profile data.
$secretSecret credentials referenced by FRL. fb-admin automatically creates Secret Keys fields for referenced keys.
$privatePrivate rule settings referenced by FRL. fb-admin automatically creates Private Settings fields for referenced keys.
$pagePage settings from the site's page settings.
$currencySite currency config, including value, baseCurrency, and the selected support mode when configured.
$contextScratch 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.
$localScratch 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:

  1. Strict schemas reject fields that do not belong to the post type.
  2. immutable protects fields that must never change.
  3. editableIf protects 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.