FRL Overview
Frontbacked Rule Language (FRL) is the rule language for a theme. It answers four questions for every post type:
- What fields are allowed?
- How should incoming data be cleaned or computed?
- Who can create, read, edit, or delete this data?
- Should this action trigger other writes or async work?
FRL is stored in the theme's single rules file: backend/index.rules.
What FRL Unlocks
FRL is more than a validation file. It is the place where a theme describes its data powers:
| Feature | What it lets your theme do |
|---|---|
post blocks | Define records such as articles, products, comments, plans, tickets, reviews, portfolios, or lessons. |
auth block | Shape and validate custom signed-in user data. |
schema | Reject unknown fields and validate nested objects, arrays, files, prices, and immutable fields. |
process | Trim, normalize, compute slugs, set derived status, and prepare trusted fields before validation. |
Action require[...] | Decide who can create, read, edit, and delete each post type. |
| Action process hooks | Run extra create/edit/delete/read-specific normalization. |
after hooks | Compute rule state after permission checks and before follow-up writes. |
| Triggers | Create, edit, or delete related posts after a successful action. |
list policies | Control who can list a post type and set default/max list limits. |
export blocks | Turn filtered rows into CSV, text, or other downloadable formats. |
| Async resolution | Model delayed workflows that later resolve through callback-style events. |
endpoint.* blocks | Add custom guarded JSON endpoints for workflows that are not plain CRUD. |
idempotency | Make repeat endpoint requests return the same outcome instead of duplicating work. |
$secret and $private | Reference admin-filled private values without building custom settings forms. |
A Minimal Rule File
post leads {
process {
$post.email = str.lower(str.trim($post.email));
}
schema {
name: { type: "string", min: 1, max: 120, required: true }
email: { type: "string", min: 3, max: 180, required: true }
message: { type: "string", max: 1000 }
createdAt: { type: "servertime" }
}
create require[
$post.email
]
read require[
$access.read
]
edit require[
$access.edit
]
delete require[
$access.delete
]
}
This defines a leads post type. When a frontend page calls Frontbacked.uploadPost({ type: "leads", post }), Frontbacked finds post leads, runs the rule, and either stores the post or returns an error.
Execution Order
For create, edit, and delete actions, Frontbacked evaluates a post rule in this order:
- Build runtime context such as
$post,$prev,$user,$access,$secret,$private,$page, and$currency. - Run the shared
processblock if the post type has one. - Run the action-specific process block, such as
create process { ... }, when present. - Validate the
schema. - Evaluate the action
require[...]block. - Run the matching
after create/edit/delete/readblock when present. - Materialize any trigger writes for that action.
For async webhook resolution, Frontbacked first checks matching on resolved handlers. If one matches, the handler can update $post, then schema validation runs again.
Post Blocks
Every data record you want a theme to write or read is declared as a post.
post products {
schema {
title: { type: "string", min: 1, max: 140, required: true }
price: { type: "price", required: true }
}
create require[ $access.edit ]
read require[ $access.read ]
edit require[ $access.edit ]
delete require[ $access.delete ]
}
The post name is the type used by the frontend API:
await Frontbacked.uploadPost({
type: "products",
post: {
title: "Starter Plan",
"$fixedAmount.price": 5000
}
});
type: "price" tells Frontbacked that the matching special keys, such as "$fixedAmount.price" or "$minAmount.price", are allowed. Frontbacked stores those values as protected price metadata and FQL can display them through $.$fixedAmount.price or $.$minAmount.price.
Auth Data Block
Use auth when you want to control what a signed-in user can write into their own data JSON.
It uses the same process, schema, and require[...] shape as a post block, but it has no name because it always protects the current authenticated user.
auth {
schema {
phone: { type: "string", max: 40 }
country: { type: "string", max: 80 }
avatarUrl: { type: "string", max: 500 }
}
edit require[
$user.id
]
}
The frontend updates this data with Frontbacked.updateUserData({ data, merge }). With merge: true, the submitted object is merged into the existing data; with merge: false, it replaces the existing data after the auth rule passes.
Custom Endpoints
Use endpoint.get, endpoint.post, endpoint.put, or endpoint.delete when a theme needs a guarded JSON route that is not just storing one post.
endpoint.post estimateReturn("/investments/estimate") {
verify $request.body.amount > 0
idempotency `${$user.id || "guest"}:${$request.body.amount}:${$request.body.plan}`
process {
$context.rate = math.max($private.dailyRate || 0, 0);
return {
ok: true,
dailyEstimate: $request.body.amount * $context.rate
};
}
}
The theme calls it at /fb-endpoints/investments/estimate. See Endpoints and Idempotency for request fields, repeat-request behavior, path params, and endpoint write helpers.
Runtime Context
FRL expressions use runtime variables. The most common are:
| Variable | Meaning |
|---|---|
$id | The generated id for the current post. In auth rules, this is the signed-in user's id. |
$post | The current post being created, edited, read, deleted, or resolved. |
$prev | The previous stored value. Most useful during edit, read, and delete. |
$user | The signed-in user, when available. |
$access | Access flags resolved by the platform, such as read, edit, and delete. |
$secret | Secret values referenced by the theme, such as $secret.PAYSTACK_SECRET. fb-admin creates fields for referenced secret keys automatically. |
$private | Private server-side settings referenced by the theme, such as $private.minimumDepositAmount. fb-admin creates fields for referenced private settings automatically. |
$page | Page settings from the site settings. These are edited through theme/page settings rather than the FRL admin forms. |
$currency | Platform currency config. $currency.value is a code such as USD or NGN; $currency.mode is currently fixed. |
$context | Scratch data you create in process and reuse later in the same rule run. |
$resolved | Webhook payload data during on resolved handlers. |
Admin Fields from $secret and $private
When a theme references $secret.someKey or $private.someKey in backend/index.rules, fb-admin automatically shows those keys on the Secret Keys or Private Settings pages for the site admin to fill.
create require[
$secret.PAYSTACK_SECRET && $private.minimumDepositAmount && $user.id
]
Theme developers only need to reference the keys in FRL. They do not need to create custom admin form fields for those values.
Expressions
FRL expressions support paths, strings, numbers, arrays, objects, function calls, comparisons, logical operators, math operators, unary operators, and template strings.
$post.slug = str.slug($post.title);
$post.total = $post.price * $post.quantity;
$post.ownerCanEdit = $user.id == $prev.authorId || $access.edit;
$context.message = `New order ${$post.id} for ${$post.email}`;
Use semicolons inside process, function bodies, and resolved handlers.
Helper Functions
Built-in helpers include:
| Function | Example | Purpose |
|---|---|---|
str.lower(value) | str.lower($post.email) | Lowercase a string. |
str.upper(value) | str.upper($post.code) | Uppercase a string. |
str.trim(value) | str.trim($post.name) | Trim whitespace. |
str.includes(value, textOrArray) | str.includes($post.title, "pro") | Check string contents. |
str.slug(...values) | str.slug($post.title, $post.id) | Build a URL-friendly slug. |
str.id(length) | str.id(12) | Generate a random id. |
arr.size(array) | arr.size($post.tags) | Count array items. |
math.min(...) | math.min($post.price, 100) | Return the minimum value. |
math.max(...) | math.max($post.price, 0) | Return the maximum value. |
Theme rules can also declare local functions with fn.
fn normalizeEmail(email) {
return str.lower(str.trim(email));
}
post signups {
process {
$post.email = normalizeEmail($post.email);
}
}
Secure Defaults
If an action has no require[...] block, it is denied. This is intentional. A theme should explicitly say who can create, read, edit, and delete each post type.
For public form submissions, use a condition based on the submitted data instead of leaving the rule open by accident.
create require[
$post.email && $post.acceptedTerms
]
For site-owner actions, prefer platform access flags.
edit require[
$access.edit
]