FQL Overview

Frontbacked Query Language (FQL) is the frontend language built into frontbacked.js. It lets a theme declare browser state, read URL/query/server values, fetch Frontbacked data, and render values into the DOM without writing manual request orchestration code for every page.

With FQL, a plain HTML page can become a signed-in dashboard, a marketplace listing page, an upload form, a settings editor, a wallet-powered checkout, or an adaptive video experience while keeping the theme code readable.

FQL appears in two places:

  1. A {STATE} comment inside <head>.
  2. The state option passed to Frontbacked.init().
  3. f-* attributes on HTML elements.

When both are present, Frontbacked.init({ state }) is used first. The {STATE} comment is the fallback when no state is passed to init.

Minimal Page

<!doctype html>
<html>
  <head>
    <!-- {STATE}
      state = {
        "name": "#name.oninput.target.value",
        "displayName": "$state.name || $query.name || 'Guest'"
      }
    -->
  </head>
  <body>
    <input id="name" name="name">
    <h1 f="true" f-text="$state.displayName">Guest</h1>

    <script src="https://cdn.frontbacked.com/frontbacked.js?v=20.0.0"></script>
    <script>
      Frontbacked.init({ endpoint: "/api" });
    </script>
  </body>
</html>

When the user types into #name, FQL updates $state.name, recomputes $state.displayName, and renders the new value into the h1.

Minimal Form

FQL selectors are normal CSS selectors. You can use ids, descendants, attributes, and spaces in the selector before the event segment.

<!doctype html>
<html>
  <head>
    <!-- {STATE}
      state = {
        "signup": {
          "email": "#signupForm input[type=email].oninput.target.value",
          "password": "#signupForm input[type=password].oninput.target.value",
          "terms(false)": "#signupForm input[name=terms].onchange.target.checked",
          "response": "#signupForm.onsubmit.handleSignup()"
        }
      }
    -->
  </head>
  <body>
    <form id="signupForm">
      <input type="email" name="email" placeholder="you@example.com">
      <input type="password" name="password" placeholder="Password">
      <label>
        <input type="checkbox" name="terms">
        I accept the terms
      </label>

      <button
        type="submit"
        f-loading-text="Creating account..."
        f-loading-attr-aria-busy="true"
      >
        Create account
      </button>
    </form>

    <script src="https://cdn.frontbacked.com/frontbacked.js?v=20.0.0"></script>
    <script>
      const signupSchema = {
        email: {
          type: "email",
          required: true,
          trim: true,
          messages: {
            required: "Enter the email address you want to use."
          }
        },
        password: {
          type: "string",
          required: true,
          min: 8,
          messages: {
            min: "Use at least 8 characters."
          }
        },
        terms: {
          type: "boolean",
          accepted: true
        }
      };

      async function handleSignup(event) {
        const result = Frontbacked.checkState("signup", signupSchema);
        if (!result.ok) return;

        const { email, password } = result.values;
        return await Frontbacked.signUp({
          email,
          password,
          name: email,
          emailVerification: {
            redirectTo: "/dashboard"
          }
        });
      }

      window.handleSignup = handleSignup;
      Frontbacked.init({ endpoint: "/api" });
    </script>
  </body>
</html>

In this form, FQL keeps state in sync, prevents the default form reload for the onsubmit binding, checkState shows validation messages, and frontbacked.js locks the submit button while Frontbacked.signUp() is waiting for a response. The button gets frontbacked-loading, its text changes through f-loading-text, and repeated submits from the same button are ignored until the request finishes.

The messages object is optional and can define messages for specific failed checks. In the example, email.required and password.min use custom copy. Other failures are still handled: an invalid email gets a generated message such as Please enter a valid email address., an empty password gets Please enter your password., and unchecked terms gets Please accept terms. Frontbacked builds those fallback messages by turning field keys such as firstName or first_name into readable names.

What FQL Handles

FQL handles common frontend jobs:

JobExample
State from DOM events"email": "#email.oninput.target.value"
Form validationFrontbacked.checkState("signup", signupSchema)
Loading/re-entry guardsf-loading-text, f-loading-attr-*, frontbacked-loading
Page access gatesf-access="auth", f-redirect-to="/signin"
Component importsf-insert="/components/header.html", f-replace="/components/footer.html"
Derived state`"title": "$state.product.title
URL and server values"$query.plan", "$params.slug", "$siteInfo.name"
Frontbacked data requests"$settings.site.name", "products{id:$query.id}.title"
DOM renderingf-text, f-attr-*, f-list
Conditional visibilityf-show="$auth.exists", f-hide="$auth.exists"
File uploadsFrontbacked.uploadPost({ type, post }) with files anywhere in post
Upload recoveryFrontbacked.uploads.resume({ post })
Image variantsFrontbacked.image.resize(post.image, { width: 640 })
Adaptive videoFrontbacked.video.attach(video, post.video, options)
Programmatic playbackplayer.seekBy(10), player.appendVideo(nextVideo)
Billing flowsFrontbacked.billing.createPayment({ priceRef, amount })
Theme skinsFrontbacked.setSkinMode("dark")
Same-site navigationFrontbacked.navigate("/dashboard")
Custom theme endpointsfetch("/fb-endpoints/your-path", { method: "POST" })

Initialization

Load frontbacked.js from the Frontbacked CDN, then call Frontbacked.init().

<script src="https://cdn.frontbacked.com/frontbacked.js?v=20.0.0"></script>
<script>
  Frontbacked.init({
    endpoint: "/api",
    debug: false,
    state: {
      "form": {
        "email": "#email.oninput.target.value"
      },
      "emailLabel": "$state.form.email || 'No email yet'"
    }
  });
</script>

If you load the CDN script with defer, run your initialization from a deferred page script or a DOMContentLoaded handler so Frontbacked is available first.

Options:

OptionPurpose
endpointBase API path used for query, auth, and post requests. Defaults to /api.
debugEnables additional runtime debugging and exposes internal state for inspection.
stateOptional state declaration object. If provided, it is used before the {STATE} HTML comment.

Functions used inside FQL are read from window or globalThis:

function uppercase(value) {
  return String(value || "").toUpperCase();
}

window.uppercase = uppercase;

FQL Lifecycle

When initialized, frontbacked.js:

  1. The server resolves any f-insert and f-replace component imports before sending the page.
  2. frontbacked.js reads the {STATE} declaration from the document head.
  3. It parses state expressions and default values.
  4. It scans elements with f="true" and list roots with f-list.
  5. It binds DOM events declared in state.
  6. It builds a request plan for settings, posts, and lists.
  7. It fetches only missing data.
  8. It renders text and attributes into the DOM.
  9. It re-runs derived state and rendering whenever state changes.

For conditional visibility, f-hide elements stay visible until their expression resolves truthy, while f-show elements are hidden as soon as Frontbacked starts and stay hidden until their expression resolves truthy. This makes f-show the safer choice for links or panels that must not flash before auth or requested data is known.

Frontbacked also handles same-site page navigation as a lightweight SPA. When a visitor clicks an internal link, frontbacked.js fetches the next HTML page, swaps in the new document, re-runs the FQL lifecycle, and updates browser history without a full reload. The browser still uses normal cache validation, so server ETags can avoid resending unchanged pages.

Add a custom loading view with f-loader:

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

Frontbacked waits for the next page's new stylesheets before swapping the body, so visitors do not see the next page before its CSS is ready. It also prefetches same-site links on hover, focus, and touch start, using normal browser cache validation so ETags still protect against stale pages.

Use f-nav="reload" when a link or whole page should keep normal browser reload behavior:

<a href="/legacy-checkout" f-nav="reload">Open checkout</a>
<body f-nav="reload">

Programmatic navigation is available through Frontbacked.navigate("/packages"). By default it pushes history and scrolls to the top. Use { history: "replace" } for redirects, { scroll: "preserve" } when the current scroll should stay in place, and { minLoadTime: 1000 } when you want to preview a loader for at least one second.

The same minimum loading time can be declared in HTML:

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

Component Imports

Use f-insert and f-replace for shared page parts:

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

The imported HTML is loaded from the same theme before the page is served. Nested imports are resolved by Frontbacked, and FQL bindings inside imported fragments work as if the HTML was written directly on the page.

Imported files can be fragments or complete HTML pages. When the imported file is a complete page with <head> and <body>, Frontbacked inserts only the content inside that imported page's <body>. The imported page's <head> is not inserted into the page, except for its FQL {STATE} declaration.

If imported files contain {STATE} comments in their <head>, Frontbacked merges those declarations into one {STATE} comment in the main page head before the page is sent to the browser. Imported state is placed first, then the main page state, so the main page can override a duplicate state key.

<!-- /components/account-summary.html -->
<!doctype html>
<html>
<head>
  <!-- {STATE}
    state = {
      "accountLabel": "$siteInfo.name || 'Account'"
    }
  -->
</head>
<body>
  <a href="/dashboard" f="true" f-text="$state.accountLabel">Account</a>
</body>
</html>
<!-- /index.html -->
<head>
  <!-- Frontbacked merges the imported state into this page head. -->
</head>
<body>
  <div f-replace="/components/account-summary.html"></div>
</body>

If an import cannot be resolved, is blocked, is too large, too deep, or creates a circular import, Frontbacked replaces that import placeholder with a visible frontbacked-import-error block so the issue is obvious during theme development.

FQL and FRL Together

FQL is not a security layer. It improves the frontend developer experience by declaring state and requests. FRL protects saved data by validating writes and enforcing permissions.

Use FQL to submit a signup form:

await Frontbacked.signUp({
  email: Frontbacked.getState("form.email"),
  password: Frontbacked.getState("form.password"),
  name: Frontbacked.getState("form.name"),
  emailVerification: {
    redirectTo: "/dashboard"
  }
});

Use FRL to validate any post data that the frontend creates or updates.

A Practical Theme Page

<head>
  <!-- {STATE}
    state = {
      "article": "articles{slug:$params.slug}",
      "title": "$state.article.title || 'Article'",
      "author": "$state.article.authorName || $siteInfo.name || 'Frontbacked'"
    }
  -->
</head>
<body>
  <article>
    <h1 f="true" f-text="$state.title">Article</h1>
    <p f="true" f-text="$state.author">Frontbacked</p>
  </article>
</body>

This page fetches an article by route param, derives display state, and renders it into the page.