Frontbacked Docs

Starter Examples

These examples are small enough to copy, but shaped like real themes. Start with the page, add the rule, then change the fields to match the product you are building.

Folder Shape

my-theme/
  index.html
  dashboard.html
  css/
    app.css
  js/
    app.js
  backend/
    index.rules
    examples.json
    paths.json

Use HTML for pages, CSS for the look, and normal JavaScript for interactions. FQL keeps the page connected to state and saved data. FRL decides what can be saved, read, changed, or exposed.

Starter Content for New Sites

Add an optional backend/examples.json file when a theme is easier to understand with a few realistic records already in place. Frontbacked creates those records once when a site is created from the theme. Site owners can edit, keep, or delete them like any other post.

{
  "version": 1,
  "assets": {
    "cover": { "path": "assets/cover.webp" }
  },
  "posts": {
    "categories": [
      { "key": "featured", "data": { "name": "Featured" } }
    ],
    "products": [
      {
        "key": "starter",
        "data": {
          "name": "Starter product",
          "price": "49.00",
          "category": { "$examplePost": "categories/featured" },
          "image": { "$themeAsset": "cover" },
          "owner": { "$postAuthor": true }
        }
      }
    ]
  }
}

The post types and fields must already be declared in backend/index.rules. Money examples contain only an amount; the new site's base currency is applied automatically. Use $examplePost for another example post, $postAuthor for the generated starter-post author, $siteOwner when the site owner is also a tenant user, and $themeAsset for a file declared in assets. Theme assets stay shared and are referenced by content hash, so creating many sites does not duplicate their bytes.

Public Waitlist

This page collects a lead, validates it in the browser, and stores it through a guarded endpoint.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Launch List</title>
    <link rel="stylesheet" href="/css/app.css">

    <!-- {STATE}
      state = {
        "lead": {
          "name": "#waitlistForm input[name=name].oninput.target.value",
          "email": "#waitlistForm input[name=email].oninput.target.value",
          "idea": "#waitlistForm textarea[name=idea].oninput.target.value",
          "response": "#waitlistForm.onsubmit.joinWaitlist()"
        }
      }
    -->
  </head>
  <body>
    <main class="page">
      <section class="intro">
        <p class="eyebrow">Private beta</p>
        <h1>Join the first builders using Launch List.</h1>
        <p>Tell us what you want to build. We will send the first invites to people with clear use cases.</p>
      </section>

      <form id="waitlistForm" class="panel">
        <label>
          Name
          <input name="name" autocomplete="name" required>
        </label>
        <label>
          Email
          <input name="email" type="email" autocomplete="email" required>
        </label>
        <label>
          Startup idea
          <textarea name="idea" rows="4"></textarea>
        </label>
        <button type="submit" f-loading-text="Joining...">Join waitlist</button>
        <p f="true" f-show="$state.lead.response.ok" f-text="$state.lead.response.message"></p>
      </form>
    </main>

    <script src="https://cdn.frontbacked.com/versions/frontbacked-v37.0.105.js"></script>
    <script src="/js/app.js"></script>
  </body>
</html>
const leadSchema = {
  name: { type: "string", required: true, trim: true, min: 2, max: 120 },
  email: { type: "email", required: true, trim: true },
  idea: { type: "string", trim: true, max: 600 }
};

async function joinWaitlist() {
  const result = Frontbacked.state.check("lead", leadSchema);
  if (!result.ok) return;

  const response = await Frontbacked.endpoints.call("joinWaitlist", {
    allowGuest: true,
    body: result.values
  });

  if (response.ok) {
    Frontbacked.state.set("lead", { name: "", email: "", idea: "", response });
  }

  return response;
}

Frontbacked.functions.define({ joinWaitlist });
Frontbacked.init({ endpoint: "/api" });
version 2;

post leads as "Waitlist Leads" {
  before create {
    $post.pending.data.name = str.trim($post.pending.data.name);
    $post.pending.data.email = str.lower(str.trim($post.pending.data.email));
    $post.pending.data.status = "new";
  }

  schema {
    name as "Name": { type: "string", min: 2, max: 120, required: true }
    email as "Email": { type: "email", required: true, immutable: true }
    idea as "Startup idea": { type: "string", max: 600 }
    status as "Status": { type: "enum", enums: ["new", "contacted", "invited"], default: "new" }
  }

  allow create: $admin.canCreate("$this")
  allow get: $admin.canRead("$this")
  allow list(limit=20, max=100): $admin.canRead("$this")
  allow edit: $admin.canEdit("$this")
  allow delete: $admin.canDelete("$this")
}

endpoint.post joinWaitlist("/waitlist") as "Join Waitlist" {
  must($request.body.email, "Email is required", 422);

  $context.email = str.lower(str.trim($request.body.email));
  dedupe `waitlist:${$context.email}`;

  post.createAsSystem("leads", {
    name: str.trim($request.body.name),
    email: $context.email,
    idea: str.trim($request.body.idea),
    status: "new"
  });

  return {
    ok: true,
    message: "You are on the list."
  };
}

Read next: FQL State, DOM Bindings, FRL Schemas and Validation, and Endpoints and Dedupe.

Protected Dashboard

Use f-access on pages that require a signed-in visitor or an admin. The dashboard can read live leads and update them with the same post API.

<head>
  <!-- {STATE}
    state = {
      "recentLeads.live": {
        "$list": "leads",
        "$order": { "createdOn": "desc" },
        "$limit": 20
      }
    }
  -->
</head>

<body f-access="admin" f-redirect-to="/signin">
  <main class="dashboard">
    <header>
      <h1>Waitlist</h1>
      <a href="/" f-admin-link>Open site admin</a>
    </header>

    <section f-list="$state.recentLeads">
      <article f-key="$.id" class="row">
        <div>
          <strong f="true" f-text="$.name">Name</strong>
          <span f="true" f-text="$.email">email@example.com</span>
        </div>
        <button
          type="button"
          f="true"
          f-attr-data-id="$.id"
          onclick="markInvited(this.dataset.id)"
          f-loading-text="Saving..."
        >
          Mark invited
        </button>
      </article>
    </section>
  </main>
</body>
async function markInvited(id) {
  return Frontbacked.updatePost({
    id,
    type: "leads",
    post: { status: "invited" },
    merge: true
  });
}

Frontbacked.functions.define({ markInvited });
Frontbacked.init({ endpoint: "/api" });

Read next: FQL Reference for page access, Auth and Posts API, and FRL Permissions.

Store Card

A product card can stay mostly HTML. FQL reads the post's money field and hands checkout to a guarded FRL payment endpoint.

{
  "products": {
    "$list": "products",
    "$where": { "status": "published" },
    "$order": { "createdOn": "desc" },
    "$limit": 12
  }
}
<article f-list="$state.products">
  <div f-key="$.id" class="product">
    <img f="true" f-attr-src="$.image.url" f-attr-alt="$.title">
    <h2 f="true" f-text="$.title">Product title</h2>
    <strong f="true" f-text="$.price.formatMoney()">USD 49.00</strong>
    <button type="button" f="true" f-attr-data-id="$.id" onclick="buyProduct(this)">
      Buy now
    </button>
  </div>
</article>
async function buyProduct(button) {
  const id = button.dataset.id;
  const products = Frontbacked.state.get("products.items") || Frontbacked.state.get("products") || [];
  const product = Array.isArray(products) ? products.find((item) => item.id === id) : null;
  const response = await Frontbacked.endpoints.call("create_product_payment", {
    body: { productId: id }
  });
  if (!response?.ok) return response;
  return Frontbacked.billing.showPayment({
    paymentId: response.payment.id,
    requireAuth: true
  });
}

Frontbacked.functions.define({ buyProduct });

Read next: FQL Data Fetching, Image Resizing, and Billing Helpers in the FQL reference.

Starter Choices

You want to buildStart with
Waitlist, contact form, support request, beta signupcreatePost or a small FRL endpoint with dedupe
Blog, directory, jobs board, listings page$list, $where, $order, f-list, and route params
Client portal or account dashboardf-access, $auth, Frontbacked.updateUserData(), and FRL profile
Store, paid course, membershiptype: "money", a server payment endpoint, $transactions, and billing helpers
Media gallery, course library, product catalogfile fields, image resizing, adaptive video, and upload resume
Multi-look theme marketplace itemskins/manifest.json, Frontbacked.setSkinMode(), and admin skin helpers

Building With AI

Open /llms.txt and give it to your AI coding tool before asking it to generate a Frontbacked theme. It contains the public FQL/FRL syntax, helper names, naming rules, and patterns that keep generated code aligned with Frontbacked.