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

ActionWhen it runs
createA new post is being created.
readA post is being read.
editAn existing post is being updated.
deleteA 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:

VariableUse
$postThe post involved in the action. During edit, this is the new final post after processing.
$prevThe stored post before the action. Useful for ownership checks and immutable comparisons.
$userCurrent signed-in user.
$accessPlatform access flags such as read, edit, and delete.
$secretSecret credentials referenced by FRL. fb-admin automatically creates Secret Keys fields for referenced keys.
$privatePrivate server-side settings referenced by FRL. fb-admin automatically creates Private Settings fields for referenced keys.
$pagePage settings from the site's page settings.
$currencyPlatform currency config. $currency.value is the selected code and $currency.mode is currently fixed.
$contextScratch values created during the current rule run, usually in process or helper functions.
$resolvedWebhook 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:

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