FRL Reference
This page is a compact reference for Frontbacked Rule Language.
File Shape
fn helperName(arg) {
return arg;
}
post postType {
process {
$post.field = "value";
}
schema {
field: { type: "string", required: true }
}
create require[ $user.id ]
read require[ $access.read ]
edit require[ $access.edit ]
delete require[ $access.delete ]
create on create[]
edit on edit[]
delete on delete[]
}
auth {
schema {
displayName: { type: "string", max: 120 }
}
edit require[ $user.id ]
}
endpoint.post endpointName("/path/:id") {
verify $request.params.id
idempotency `${$request.params.id}:${$request.body.reference}`
process {
return { ok: true };
}
}
Top-Level Declarations
| Declaration | Purpose |
|---|---|
fn name(params) { ... } | Define a reusable helper function. |
post name { ... } | Define one post type. |
auth { ... } | Define schema and permissions for custom signed-in user data. |
endpoint.get/post/put/delete name("/path") { ... } | Define a custom guarded JSON endpoint. |
Post Sections
| Section | Purpose |
|---|---|
process { ... } | Normalize and compute values before validation. |
create/edit/delete/read process { ... } | Run extra action-specific normalization before schema validation. |
after create/edit/delete/read { ... } | Run after permission checks and before triggered writes. |
schema { ... } | Define allowed fields and validation rules. |
list { ... } | Set list visibility and default/max list sizes. |
export name { ... } | Render filtered rows into a downloadable file. |
create require[...] | Guard create actions. |
read require[...] | Guard read actions. |
edit require[...] | Guard update actions. |
delete require[...] | Guard delete actions. |
on resolved field from "..." match ... { ... } | Handle async webhook resolution. |
create/edit/delete on create/edit/delete[...] | Trigger related writes after a successful action. |
Action Process and After Hooks
Use a shared process block for normalization that should run for every create/edit write. Use action-specific process blocks when one action needs extra data.
post articles {
process {
$post.title = str.trim($post.title);
}
create process {
$post.slug = str.slug($post.title, str.id(8));
$post.status = $post.status || "draft";
}
after edit {
$context.auditId = str.id(18);
}
}
after blocks run only after the matching require[...] passes, so they are useful for audit ids, post-action summaries, or context needed by triggers.
List Policies
post articles {
list {
require[ $access.read || $user.id ]
defaultLimit: 20;
maxLimit: 100;
}
}
| Property | Purpose |
|---|---|
require[...] | Controls whether the current user can list this post type. |
defaultLimit | Default list size when the frontend does not provide one. |
maxLimit | Largest list size this post type allows. |
Export Blocks
Use export when a theme needs a downloadable file generated from filtered post rows.
post orders {
export csv {
rowsPerPage: 500;
contentType: "text/csv; charset=utf-8";
extension: "csv";
filename: "orders.csv";
require[ $access.read ]
header {
return "Order ID,Email,Amount";
}
row {
return `${$rowMeta.id},${$row.email},${$row.amount}`;
}
}
}
Render it from the frontend with Frontbacked.exportPosts({ type: "orders", name: "csv" }).
Export blocks can use $row, $rowMeta, $query, $user, $secret, $private, $page, $currency, and $context.
Endpoint Blocks
endpoint.post createLead("/leads/submit") {
verify $request.body.email
idempotency `${str.lower(str.trim($request.body.email))}:${$request.body.campaign || "default"}`
process {
$context.email = str.lower(str.trim($request.body.email));
return {
ok: true,
email: $context.email
};
}
}
| Section | Purpose |
|---|---|
verify expression | Optional guard evaluated before the endpoint process runs. |
idempotency expression | Optional stable key for repeat-safe requests. |
process { ... } | Returns the endpoint response and can call endpoint-safe helpers. |
Endpoint paths are called under /fb-endpoints, so the example above is called at /fb-endpoints/leads/submit. Use Endpoints and Idempotency for the full guide.
Schema Types
name: { type: "string", min: 1, max: 120, required: true }
age: { type: "number", min: 18 }
active: { type: "boolean" }
startsAt: { type: "datetime" }
createdAt: { type: "servertime" }
profile: {
type: "object",
fields: {
bio: { type: "string", max: 400 }
}
}
tags: { type: "array", of: { type: "string", max: 40 }, max: 10, unique: true }
image: { type: "file", maxSize: 7000000, mimeTypes: ["image/jpeg", "image/png"] }
video: { type: "file", maxSize: 500000000, maxDurationSeconds: 300, mimeTypes: ["video/mp4"] }
answer: { anyOf: [{ type: "string", max: 100 }, { type: "number", min: 1, max: 10 }] }
Schema Rules
| Rule | Works with | Example |
|---|---|---|
required | all field types | email: { type: "string", required: true } |
min | string, number, array | title: { type: "string", min: 1 } |
max | string, number, array | tags: { type: "array", max: 10 } |
pattern | string | slug: { type: "string", pattern: "^[a-z0-9-]+$" } |
enum | string | status: { type: "string", enum: ["draft", "published"] } |
minKeys | object | meta: { type: "object", minKeys: 1 } |
maxKeys | object | meta: { type: "object", maxKeys: 20 } |
maxDepth | object | meta: { type: "object", maxDepth: 2 } |
maxChars | object | meta: { type: "object", maxChars: 2000 } |
unique | array | tags: { type: "array", unique: true } |
maxSize | file | image: { type: "file", maxSize: 3000000 } |
mimeTypes | file | file: { type: "file", mimeTypes: ["application/pdf"] } |
maxDurationSeconds | video file | video: { type: "file", maxDurationSeconds: 300 } |
immutable | edit validation | id: { type: "string", required: true, immutable: true } |
immutableUnless | edit validation | status: { type: "string", immutableUnless: $access.edit } |
Runtime Variables
| Variable | Available in |
|---|---|
$id | Current generated post id, or current signed-in user id inside auth rules. |
$post | process, schema immutable guards, permissions, triggers, resolved handlers |
$prev | edit/read/delete flows, immutable guards, permissions, triggers |
$user | permissions, process, triggers |
$visitor | endpoint and rule context for guest/user/admin detection |
$admin | admin context when the current user is acting as a site admin |
$access | permissions and guarded workflow fields |
$wallet | signed-in user's wallet balances |
$request | endpoint requests, exports, and request-aware rule helpers |
$query | query/filter data for lists, exports, and endpoints |
$secret | secret values 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 settings |
$currency | platform currency config; $currency.value is the selected code and $currency.mode is currently fixed |
$context | scratch data created during the current rule run |
$resolved | resolved webhook handlers |
Admin-Supplied Variables
If a theme references $secret.PAYSTACK_SECRET or $private.minimumDepositAmount, fb-admin infers those names from backend/index.rules and shows matching fields on the Secret Keys and Private Settings pages. Site admins fill the values there; theme developers do not need to create separate admin UI for them.
Operators
| Operator | Example |
|---|---|
| ` | |
&& | $post.email && $post.acceptedTerms |
==, != | $post.status == "published" |
>, <, >=, <= | $post.price > 0 |
includes | $post.role includes ["admin", "editor"] |
+, -, *, /, % | $post.quantity * $post.price |
!, not | !$post.archived |
Built-In Functions
| Function | Purpose |
|---|---|
str.lower(value) | Lowercase a string. |
str.upper(value) | Uppercase a string. |
str.trim(value) | Trim a string. |
str.includes(value, textOrArray) | Check whether a string contains one or more values. |
str.replace(value, search, replacement) | Replace all matching text. |
str.join(values, separator) | Join array values as strings. |
str.slug(...values) | Create a URL-friendly slug. |
str.id(length) | Generate a random id. |
arr.size(array) | Return array length. |
arr.map(array, fn) | Map array values. |
arr.join(array, separator) | Join array values. |
math.min(...values) | Return minimum. |
math.max(...values) | Return maximum. |
time.now() | Return the current timestamp. |
time.add(value, { days, hours, minutes }) | Add time to a date value and return an ISO timestamp. |
http.post(url, options) | Perform an HTTP POST request. |
console.log(...values) | Log from a rule runtime. |
Rule Resource Helpers
These helpers are available inside FRL expressions and process blocks when the current runtime supports them.
| Helper | Purpose |
|---|---|
posts.findOne(args) | Read one visible post that matches the given filters. |
posts.findAll(args) | Read visible posts that match the given filters. |
posts.count(args) | Count visible posts that match the given filters. |
trigger.post.create(type, data) | Queue a related post create after the current action succeeds. Frontbacked generates the id and author. |
trigger.post.edit(id, data) | Queue a related post edit by generated post id. |
trigger.post.delete(id) | Queue a related post delete by generated post id. |
chargeUser(args) | Request wallet confirmation before continuing a paid rule action. |
Price Fields
Use type: "price" when a post schema is allowed to create server-enforced price metadata:
post products {
schema {
title: { type: "string", min: 1, max: 140, required: true }
price: { type: "price", required: true }
}
create require[ $user.id ]
read require[ true ]
edit require[ $user.id == $prev.authorId ]
delete require[ $user.id == $prev.authorId ]
}
The frontend submits price metadata through special keys. These keys are not stored as normal post data; Frontbacked stores them as protected price metadata for the price field:
await Frontbacked.uploadPost({
type: "products",
post: {
title: "Starter Plan",
"$minAmount.price": 500,
"$maxAmount.price": 10000
}
})
Use "$fixedAmount.price" for fixed one-time prices, or "$minAmount.price" with optional "$maxAmount.price" for variable amounts. Fixed prices cannot be mixed with min/max metadata.
Secure Rule Checklist
Before publishing a theme, check every post type:
- Every stored field is in
schema. - Owner, site, ids, and external reference fields are
immutable. - Workflow fields use
immutableUnlesswhen only editors or trusted flows may change them. - Every action has a
require[...]block. - Public create rules still require meaningful submitted fields.
- Edit/delete rules use
$prevfor ownership checks. - File fields declare
maxSize,mimeTypes, and videomaxDurationSecondswhere needed. - Trigger targets have their own post rules.
- Custom endpoints use
verifyandidempotencywhen repeated requests could duplicate work. - List and export policies have explicit
require[...]blocks when the data is not public.