Frontbacked Docs

FQL Reference

This page is a compact reference for Frontbacked Query Language.

AI Context

When using an AI coding tool, give it /llms.txt before asking it to generate or refactor a Frontbacked theme. The file lists the public FQL/FRL syntax, helper names, and naming rules the AI should stay inside.

State Comment

<!-- {STATE}
  state = {
    "key(defaultValue)": "sourceExpression",
    "derived": "$state.key || 'fallback'"
  }
-->

State Key Defaults

SyntaxResult
"accepted(true)"$state.accepted starts as true.
"count(0)"$state.count starts as 0.
"name('Guest')"$state.name starts as "Guest".
"coupon(null)"$state.coupon starts as null.

Live State Keys

Append .live to a state key to keep its saved-data value current. A default comes before the modifier.

{
  "messages.live": {
    "$list": "messages",
    "$where": { "roomId": "$params.roomId" },
    "$order": { "createdOn": "asc" },
    "$limit": 50
  },
  "product('No product').live": "$posts.products.{id: $params.productId}",
  "productName('No product').live": "$state.product.data.name"
}

The public paths are $state.messages, $state.product, and $state.productName. .live is declaration metadata, not part of the key. $live selector options, modifiers before defaults, repeated modifiers, and unknown modifiers are invalid.

All live keys on a page share one managed connection. Frontbacked combines selections that depend on the same saved data, performs one fresh request when they intersect, and applies the current visitor authentication and FRL permissions to that request. $now does not tick by itself; .live reacts to saved data changes.

Read live status values from $status.<statePath> or Frontbacked.live.status(path):

StatusMeaning
loadingThe live selection is loading or applying a fresh result.
liveThe page is connected for saved-data changes.
reconnectingFrontbacked is retrying after an interruption.
errorThe latest live error, or null.
changedOnISO timestamp of the latest lifecycle update.

Frontbacked.live.isConnected() returns the page-level connection state. Frontbacked keeps the last rendered data during an interruption and reloads it only when a saved-data change was missed. The page also dispatches frontbacked:live-update after fresh live data has been rendered.

Event Source Syntax

selector.onevent.target.path.functionName()

Examples:

SourceValue
#name.oninput.target.valueInput value.
#terms.onchange.target.checkedCheckbox checked state.
#avatar.onchange.target.files[0]First selected file.
#form.onsubmit.handleSubmit()Return value of handleSubmit(event).

Multiple Sources

{
  "terms(true)": "#terms.onchange.target.checked, #termsButton.onclick.target.toggleTerms()"
}

Use commas for multiple event sources for the same state key.

Fallbacks

{
  "displayName": "$state.form.name || $query.name || 'Guest'",
  "canSubmit": "$state.form.email && $state.form.terms"
}

Use || for truthy fallbacks and ?? when only null or undefined should fall back. When a fallback mixes ?? with || or &&, add parentheses so the expression says exactly which branch should resolve first.

Use && for truthy gating. It evaluates left to right, returns the first falsy value, and returns the last value when all values are truthy. && has higher precedence than ||.

Expression Operators

GroupOperators
Fallback and logic??, `
Equality==, !=
Comparison>, >=, <, <=
Arithmetic+, -, *, /, %
Unary!, unary +, unary -
Conditionalcondition ? whenTrue : whenFalse

Parentheses control grouping. Arithmetic follows normal precedence. FQL equality is type-strict, so 1 == '1' is false.

Data Roots

RootExample
$state$state.form.email
$query$query.plan
$params$params.slug
$siteInfo$siteInfo.name
$auth$auth.email
$currency$currency.value
$wallet$wallet.default
$editMode$editMode.active
$settings$settings.site.name
bare post typeproducts{id:$query.id}
$posts or $post$posts.products{id:$query.id}
$transactions$transactions.{status:'successful'}.$counts
$$.title inside an f-list template

$siteInfo can include public contact fields selected by the site admin: email, phone, businessAddress, whatsappContact, telegramContact, and socialProfiles. Each social profile uses a stable platform key and includes a generated url only when Frontbacked can safely build one, so themes should render a social link only when $.url exists.

Single Post Lookup

{
  "product": "products{id:$query.productId}",
  "sameProduct": "$posts.products{id:$query.productId}",
  "title": "$state.product.data.title || 'Product'",
  "id": "$state.product.id"
}

The object inside braces is the lookup condition. A single post lookup can start with the bare post type, such as products{...}, or with $posts/$post, such as $posts.products{...}.

Single post lookups resolve to visible post data:

{
  id,
  type,
  authorId,
  createdOn,
  updatedOn,
  data: {
    ...publicPostFields,
    ...authorOnlyPostFieldsWhenCurrentUserIsAuthor
  }
}

id, type, createdOn, and updatedOn are generated by Frontbacked for posts submitted with Frontbacked.createPost() and preserved for posts changed with Frontbacked.updatePost(). Do not submit these fields yourself; read them from single post lookups and list items.

Theme-owned lookup fields are under data; system fields remain at the object root:

{
  "byName": "$posts.profile_updates{name:'Elijah'}.data.name",
  "byAuthor": "$posts.profile_updates{authorId:$auth.id}.data.name",
  "created": "$posts.profile_updates{name:'Elijah'}.createdOn"
}

Auth Data

Use $auth for the current user:

{
  "isSignedIn": "$auth.exists",
  "userId": "$auth.id",
  "displayName": "$auth.name || 'Guest'",
  "email": "$auth.email",
  "customPhone": "$auth.data.phone"
}

$auth contains the system-managed account envelope created by Frontbacked.signUp() and refreshed by Frontbacked.signIn(). System fields such as id, createdOn, and updatedOn are at the root. Custom user data saved by Frontbacked.updateUserData() is under $auth.data, such as $auth.data.phone and $auth.data.country.

Currency Data

Use $currency.value to read the site owner's selected platform currency. If no currency has been selected yet, it resolves to USD.

<span f="true" f-text="$currency.value">USD</span>

Wallet Data

Use $wallet for the signed-in user's wallet balances. $wallet.default reads the balance for the current platform currency, so a site using NGN can render the user's NGN balance without hardcoding the currency key.

<strong f="true" f-text="$wallet.default.formatMoney()">USD 0</strong>
<span f="true" f-text="$wallet.USD.formatMoney()">USD 0</span>

If the user has no stored balance yet, missing wallet values resolve to 0.

Transaction Data

Use $transactions to render the current user's site transactions:

<!-- {STATE}
  state = {
    "transactions": {
      "$list": "$transactions",
      "$order": { "createdOn": "desc" },
      "$limit": 10
    }
  }
-->
<tbody id="transactions" f="true" f-list="$state.transactions">
  <tr>
    <td f="true" f-text="$.tag">investment</td>
    <td f="true" f-text="$.requestedAmount">500</td>
    <td f="true" f-text="$.requestedCurrency">USD</td>
    <td f="true" f-text="$.status">successful</td>
  </tr>
</tbody>

Use requestedAmount and requestedCurrency for the amount the site asked the user to pay. Use paidAmount and paidCurrency for the actual method amount after conversion, including crypto payments.

Transaction list selectors use special state keys such as $list, $where, $order, $limit, and $page.

List Query

{
  "products": {
	    "$list": "products",
	    "$where": { "status": "published" },
	    "$order": { "createdOn": "desc" },
	    "$limit": 6
	  }
	}

Use that selector with f-list="$state.products". The Frontbacked response for the list is also available through $lists.<listId> and includes paging metadata plus items. Each item has the same visible post data shape as a single post lookup, including id, type, createdOn, and updatedOn.

Those root fields are the system-generated post values added by Frontbacked when posts are read as list items.

Inside an f-list template, $ is the current item:

<article f-key="$.id">
  <span f="true" f-text="$.data.name"></span>
  <time f="true" f-text="$.createdOn"></time>
</article>

Saved-data and local-data lists use keyed rendering. Each item needs id; otherwise add f-key="expression" to the repeated first-child template. Keys must be present and unique. Frontbacked then preserves matching DOM nodes when live data changes or a list is reordered.

Selector Operators

Use $where objects for list and aggregate filters. Plain keys inside $where are equality checks. Add a comparison suffix for non-equality checks:

{
  "pendingTransactions": {
    "$list": "$transactions",
    "$where": { "status": "pending", "expiresOn.$gt": "$now" },
    "$order": { "expiresOn": "asc", "createdOn": "desc" },
    "$limit": 20
  }
}

Supported suffixes are $gt, $gte, $lt, $lte, $ne, $contains, $startsWith, and $endsWith. Use $and or $or with arrays for grouped logic:

$transactions.{$where: {$or: [{status: 'pending'}, {status: 'pending_review'}]}}.$counts

In f-text and other raw FQL attributes, $now can be written with or without quotes when it is the whole value:

$transactions.{$where: {status: 'pending', 'expiresOn.$gt': $now}}.$counts
$transactions.{$where: {status: 'pending', 'expiresOn.$gt': '$now'}}.$counts

Both forms resolve $now to the page's current-time snapshot. In {STATE} selectors, $now may be written bare in JS-style state declarations or as "$now" in JSON-compatible state. For transaction filters, use expiresOn, not expires.

Text Search

Use $search alongside $where in a list selector when the visitor should search across one or more text fields. $where restricts which records qualify; $search finds matches within those records. Nesting $search inside $where is not supported. $search can be a string or an object with $term and $fields:

{
  "products": {
    "$list": "products",
    "$where": {
      "visibility": "published",
      "price@money.$gte": { currency: "USD", amount: "100" },
      "price@money.$lte": { currency: "USD", amount: "400" }
    },
    "$search": {
      "$term": "$state.filters.search",
      "$fields": ["name", "sku", "texture"]
    },
    "$order": { "price@money": "asc", "createdOn": "desc" },
    "$limit": 24
  }
}

Frontbacked splits the search term into words and requires each word to appear in at least one searched field. Prefer marking fields with searchable: true in your FRL schema, then use $search: "$state.filters.search" without $fields. Frontbacked keeps that search content up to date automatically. An explicit $fields list overrides the schema selection while respecting field visibility. If a schema has no searchable declarations, omitting $fields keeps the existing search across readable post data.

Aggregates

Aggregates are exposed as special path segments.

SyntaxResult
$transactions.{$where: {status:'successful'}}.$countCount matching current-user transactions.
$transactions.{$where: {status:'successful'}}.$sum.requestedAmountSum requestedAmount on matching current-user transactions.
$posts.investments.{$where: {status:'active'}}.$countCount matching posts.
$posts.investments.{$where: {status:'active'}}.$sum.amountSum the amount field on matching posts.

For sums, the field is written after $sum:

$transactions.{$where: {status: 'successful'}}.$sum.requestedAmount

Function calls are written as path segments. The resolved value before .functionName() is passed to the browser function:

$transactions.{$where: {status: 'successful'}}.$sum.requestedAmount.formatMoney()
$transactions.{$where: {status: 'pending'}}.$count.formatCount()

Frontbacked returns the aggregate first, then passes that value to the registered function. Function arguments are supported, but the aggregate field still belongs after $sum: $sum.requestedAmount.formatMoney('USD'), not $sum(requestedAmount).

Older themes may still use $counts, but new FQL should use $count.

When summing paidAmount, filter by paidCurrency or another single-method condition to avoid summing mixed units:

$transactions.{$where: {status: 'successful', paidCurrency: 'BTC'}}.$sum.paidAmount

Transaction rows expose the stored status. Completed payments currently appear as successful in FQL transaction filters and rows. FRL payment events expose the same completion event as payment confirmed with $payment.status == "confirmed" and $payment.rawStatus available for compatibility.

DOM Attributes

AttributePurpose
f="true"Marks an element for Frontbacked rendering.
f-text="expression"Sets textContent.
f-attr-name="expression"Sets/removes the named attribute.
f-show="expression"Shows the element only when the expression resolves truthy. The element is hidden by default while needed data is unavailable.
f-hide="expression"Hides the element when the expression resolves truthy. The element is shown by default while needed data is unavailable.
f-list="$state.selectorName"Repeats the first child using a state selector object.
f-list="$state.settingsList"Repeats a settings selector such as { $list: "$settings.path.to.items" }; authored children remain default/showcase items.
f-key="expression"Declares a stable key on a non-settings list's repeated first child when items do not use id.
f-insert-pagination="$state.selectorName"Inserts generated pagination buttons into the host element for the matching list selector.
f-insert-pagination="#listId"Inserts generated pagination buttons for the list element with that id.
f-replace-pagination="#listId"Replaces the host element with generated pagination buttons for the list element with that id.
`f-access="guestauth
`f-page-access="guestauth
f-redirect-to="/path"Redirect target used with f-access.
f-redirect="/path"Alias for f-redirect-to.
`f-auth-page="signinsignup"`
`f-auth-link="signinsignup"`
f-auth-provider="google"Starts a site provider sign-in flow when clicked. Use provider ids available for the site, such as google or github. Frontbacked keeps the element hidden until the provider is supported and enabled.
f-google-authBackwards-compatible alias for f-auth-provider="google".
f-auth-return-to="/path"Local path to open after provider sign-in completes.
f-google-auth-return-to="/path"Backwards-compatible alias for f-auth-return-to.
`f-auth-mode="signinsignup"`
f-auth-labelLets Frontbacked fill the button label from the provider name when the element has no text.
f-loaderMarks an element that should be shown while Frontbacked is loading another page through SPA navigation. It is hidden automatically when idle.
f-nav="reload"Opts out of SPA navigation. Put it on one link, an ancestor, or <body> to make matching links use normal browser reloads.
f-min-load-time="1000"Keeps SPA navigation in the loading state for at least the given milliseconds. Put it on a link, an ancestor, or <body> to preview/test loader UI.
f-insert="/components/header.html"Imports a same-theme HTML fragment and inserts it inside the element before the page is served.
f-replace="/components/footer.html"Imports a same-theme HTML fragment and replaces the host element before the page is served.
f-skeleton-count="3"Sets how many placeholder rows a list renders while loading. Values are capped at 12.
f-no-skeletonDisables Frontbacked skeleton placeholders for that element or list.
f-video="expression"Attaches video playback to a video element.
f-video-player="standard"Enables the Frontbacked player UI.
`f-video-controls="truefalse"`
`f-video-quality-selector="truefalse"`
`f-video-timeline-thumbnails="truefalse"`
`f-video-layout="landscapeportrait"`
`f-video-autoplay="truefalse"`
`f-video-muted="truefalse"`
`f-video-poster="autoURL"`
`f-video-hide-controls="truefalse"`
f-video-hidden-controls="time mute"Hides selected Frontbacked player controls.
f-video-max-quality="720"Caps automatic quality selection.
`f-video-data-saver="respectforce"`
`f-video-hls-loader="URLfalse
f-support-launcherRenders the reusable support launcher.
f-support-key="site-help"Gives a support launcher or support chat a stable key.
f-support-id="site-help"Alias for f-support-key.
f-support-launcher-open="site-help"Opens the launcher from a custom button.
f-support-launcher-close="site-help"Closes the launcher from a custom button.
f-support-launcher-toggle="site-help"Toggles the launcher from a custom button.
f-support-toggle="site-help"Toggles the native site chat directly.
f-admin-linkFills an admin link for signed-in site admins.
f-edit-mode-enableEnables page edit mode from a button.
f-edit-mode-disableDisables page edit mode from a button.
f-edit-mode-toggleToggles page edit mode from a button.

Visibility directives are intentionally asymmetric during initial loading. Use f-hide for content that is safe to show first and should disappear only when the expression becomes truthy. Use f-show for content that should stay hidden until Frontbacked has enough data to prove it should be visible, such as dashboard links that depend on $auth.exists.

<a href="/signin" f="true" f-hide="$auth.exists">Sign in</a>
<a href="/dashboard" f="true" f-show="$auth.exists">Dashboard</a>

When Frontbacked hides an element through f-show or f-hide, it sets hidden, aria-hidden="true", and a managed inline style with display: none !important and opacity: 0 !important. When the element becomes visible again, Frontbacked restores the inline display and opacity values that were present before it hid the element.

Use f-auth-provider on provider buttons:

<button type="button" f-auth-provider="google" f-auth-return-to="/dashboard">
  Continue with Google
</button>
<button type="button" f-auth-provider="github" f-auth-return-to="/dashboard">
  Continue with GitHub
</button>

Frontbacked reveals the element only when that provider is available for the site. If the provider is turned off, the element stays hidden. If the theme supplies an unsupported provider id, Frontbacked renders a visible setup error so theme developers can fix the value early.

In local development, provider sign-in uses simulated accounts. You can edit those local accounts after creation, including an admin toggle for trying admin-only theme pages and fb-admin access.

Settings-backed lists can include more than one authored child. The first child is still the template used for saved data. The remaining children are default/showcase items for fresh sites:

<!-- {STATE}
  state = {
    "services": { "$list": "$settings.home.services.items" }
  }
-->
<section id="services" f="true" f-list="$state.services">
  <article>
    <span class="service-icon">...</span>
    <h3 f="true" f-text="$.title">Default service</h3>
    <p f="true" f-text="$.body">Default copy.</p>
  </article>
  <article>
    <span class="service-icon">...</span>
    <h3>Second default service</h3>
    <p>Second default copy.</p>
  </article>
</section>

If the site owner has no saved array for that setting yet, Frontbacked renders all authored children. Once Frontbacked has a saved array, even an empty array, the saved array controls the public render. If the admin removes every item, the public page renders no list items.

In settings edit mode, when the saved array is shorter than the authored defaults, Frontbacked renders the missing authored defaults as grey restore suggestions, not as live content. Each suggestion has an add-back button that writes a local draft. When the saved array is empty, Frontbacked also shows an Edit List button so the admin can add new items or restore the theme defaults from the list editor.

In settings edit mode, the list container itself is not edited as raw JSON. Frontbacked adds edit controls to the fields declared in the first template, such as $.title, $.body, f-attr-src="$.image", and f-attr-href="$.url", so theme developers can keep icons, badges, and layout details theme-owned while making only the intended text, image, and link fields editable.

Use data-frontbacked-settings-max-items="3" on a settings-backed list when the design has a fixed slot count, such as a three-slide hero carousel. Frontbacked caps public rendering and disables Add item in the list editor after the saved list reaches that count.

Settings edit submissions are local drafts first. The editor writes draft values to localStorage; the fixed edit bar shows a Save button with the number of pending edits, plus undo and redo controls. Frontbacked is updated only when the admin clicks the edit bar Save button.

Client Methods

Frontbacked.init({ endpoint, debug, state })
Frontbacked.navigate(url, options?)
Frontbacked.admin.url(path?)
Frontbacked.admin.open(path?, options?)
Frontbacked.editMode.status()
Frontbacked.editMode.isActive()
Frontbacked.editMode.enable(options?)
Frontbacked.editMode.disable()
Frontbacked.editMode.toggle(options?)
Frontbacked.startEditMode(redirectTo?)
Frontbacked.exitEditMode()
Frontbacked.getCurrency()
Frontbacked.setCurrency(value)
Frontbacked.schemas.all()
Frontbacked.schemas.post(type, options?)
Frontbacked.schemas.profile(options?)
Frontbacked.schemas.extend(schema, options?)

Frontbacked.state.get(path?)
Frontbacked.state.set(path, value)
Frontbacked.state.set(values)
Frontbacked.state.reset(path)
Frontbacked.state.check(path, schema, check?)
Frontbacked.lists.goTo(listId, page, cursor?)
Frontbacked.lists.next(listId)
Frontbacked.lists.prev(listId)
Frontbacked.live.status(path?)
Frontbacked.live.isConnected()
Frontbacked.functions.define(name, fn)
Frontbacked.functions.define(functions)
Frontbacked.functions.remove(name)
Frontbacked.functions.has(name)
Frontbacked.storage.get(key, fallback?)
Frontbacked.storage.set(key, value, options?)
Frontbacked.storage.remove(key)
Frontbacked.storage.all(prefix?)
Frontbacked.storage.clear(prefix?)
Frontbacked.storage.syncToAuth(options?)
Frontbacked.storage.syncRegisteredToAuth()
Frontbacked.confirm(message, title?)
Frontbacked.supportLauncher.init(root?)
Frontbacked.supportLauncher.open(key?)
Frontbacked.supportLauncher.close(key?)
Frontbacked.supportLauncher.toggle(key?)
Frontbacked.supportLauncher.contacts()
Frontbacked.supportLauncher.whatsappUrl(contact?, message?)
Frontbacked.supportLauncher.telegramUrl(contact?, message?)
Frontbacked.supportLauncher.telegramSupportsDraft(contact?)
Frontbacked.openSupportChat(key?, callback?)
Frontbacked.closeSupportChat(key?, callback?)
Frontbacked.toggleSupportChat(key?, callback?)
Frontbacked.support.open(key?)
Frontbacked.support.close(key?)
Frontbacked.support.toggle(key?)
Frontbacked.support.contacts()

Frontbacked.getSkin()
Frontbacked.listSkins()
Frontbacked.setSiteSkin(skinId, options?)
Frontbacked.setSkinMode(mode)
Frontbacked.toggleSkinMode()
Frontbacked.skins.current()
Frontbacked.skins.list()
Frontbacked.skins.setSiteSkin(skinId, options?)
Frontbacked.skins.setMode(mode)
Frontbacked.skins.toggleMode()

Frontbacked.image.resize(source, options)
Frontbacked.image.url(source, options)
Frontbacked.image.srcset(source, entries)

Frontbacked.video.attach(videoElement, source, options?)
Frontbacked.video.source(source)
Frontbacked.video.setSimulation(profile)
Frontbacked.video.clearSimulation()
Frontbacked.video.playSimulation(sequence, element?)
Frontbacked.video.stopSimulationSequence()
Frontbacked.video.environment(videoElement)
player.play()
player.pause()
player.toggle()
player.seekTo(seconds)
player.seekBy(seconds)
player.currentTime()
player.duration()
player.remaining()
player.mute()
player.unmute()
player.setMuted(value)
player.setVolume(value)
player.setQuality(value)
player.setLayout(value)
player.appendVideo(source)
player.next()
player.prev()
player.list()
player.destroy()

Frontbacked.uploads.status(sessionId?)
Frontbacked.uploads.pending(post?)
Frontbacked.uploads.resume(options)
Frontbacked.uploads.resumePost(input)
Frontbacked.uploads.clearCompleted()
Frontbacked.uploadPost({ type, post })
Frontbacked.createPost({ type, post })
Frontbacked.updatePost({ id, type, post, merge })
Frontbacked.deletePost({ id })
Frontbacked.endpoints.call(name, { params, query, body, allowGuest, signal })

Frontbacked.signUp({ email, password, name, username, data, emailVerification })
Frontbacked.signIn({ email, password, rememberMe })
Frontbacked.signInWithProvider(provider, options?)
Frontbacked.signInWithGoogle(options?)
Frontbacked.signUpWithProvider(provider, options?)
Frontbacked.signUpWithGoogle(options?)
Frontbacked.completeProviderAuth({ provider, ticket, rememberMe })
Frontbacked.completeGoogleAuth({ ticket, rememberMe })
Frontbacked.signOut()
Frontbacked.logout()
Frontbacked.sendEmailVerification({ email, redirectTo })
Frontbacked.passwordReset({ email, path })
Frontbacked.requestPasswordReset({ email, path })
Frontbacked.confirmPasswordReset({ email, token, newPassword })
Frontbacked.updateUserData({ data, merge })
Frontbacked.updatePassword({ currentPassword, newPassword })
Frontbacked.checkUsernameAvailability(username)

Frontbacked.billing.getMethods()
Frontbacked.endpoints.call(name, { body })
Frontbacked.billing.showPayment(paymentIdOrObject)
Frontbacked.billing.checkPaymentStatus({ paymentId, methodId, requireAuth, signal })
Frontbacked.billing.createDepositAddress({ paymentId, methodId })
Frontbacked.billing.claimPayment({ paymentId, methodId, txHash, receipt, metadata })
Frontbacked.billing.paymentLink(paymentIdOrObject)

All network helpers use the endpoint configured in Frontbacked.init(). They return JSON responses, usually shaped like { ok: boolean, error?: string, message?: string }. Authenticated helpers include the stored bearer token automatically. If a response includes { ok: true, localEmail: { sent: true, mailboxUrl } }, Frontbacked shows a local development notification with a mailbox link for 20 seconds.

Call a custom FRL endpoint by its declared name. Frontbacked resolves the endpoint's method and path, so theme code does not need the site's domain or the endpoint URL:

const result = await Frontbacked.endpoints.call("createLead", {
  body: {
    email: "ada@example.com"
  }
});

Endpoint calls require a signed-in visitor by default. Set allowGuest: true for an endpoint that should also run when no one is signed in. Frontbacked still sends the visitor's authentication whenever it is available, regardless of allowGuest.

const result = await Frontbacked.endpoints.call("calculateEstimate", {
  allowGuest: true,
  params: { planId: "professional" },
  query: { months: 24 }
});

Pass an AbortSignal when a newer action should be able to cancel an older request. A canceled call returns { ok: false, aborted: true }.

let searchController;

async function searchProducts(search) {
  searchController?.abort();
  searchController = new AbortController();

  return Frontbacked.endpoints.call("searchProducts", {
    allowGuest: true,
    query: { search },
    signal: searchController.signal
  });
}

Use the FRL Endpoints and Dedupe guide to define the endpoint rule, request guard, response, and repeat-request behavior.

Initialization

Frontbacked.init(options) boots FQL for the current page. It parses state from the {STATE} comment or options.state, scans f-* bindings, fetches required server data, applies page access rules, wires SPA navigation, and renders the DOM.

OptionPurpose
endpointBase API path used for query, auth, and post requests. Defaults to /api.
debugShows extra FQL details in the browser console while you build.
stateOptional FQL state declaration object. When provided, it is used before the {STATE} HTML comment.
navigationIndicatorControls the built-in navigation progress bar. Pass false to turn it off, or { color, height, delay } to match your theme.

Use f-access and f-redirect-to on <body> to let Frontbacked gate pages without custom page code:

<body f-access="guest" f-redirect-to="/dashboard">
<body f-access="auth" f-redirect-to="/signin">
<body f-access="admin" f-redirect-to="/signin">

Frontbacked.admin.url(path) resolves the current site's admin URL from $siteInfo, falling back to /fb-admin. Frontbacked.admin.open(path, options) opens that URL, defaulting to a new tab with noopener,noreferrer.

Frontbacked.editMode.enable(options) enables page edit mode for admins with edit permission. The first enable in a browser shows a Frontbacked warning modal explaining that edit mode can change editable site text, images, links, and list content directly on the page; once the admin confirms, Frontbacked remembers that acceptance in the browser and does not show the warning again there. Frontbacked.editMode.disable() and Frontbacked.editMode.toggle() update $editMode and refresh the current render. Frontbacked.startEditMode(redirectTo = "/") and Frontbacked.exitEditMode() remain compatibility shortcuts.

Theme developers can use declarative admin controls without custom JavaScript:

<button type="button" f-edit-mode-enable f="true" f-show="$auth.admin.canEdit" f-hide="$editMode.active">
  Enable edit mode
</button>
<button type="button" f-edit-mode-disable f="true" f-show="$auth.admin.canEdit && $editMode.active">
  Exit edit mode
</button>
<a href="/fb-admin" f-admin-link target="_blank" rel="noopener noreferrer">Advanced Admin</a>

When $auth is requested, Frontbacked also exposes the current user's site-admin capability:

<a href="/admin/products" f="true" f-show="$auth.admin.canRead">Manage products</a>
<button f="true" f-show="$auth.admin.canCreate">Add product</button>
<button f="true" f-show="$auth.admin.canEdit">Save changes</button>
<button f="true" f-show="$auth.admin.canDelete">Delete</button>

$auth.admin.exists and $auth.isAdmin are true when the user is listed as a site admin. $auth.admin.permissions.read, .create, .edit, and .delete mirror the direct canRead, canCreate, canEdit, and canDelete flags.

State

Frontbacked.state.get(path?) reads the full state object when path is omitted, or one nested value when path is provided.

const state = Frontbacked.state.get();
const email = Frontbacked.state.get("form.email");

Frontbacked.state.set(path, value) writes one nested value. Frontbacked.state.set(values) merges an object into the current state.

Frontbacked.state.set("form.status", "Saved");
Frontbacked.state.set({
  form: {
    email: "ada@example.com"
  }
});

Frontbacked.state.reset(path) clears an edited state value and reapplies its declared defaults. Use it when selecting a different saved record in an editable form.

Frontbacked.state.get(path) safely returns undefined for paths that move through missing objects. It throws only when the path tries to read deeper through a defined non-object value, such as Frontbacked.state.get("ages.array[0].throws") when ages.array[0] is 24.

Array indexes can use square brackets or dot indexes. Frontbacked.state.get("sample.array[0]") and Frontbacked.state.get("sample.array.0") read the same path. Frontbacked.state.set("sample.array[0]", 1) and Frontbacked.state.set("sample.array.0", 2) write to the same path.

Schema Helpers

Frontbacked.schemas.all() returns the public schema manifest for the theme.

Frontbacked.schemas.post(type, options?) returns a client validation schema for one FRL post type. Frontbacked.schemas.profile(options?) does the same for the profile rule.

const productSchema = Frontbacked.schemas.post("products", {
  action: "create",
  omit: ["status"],
  patch: {
    image: {
      required: true,
      messages: { required: "Add a product image." }
    }
  },
  feedback: { to: "formErrors" }
});

const result = Frontbacked.state.check("productForm", productSchema);

Frontbacked.schemas.extend(schema, options?) applies the same options to a schema object you already have.

OptionPurpose
pickKeep only selected field paths.
omitRemove selected field paths.
patchMerge validation rules into existing field paths.
addAdd new field paths for browser-only checks.
action: "create"Treat fields with create-time defaults as optional in the browser.
feedbackSet a default feedback destination for Frontbacked.state.check().

Confirmation

Frontbacked.confirm(message, title?) shows Frontbacked's built-in confirmation modal and resolves to true or false:

const ok = await Frontbacked.confirm("Do you want to sign out?", "Sign out?");
if (ok) Frontbacked.signOut();

Navigation

Frontbacked.navigate(url, options?) loads a same-site page without a full browser refresh. It swaps in the next page, re-runs FQL for that page, and keeps browser history in sync.

Frontbacked.navigate("/packages");
Frontbacked.navigate("/signin", { history: "replace" });
Frontbacked.navigate("/dashboard#deposits", { scroll: "top" });

Navigation options:

OptionPurpose
history"push" by default. Use "replace" to replace the current history entry or "none" for popstate handling.
scroll"top" by default. Hashes scroll to the matching element when present. Use "preserve" to keep the current scroll position.
minLoadTimeMinimum loading time in milliseconds before the loaded page is shown. Useful when a design should keep its loader visible briefly.
fallbackReloads the browser on navigation errors by default. Set false if you want to handle failures yourself.

Frontbacked shows a small progress bar at the top of the page when navigation takes longer than a moment. It gives visitors immediate feedback while the next page is prepared and respects reduced-motion preferences.

Match the built-in indicator to your theme either during initialization or later from your theme JavaScript:

Frontbacked.init({
  navigationIndicator: { color: "#0b6e4f", height: "5px", delay: 80 }
});

// The same settings can be updated after initialization.
Frontbacked.navigationIndicator({ color: "#0b6e4f", height: "5px", delay: 80 });

Set navigationIndicator: false or call Frontbacked.navigationIndicator(false) when your design should not show an indicator.

For a fully custom loading experience, add f-loader to its root element. Frontbacked automatically shows that element during navigation and does not add its built-in progress bar:

<div class="page-loader" f-loader hidden>
  <span></span>
</div>

Opt a link or page out of SPA navigation when you need a hard reload:

<a href="/download" f-nav="reload">Download</a>
<body f-nav="reload">

To inspect a custom loader during development, add a minimum load time:

<body f-min-load-time="1000">
<a href="/packages" f-min-load-time="1500">Compare packages</a>

Frontbacked also prefetches same-site pages on link hover, focus, and touch start. It fetches the HTML with normal browser cache rules, reads the next page's stylesheets, and preloads missing CSS before the actual navigation. During navigation, the old page stays visible until new stylesheets are ready, which prevents a flash of unstyled content.

Skin Helpers

Frontbacked.getSkin() returns the active skin selection, including current mode details when the site provides skin metadata.

Frontbacked.listSkins() returns the available skin catalog for the current site.

Frontbacked.setSiteSkin(skinId, options?) updates the site-selected skin for admins with edit permission, then applies the returned skin without storing a visitor override.

Frontbacked.skins.current(), Frontbacked.skins.list(), Frontbacked.skins.setSiteSkin(...), Frontbacked.skins.setMode(...), and Frontbacked.skins.toggleMode() are namespace aliases for theme UIs that group skin controls under one object.

Frontbacked.setSkinMode(mode) sets the current skin mode preference. mode can be a concrete mode such as "dark" or a comma-separated fallback such as "system,dark". Frontbacked stores the preference in localStorage, writes a fb_skin_mode cookie, and reloads the page so the next render matches the selected mode.

Frontbacked.toggleSkinMode() cycles through available modes for the selected skin, updates storage/cookie state, and reloads the page. It returns the current skin selection when no modes are available.

Support Launcher Helpers

Frontbacked.supportLauncher.init(root?) scans a document or element for support launcher bindings. Most themes do not need to call it manually because Frontbacked.init() wires support controls during page setup.

Frontbacked.supportLauncher.open(key?), .close(key?), and .toggle(key?) control the reusable floating support launcher. When visible WhatsApp or Telegram contacts exist in $siteInfo, the launcher presents contact choices before falling back to native site chat.

Frontbacked.supportLauncher.contacts() returns the public support contact values from $siteInfo, including WhatsApp, Telegram, and site name.

Frontbacked.supportLauncher.whatsappUrl(contact?, message?) and Frontbacked.supportLauncher.telegramUrl(contact?, message?) build contact links when a theme wants to render its own support buttons.

Frontbacked.supportLauncher.telegramSupportsDraft(contact?) returns whether a Telegram contact can open with a typed draft message. Username and phone contacts support drafts; unsupported Telegram links open directly.

Frontbacked.support.open(...), .close(...), .toggle(...), and .contacts() are shorter aliases for the launcher helpers.

Image Helpers

Frontbacked.image.resize(source, options) builds a resized URL for a Frontbacked-owned image. source can be a URL string or a file metadata object returned from an upload. At least one of w/width or h/height is required.

const imageUrl = Frontbacked.image.resize(post.image, {
  w: 640,
  h: 420,
  fit: "cover",
  f: "auto",
  q: 82
});

Frontbacked.image.srcset(source, entries) builds a comma-separated srcset string from resize option entries.

Frontbacked.image.url(source, options) is an alias for resize(...), useful when a theme reads more naturally with URL-oriented naming.

External image URLs are not transformed. Use the Image Resizing guide for the full option table and examples.

Video Helpers

Use f-video for Frontbacked-owned video file fields:

<video
  f="true"
  f-video="$.video"
  f-video-player="standard"
  f-video-quality-selector="true"
  f-video-timeline-thumbnails="true"
  playsinline
  preload="metadata"
></video>

Frontbacked.video.attach(videoElement, source, options?) attaches the same adaptive playback behavior from JavaScript and returns a player instance.

const player = await Frontbacked.video.attach(video, post.video, {
  player: "standard",
  qualitySelector: true,
  timelineThumbnails: true,
  hiddenControls: ["time"],
  onPlayback(event, player) {
    console.log(event.remaining);
  }
});

player.seekBy(10);
player.setQuality("auto");
player.appendVideo(nextVideo);
await player.next();
await player.prev();

Player methods:

MethodPurpose
play(), pause(), toggle()Control playback.
seekTo(seconds), seekBy(seconds)Move playback to an absolute or relative time.
currentTime(), duration(), remaining()Read playback time values.
mute(), unmute(), setMuted(value), setVolume(value)Control audio state.
setQuality(value)Set a quality such as "auto" or a rendition height.
setLayout(value)Switch player layout, such as standard or portrait.
appendVideo(source), next(), prev(), list()Build and control a playlist.
destroy()Remove Frontbacked player behavior from the element.

Frontbacked.video.source(source) normalizes a video source into the shape attach(...) uses. Use it when custom player UI needs to inspect poster, fallback, thumbnails, or prepared playback data before attaching.

Frontbacked.video.environment(videoElement) returns the current playback environment Frontbacked uses when choosing an initial quality. It is helpful for custom quality UIs and local testing.

Frontbacked.video.setSimulation(profile), .clearSimulation(), .playSimulation(sequence, element?), and .stopSimulationSequence() are local testing helpers for previewing quality selection and playback callbacks.

Video file objects include media metadata on reads. Check video.media.status for uploaded, queued, processing, ready, or failed. Use the Adaptive Video guide for player options, quality selection, timeline thumbnails, playlists, append behavior, previous-video loading, and callbacks.

Upload Helpers

Frontbacked.uploads.status(sessionId?) returns upload progress for files being uploaded in the current page session.

Frontbacked.uploads.pending(post) returns unfinished uploads found in a post object or post id.

Frontbacked.uploads.resume({ post, files?, onFile?, onProgress? }) resumes interrupted uploads. When no matching file is supplied, Frontbacked shows a resume modal that asks the user to choose the original file.

await Frontbacked.uploads.resume({
  post,
  onProgress(progress) {
    console.log(progress.fieldPath, progress.percent);
  }
});

Use the Upload File guide for progress fields, limits, and resume examples.

Frontbacked.uploads.resumePost(input) retries the saved post operation after files resume. Pass an id to update an existing post; omit id to create a post.

Frontbacked.uploads.clearCompleted() removes completed upload progress entries from the current page session.

Post Helpers

Frontbacked.createPost({ type, post }) creates a post.

OptionRequiredPurpose
typeyesFRL post type, such as products.
postyesPost data to validate and store.

If post contains File or Blob values at any depth, Frontbacked uploads each file with a resumable flow and stores file metadata in the post. The post body itself is sent as JSON.

Post IDs are generated by Frontbacked. Theme code can use the exact string "$id" as a normal field value on create when that field should store the generated post ID.

Frontbacked.updatePost({ id, type, post, merge }) updates an existing post. id and post are required. merge defaults to true; when true, Frontbacked can merge submitted fields with the stored post before validation.

Frontbacked.deletePost({ id }) deletes a post after Frontbacked checks the post type's delete rule.

Frontbacked.uploadPost(...) remains available as a compatibility alias for Frontbacked.createPost(...).

Browser Storage

Frontbacked.storage is a generic scoped browser-storage helper for theme state such as carts, wishlists, comparison trays, drafts, or saved filters. It stores data in the visitor's browser by site scope:

Frontbacked.storage.set("cart", items, { syncOnAuth: true });
const items = Frontbacked.storage.get("cart", []);
Frontbacked.storage.remove("cart");

Keys marked with syncOnAuth: true are synced into the signed-in user's data after authentication. Synced values are stored in the profile’s browserData field by default. Your FRL profile schema must declare this namespace and its allowed fields; storage sync does not bypass profile validation. Use a custom namespace when needed:

Frontbacked.storage.set("compare", selectedIds, {
  syncOnAuth: true,
  namespace: "savedChoices"
});
await Frontbacked.storage.syncToAuth({ keys: ["compare"], namespace: "savedChoices" });

This is generic storage, not a cart or checkout API: your theme defines item IDs, quantities, display data, and merge behavior. Always calculate checkout prices in an FRL endpoint from saved product data. syncOnAuth uploads the local snapshot after authentication; it does not automatically load or merge a cart from another device.

Storage sync is opt-in. Frontbacked does not upload arbitrary local browser data unless the theme explicitly marks a key for auth sync or calls syncToAuth().

Synchronized data

Use Frontbacked.sync.define() when visitors should keep their selections on this device and in their account. Keep using Frontbacked.storage for browser-only values. Your normal FRL profile schema and rules validate account saves; no extra theme endpoint is needed.

const shopping = Frontbacked.sync.define("shopping", {
  local: "localStorage",
  remote: Frontbacked.userData.at("browserData"),
  reconcile: {
    cart: ({ local = [], remote = [] }) =>
      Frontbacked.merge.byKey(remote, local, {
        key: "product",
        sum: ["quantity"]
      })
  },
  conflict: {
    cart: ({ local = [], remote = [], base = [] }) =>
      Frontbacked.merge.byKey(remote, local, {
        key: "product", sum: ["quantity"], base
      })
  }
});

shopping.set("cart", [{ product: "wig-id", quantity: 2 }]);
shopping.get("cart", []);
shopping.remove("cart");
const result = await shopping.flush();

Define connections once in your shared theme script. Calling define again with the same name returns the existing connection. Names and field keys must be stable; avoid overlapping ownership of the same field across different connections.

set saves locally immediately. Guests keep their changes on this device until authentication. Account saves are batched: connections using userData.at(...) share one destination, including connections targeting different profile paths. userData.at("") targets the profile data root. Only changed fields are submitted; removing a key also removes its saved account value, subject to FRL validation.

reconcile combines guest values with account values once. conflict handles an ordinary edit when both this device and the account have changed since the last successful read. Each callback receives { local, remote, base }. Without a resolver, guest values replace the corresponding account field; conflicting established account edits pause for review. With a base, merge.byKey applies quantity differences and local removals while preserving remote additions. Without a base, it combines lists and sums the named fields. merge.unique(values) removes identical JSON values.

Post references declared in the profile schema are saved as IDs, even when their display data is expanded in $auth. Pending work survives reloads. An imported guest selection belongs to the account that claimed it; signing out starts a separate guest scope. Retried saves do not repeat a successful import.

Read connection values directly in FQL:

<!-- {STATE}
  state = {
    "cart": "$sync.shopping.values.cart || []",
    "saving": "$sync.shopping.status == 'saving'"
  }
-->
<p f="true" f-show="$state.saving">Saving your selection…</p>

Connection statuses are local, pending, saving, synced, and paused. Read connection.error or $sync.shopping.error for a paused save. flush() waits for the destination queue and returns { ok: true }, a pending result, or an error; it does not open an alert. Frontbacked.sync.flush() flushes all destinations.

Optional conditions and error handling:

Frontbacked.sync.define("preferences", {
  remote: Frontbacked.userData.at("preferences"),
  when: ({ auth, state }) => auth.exists && state.savePreferences,
  watch: ["auth", "state.savePreferences"],
  onError: ({ reason, response }) => {
    if (reason === "unauthenticated") return { retryOn: "auth" };
    if (reason === "offline") return { retryOn: "online" };
    if (reason === "temporary") return { retryAfter: 2000, maxAttempts: 3 };
    return "pause";
  }
});

The built-in destination already waits for authentication and retries temporary connection failures. Invalid values and denied saves pause without losing local edits. Correct a value with set, then use flush to retry. Watches reevaluate eligibility; they do not repeatedly import previously saved guest data.

A custom destination can supply { id, read, write } instead of userData.at(...). read({auth}) returns {ok:true,data,version}. write({changes,remove,operationId,version,auth}) receives the combined object patch and dotted deletion paths, and returns {ok:true} or a structured error with an HTTP status. Return {ok:false,sync:{conflict:true}} when the version is stale. The destination must enforce permissions, atomic version checks, and operation-ID deduplication to support safe retries. Custom destination functions are defined in the theme; only their pending data is persisted.

The older storage syncOnAuth, syncToAuth, and syncRegisteredToAuth helpers remain compatibility APIs. Do not use them alongside a synchronized connection for the same data. Migrate the browser value once, then remove the old storage key to retire its snapshot upload.

MethodPurpose
get(key, fallback?)Return one scoped value, or fallback when it is missing.
set(key, value, options?)Store one value. Use { syncOnAuth: true } to mark it for profile sync after sign-in.
remove(key)Remove one key.
all(prefix?)Return every scoped key, optionally filtered by prefix.
clear(prefix?)Remove every scoped key, optionally filtered by prefix.
syncToAuth({ keys?, key?, prefix?, namespace?, clearLocal? })Copy selected browser values into the signed-in user's data.
syncRegisteredToAuth()Sync every key previously stored with syncOnAuth: true. Frontbacked also calls this after sign-in.

Storage keys must be non-empty strings up to 160 characters.

Auth Helpers

Frontbacked.signUp({ email, password, name, username, data, emailVerification }) creates a user account. On success, if the response returns a token, Frontbacked stores it in the browser and updates $auth.

const result = await Frontbacked.signUp({
  email: "ada@example.com",
  password: "secret-password",
  name: "Ada",
  data: {
    plan: "starter"
  },
  emailVerification: {
    redirectTo: "/dashboard"
  }
});

emailVerification.redirectTo is optional. When present, Frontbacked adds it to the verification email link. After the user clicks the email link and the token is accepted, the page redirects to that local path.

Frontbacked.signIn({ email, password, rememberMe }) signs in. On success, Frontbacked stores the token and updates $auth. rememberMe defaults to true; true keeps the user signed in across browser restarts, while false keeps sign-in for the current browser session.

const result = await Frontbacked.signIn({
  email: "ada@example.com",
  password: "secret-password",
  rememberMe: true
});

Frontbacked.signOut() and Frontbacked.logout() are the same operation. They remove the auth token from both browser storage locations, set $auth.exists to false, and re-render state-dependent UI.

Frontbacked.checkUsernameAvailability(username) checks whether a username is available before signup or profile updates.

Frontbacked.signInWithProvider(provider, options?) and Frontbacked.signUpWithProvider(provider, options?) start a provider sign-in or signup flow from JavaScript. provider defaults to "google". Use options.returnTo or options.redirectTo for the local page to open after sign-in when your UI needs a custom destination.

Frontbacked.signInWithGoogle(options?) and Frontbacked.signUpWithGoogle(options?) are Google-specific aliases. Frontbacked.completeProviderAuth(...) and Frontbacked.completeGoogleAuth(...) are available for custom callback pages; most themes can use f-auth-provider and let Frontbacked complete the flow automatically.

Frontbacked.sendEmailVerification({ email, redirectTo }) sends or resends the current user's email verification link. Frontbacked checks the current $auth.emailVerified value first; when it is already true, it returns { ok: true, message: "Email is already verified." } without sending a request. Frontbacked also enforces that only unverified email addresses for the authenticated user are sent.

const result = await Frontbacked.sendEmailVerification({
  email: Frontbacked.state.get("profile.email"),
  redirectTo: "/dashboard"
});

Frontbacked.requestPasswordReset({ email, path }) requests a password reset email. path is the local page the reset email link should open, such as /password-reset. The response uses a generic success message so account existence is not leaked.

const result = await Frontbacked.requestPasswordReset({
  email: "ada@example.com",
  path: "/password-reset"
});

Frontbacked.passwordReset(...) remains available as a compatibility alias for Frontbacked.requestPasswordReset(...).

Frontbacked.confirmPasswordReset({ email, token, newPassword }) submits the new password from a password reset page. email and token normally come from the reset email URL query string.

const params = new URLSearchParams(location.search);
const result = await Frontbacked.confirmPasswordReset({
  email: params.get("email") || "",
  token: params.get("token") || "",
  newPassword: Frontbacked.state.get("reset.newPassword")
});

Frontbacked.updateUserData({ data, merge }) updates the signed-in user's public auth data. merge defaults to true; when false, the submitted object replaces the existing data payload.

Frontbacked.updatePassword({ currentPassword, newPassword }) updates the signed-in user's password. Frontbacked verifies currentPassword before storing newPassword.

Billing Helpers

Frontbacked.billing.getMethods() returns the billing methods currently available to the site.

Payments are created by a theme-defined FRL endpoint. The endpoint validates the request, calculates the amount from trusted server-side data, and calls payment.create({ amount, metadata }). The browser only calls that endpoint and opens the returned payment:

const response = await Frontbacked.endpoints.call("create_product_payment", {
  body: { productId }
});

if (response.ok) {
  await Frontbacked.billing.showPayment({
    paymentId: response.payment.id,
    requireAuth: true
  });
}

The endpoint can use any money field or server-side calculation. Metadata is optional business context for payment events; it is not a price reference and must not be used as an authority for the amount.

Themes can read or set the visitor's storefront currency with the same code locally and after publishing:

Frontbacked.getCurrency()      // { value: "USD", baseCurrency: "USD", ... }
Frontbacked.setCurrency("NGN") // refetches FQL data with NGN as request context

Site admins manage storefront currency from fb-admin > Site Settings > Currency. Themes can read the saved public currency values in this shape:

{
  "storefront_currency": {
    "value": "USD",
    "baseCurrency": "USD",
    "supportedMode": "selected",
    "supportAllCurrencies": false,
    "supportedCurrencies": ["USD", "NGN", "GBP"]
  }
}

When supportAllCurrencies is true, any listed country currency can be requested. When it is false, Frontbacked falls back to baseCurrency if the visitor requests a currency outside supportedCurrencies.

Money fields are exposed to FQL as rich money objects. Theme-defined functions can call the safe methods added to each value:

<strong f="true" f-text="$.price.formatMoney()">USD 99.00</strong>
<button
  f="true"
  f-attr-data-price="$.price.getAmount()"
>
  Buy now
</button>

For money filters and ordering, use the existing typed-field syntax with @money and pass the currency explicitly:

{
  $list: "products",
  $where: {
    "price@money.$gte": { currency: "USD", amount: "100" },
    "price@money.$lte": { currency: "USD", amount: "400" }
  },
  $order: { "price@money": "asc" }
}

Pass onClose: ({ payment }) => { ... } to run an action when the customer closes the widget. payment contains the latest payment status. For example, navigate to order history when payment.status === "successful". A payment with metadata.cryptoSettlement.status === "underpaid_wallet_funding" funded the wallet and did not complete the order; keep that customer in checkout.

Frontbacked.billing.showPayment({ paymentId }) reopens the payment widget for a pending, unexpired transaction. This is useful for dashboard or transactions rows rendered from $transactions.

<button
  type="button"
  data-payment-id="pay_123"
  onclick="Frontbacked.billing.showPayment({ paymentId: this.dataset.paymentId })"
>
  View payment
</button>

Frontbacked.billing.claimPayment({ paymentId, methodId, txHash, receipt, metadata }) submits a manual payment claim for admin review. Use it only for manual billing methods.

Frontbacked.billing.checkPaymentStatus({ paymentId, methodId, requireAuth, signal }) returns the latest status for one payment without opening the widget. Use it for custom dashboard rows or a compact checkout panel.

Frontbacked.billing.createDepositAddress({ paymentId, methodId }) prepares the payable address/details for a selected deposit method. The built-in widget calls this for you; custom checkout UIs can call it directly.

Frontbacked.billing.paymentLink(paymentId) returns a shareable payment URL for a payment id.

Validation

Frontbacked.state.check(path, schema, check?) validates one state path or a whole state object against a schema. It returns { ok, value, values, errors }. If validation fails, Frontbacked shows messages and returns ok: false. See Schema and Validation for full examples.

const result = Frontbacked.state.check("form", {
  name: {
    type: "string",
    required: true,
    trim: true,
    min: 2
  },
  email: {
    type: "email",
    required: true,
    trim: true
  },
  terms: {
    type: "boolean",
    accepted: true
  },
  avatar: {
    type: "file",
    maxSize: 3000000,
    mimeTypes: ["image/jpeg", "image/png"]
  }
});

if (!result.ok) return;
const { name, email } = result.values;

The optional third argument is a mutable check object:

const check = { hasError: false, errors: [] };
const nameResult = Frontbacked.state.check("form.name", schema.name, check);
const emailResult = Frontbacked.state.check("form.email", schema.email, check);

if (check.hasError) return;

const name = nameResult.value;
const email = emailResult.values.email;

Useful schema keys:

KeyPurpose
typestring, email, number, boolean, object, array, file, datetime, or post.
requiredRequire a present value. Empty strings, empty arrays, and missing files fail.
trim, lowercase, uppercaseNormalize string values before validation returns. email lowercases by default.
min, maxString length, number value, or array length limit.
patternRegular expression for string values.
enumAllowed values.
sameAsRequire the value to equal another state path.
acceptedRequire a boolean value to be true, useful for terms checkboxes.
maxSizeMaximum file size in bytes.
mimeTypesAllowed file MIME types.
maxDurationSecondsMaximum prepared video playback duration in seconds.
fieldsNested object schema.
of or itemsArray item schema.
messagesOptional messages by failed rule, such as required, min, email, sameAs, accepted, maxSize, or mimeTypes.
feedbackMessage destination options. Omit for automatic inline placement, or use { to: "alert" }, { to: "toast", duration: 4000 }, { to: "formErrors" }, or { to: "showFormError()" }.
validateCustom validation function that receives (value, state). Return false or a string message to fail.
allowFalseTreat boolean false as valid.
allowEmptyStringTreat an empty string as valid.
allowEmptyArrayTreat an empty array as valid.

When a message is missing, Frontbacked generates one from the field key and failed rule. For example, firstName becomes first name, so a missing required value becomes Please enter your first name., while min: 2 becomes First name must be at least 2 characters..

Loading Attributes

Frontbacked automatically locks the element that triggered an FQL event while a Frontbacked network request is pending. The locked element receives frontbacked-loading, and repeated events from that same element are ignored until the request returns.

For onsubmit state bindings, Frontbacked calls preventDefault() before your handler runs, so bound forms do not reload the page while async handlers are waiting.

<button
  type="submit"
  f-loading-text="Saving..."
  f-loading-attr-aria-busy="true"
  f-loading-attr-data-state="loading"
>
  Save
</button>

<button type="submit" f-loading="off">
  Save with a custom loader
</button>
AttributePurpose
f-loading-textTemporary innerText while the request is pending.
f-loading-attr-XTemporary attribute value while loading, where X is the HTML attribute name.
f-loading="off"Opts the trigger out of Frontbacked loading UI and re-entrancy locking. The request still runs.

Frontbacked restores the previous text, attributes, disabled state, and class after the request finishes.

Older themes may still use f-loading-disabled="true" for the same opt-out behavior, but new themes should use f-loading="off".

Component Imports

Use imports to keep repeated HTML in small theme components without writing layout helper JavaScript.

<div f-insert="/components/header.html"></div>
<div f-replace="/components/footer.html"></div>

f-insert keeps the host element and places the imported HTML inside it. f-replace swaps the host element out completely.

Imported fragments can contain normal FQL markup:

<!-- /components/account-link.html -->
<a href="/dashboard" f="true" f-text="$state.accountLabel || 'Dashboard'">Dashboard</a>

Frontbacked resolves imports before the browser receives the page, so shared components are already in place. Imported f-text, f-attr-*, f-list, and event-bound state sources work like markup written directly on the page.

Import paths are same-theme HTML paths only. URLs, .., hidden files, backend, skins, uploads, and node_modules are blocked. Nested imports are resolved by Frontbacked with depth and size limits plus recursion detection.

Importing Full Pages

An imported file may be a full HTML document:

<!-- /components/profile-card.html -->
<!doctype html>
<html>
<head>
  <title>This title is not imported</title>
  <!-- {STATE}
    state = {
      "profileName": "$siteInfo.name || 'Investor'"
    }
  -->
</head>
<body>
  <article f="true" f-text="$state.profileName">Investor</article>
</body>
</html>

When a full page is imported, only the imported page's <body> content is inserted. The imported <head> is not inserted, so titles, meta tags, styles, and scripts from the imported page do not leak into the parent page.

The exception is the imported page's {STATE} comment. Frontbacked merges imported head state into the parent page's single {STATE} comment before serving the HTML:

<div f-replace="/components/profile-card.html"></div>

The final page sent to the browser has one head state declaration that contains both imported state and parent page state. Parent page state is added last, so a duplicate key on the parent page overrides the imported key.

If an import fails or a circular import is detected, the placeholder is replaced with a visible frontbacked-import-error block. Valid sibling imports still render.

Page Functions

function formatCurrency(value) {
  return "NGN " + Number(value || 0).toLocaleString();
}

function toggleTerms() {
  return !Frontbacked.state.get("terms");
}

function projectedReturn(percent, amount) {
  return amount + amount * percent / 100;
}

Frontbacked.functions.define({
  formatCurrency,
  projectedReturn,
  toggleTerms
});

Use registered functions by name in FQL:

{
  "amount": "#amount.oninput.target.value.Number()",
  "category": "#categories.onchange.target.value.load().post().parseResponse()",
  "formatted": "$state.amount.formatCurrency()",
  "projected": "$state.roi.projectedReturn($state.amount)",
  "terms(true)": "#terms.onchange.target.checked, #termsButton.onclick.target.toggleTerms()"
}

The value before .functionName(...) is always the first argument. Explicit FQL arguments follow it. Inside a list, pass $ when a function needs the full current item, such as $.status.statusLabel($). Frontbacked never supplies a hidden item argument.

Function names must be single JavaScript-style identifiers, such as formatCurrency; dotted registry names are invalid. When multiple .functionName() segments are chained, Frontbacked pipes the value through them from left to right before setting state. Built-ins are Number, String, Boolean, count, trim, lowercase, uppercase, and avatar. Money formatters are theme-defined functions and can call Frontbacked.money.*; unknown functions fail visibly.

Money helpers keep theme markup independent from currency precision:

<strong f="true" f-text="$.balance.moneyFormat()">USD 0.00</strong>
<span f="true" f-text="$.balance.moneyCurrency()">USD</span>
Frontbacked.functions.define({
  moneyAmount: (money) => Frontbacked.money.getAmount(money),
  moneyCurrency: (money) => Frontbacked.money.getCurrency(money),
  moneyFormat: (money) => Frontbacked.money.format(money, { minDecimals: 2, maxDecimals: 2 }),
});

Money values arrive as { values: { USD: { amount: "125.50", currency: "USD" } } }. Functions at the end of an FQL value path are theme-defined, so a theme can register moneyAmount, moneyCurrency, or moneyFormat and receive the resolved money value as the first argument. Inside those functions, use Frontbacked.money.getAmount, Frontbacked.money.getCurrency, Frontbacked.money.format, Frontbacked.money.add, Frontbacked.money.subtract, Frontbacked.money.compare, Frontbacked.money.convertTo, and Frontbacked.money.allocate. Amounts are returned as strings to preserve exactness. Formatting accepts minDecimals, maxDecimals, and preserveInteger.

Publishing Checklist for FQL Pages

  1. State bindings use stable selectors that continue to match the intended elements.
  2. Every rendered element has f="true".
  3. Lists have an id, a first-child template, and stable id values or an explicit f-key.
  4. Forms prevent duplicate submits and show a submitting state.
  5. File inputs store File values in state before upload/update.
  6. Commas are used for multiple event sources, && is used for truthy gating, and || is used for fallbacks.
  7. Writes have matching FRL schemas and permission rules.
  8. Search and filter selectors name the fields visitors should actually search or filter.