# Frontbacked Theme Development A Frontbacked theme is a reusable website package made from HTML, CSS, JavaScript, assets, optional skins, and optional rules for the data and actions the site supports. Use Frontbacked Query Language (FQL) in pages and Frontbacked Rule Language (FRL) in `backend/index.rules`. Documentation: - Introduction: https://docs.frontbacked.com/introduction - Getting started: https://docs.frontbacked.com/getting-started - Starter examples: https://docs.frontbacked.com/starter-examples - FQL overview: https://docs.frontbacked.com/fql-overview - FQL reference: https://docs.frontbacked.com/fql-reference - FRL overview: https://docs.frontbacked.com/frl-overview - FRL reference: https://docs.frontbacked.com/frl-reference ## Theme Shape Common files: - `index.html`, `signin.html`, `signup.html`, `dashboard.html`, and other normal pages - `css/` and `js/` for theme styling and behavior - `backend/index.rules` for FRL schemas, permissions, hooks, endpoints, and payment events - `backend/paths.json` for clean routes such as `/blog/:slug` - `skins/manifest.json` and skin CSS files when the theme supports multiple looks Load the public script before calling `Frontbacked.init()`: ```html ``` ## FQL Essentials FQL appears in a `{STATE}` comment inside ``, in `Frontbacked.init({ state })`, and in `f-*` DOM attributes. State examples: ```html ``` Use `.live` at the end of a state key when a value should stay current after saved data changes. The public key does not include `.live`; read `$state.orders`, not `$state.orders.live`. Common FQL roots: - `$state` for browser state - `$auth` for the signed-in visitor - `$query` for URL query params - `$params` for clean route params from `backend/paths.json` - `$settings` for public site settings - `$siteInfo` for public site info - `$posts.type.{id: id}` for one post - `$posts.type.{$where: {...}}.$count` and `$sum.field` for aggregates - `$transactions` and `$wallet` for account billing and wallet views - `$now` for the page's current-time snapshot Common DOM directives: - `f="true"` marks an element for rendering - `f-text="expression"` writes text content - `f-attr-name="expression"` writes or removes an attribute - `f-show="expression"` shows only when truthy - `f-hide="expression"` hides when truthy - `f-list="$state.items"` repeats the first child as the item template - `f-key="expression"` gives list items a stable key - `f-access="auth"` or `f-access="admin"` protects a page or section - `f-page-access="auth"` is an alias for `f-access` - `f-redirect-to="/signin"` chooses the redirect path - `f-redirect="/signin"` is an alias for `f-redirect-to` - `f-loading-text="Saving..."` changes button text while an async handler runs - `f-loading-attr-aria-busy="true"` sets loading attributes - `f-loading="off"` disables built-in loading UI for one trigger - `f-insert="/components/header.html"` inserts shared HTML - `f-replace="/components/footer.html"` replaces an element with shared HTML - `f-auth-provider="google"` starts provider sign-in when the provider is available - `f-auth-return-to="/dashboard"` sets the local return path after provider sign-in - `f-auth-mode="signin"` or `f-auth-mode="signup"` sets a provider auth mode - `f-google-auth` is the Google-specific compatibility alias - `f-skeleton-count="3"` sets list placeholder row count while loading - `f-no-skeleton` disables skeleton placeholders for a section - `f-video="$.video"` attaches video playback to a video element - `f-video-player`, `f-video-quality-selector`, `f-video-timeline-thumbnails`, `f-video-layout`, `f-video-autoplay`, `f-video-muted`, `f-video-poster`, `f-video-hide-controls`, `f-video-hidden-controls`, `f-video-max-quality`, `f-video-data-saver`, and `f-video-hls-loader` map to video player options - `f-support-launcher` renders the support launcher - `f-support-key`, `f-support-id`, `f-support-launcher-open`, `f-support-launcher-close`, `f-support-launcher-toggle`, and `f-support-toggle` control support UI - `f-admin-link` links admins to the site admin area - `f-edit-mode-enable`, `f-edit-mode-disable`, and `f-edit-mode-toggle` control page edit mode Register functions before `Frontbacked.init()`: ```js async function signUpUser() { const checked = Frontbacked.state.check("form", { email: { type: "email", required: true, trim: true }, password: { type: "string", required: true, min: 8 } }); if (!checked.ok) return; return Frontbacked.signUp(checked.values); } Frontbacked.functions.define({ signUpUser }); Frontbacked.init({ endpoint: "/api" }); ``` Important FQL helpers: - `Frontbacked.init({ endpoint, debug, state })` - `Frontbacked.navigate(url, options?)` - `Frontbacked.state.get(path?)`, `set(path, value)`, `set(values)`, `check(path, schema, check?)` - `Frontbacked.lists.goTo(listId, page, cursor?)`, `next(listId)`, `prev(listId)` - `Frontbacked.live.status(path?)`, `isConnected()` - `Frontbacked.functions.define(name, fn)`, `define(functions)`, `remove(name)`, `has(name)` - `Frontbacked.storage.get(key, fallback?)`, `set(key, value, options?)`, `remove(key)`, `all(prefix?)`, `clear(prefix?)`, `syncToAuth(options?)` - `Frontbacked.createPost({ type, post })`, `uploadPost({ type, post })`, `updatePost({ id, type, post, merge })`, `deletePost({ id })` - `Frontbacked.endpoints.call(name, { params, query, body, allowGuest, signal })` - `Frontbacked.signUp()`, `signIn()`, `signOut()`, `logout()`, `sendEmailVerification()`, `passwordReset()`, `requestPasswordReset()`, `confirmPasswordReset()` - `Frontbacked.signInWithProvider(provider, options?)`, `signUpWithProvider(provider, options?)` - `Frontbacked.updateUserData({ data, merge })`, `updatePassword({ currentPassword, newPassword })`, `checkUsernameAvailability(username)` - `Frontbacked.uploads.status(sessionId?)`, `pending(post?)`, `resume(options)`, `resumePost(input)`, `clearCompleted()` - `Frontbacked.image.resize(source, options)`, `url(source, options)`, `srcset(source, entries)` - `Frontbacked.video.attach(videoElement, source, options?)`, `source(source)`, `environment(videoElement)` - `Frontbacked.billing.getMethods()`, `showPayment(input)`, `checkPaymentStatus(input)`, `createDepositAddress(input)`, `claimPayment(input)`, `paymentLink(input)`; payment creation belongs in a theme-defined FRL endpoint. - `Frontbacked.getSkin()`, `listSkins()`, `setSiteSkin(skinId, options?)`, `setSkinMode(mode)`, `toggleSkinMode()`, plus `Frontbacked.skins.*` aliases - `Frontbacked.supportLauncher.open(key?)`, `close(key?)`, `toggle(key?)`, `contacts()`, `whatsappUrl(contact?, message?)`, `telegramUrl(contact?, message?)` - `Frontbacked.openSupportChat(key?)`, `closeSupportChat(key?)`, and `toggleSupportChat(key?)` control native site chat directly - `Frontbacked.confirm(message, title?)` ## FRL Essentials Use `version 2;` as the first statement in `backend/index.rules`. ```frl version 2; post products as "Products" { before create, edit { $post.pending.data.title = str.trim($post.pending.data.title); $post.pending.data.slug = $post.pending.data.slug || str.slug($post.pending.data.title); } schema { title as "Title": { type: "string", min: 1, max: 140, required: true } slug as "Slug": { type: "string", max: 180, required: true, immutable: true } status as "Status": { type: "enum", enums: ["draft", "published"], default: "draft" } image as "Image": { type: "file", maxSize: 7000000, mimeTypes: ["image/jpeg", "image/png"] } price as "Price": { type: "money" } } allow create: $admin.canCreate("$this") allow get: $post.current.data.status == "published" || $admin.canRead("$this") allow list(limit=24, max=100): true allow edit: $admin.canEdit("$this") allow delete: $admin.canDelete("$this") } ``` FRL declarations: - `fn name(params) { ... }` for helpers - `post name { ... }` for records - `user { ... }` for custom user data and create, edit, delete, or sign-in lifecycle hooks - `endpoint.get/post/put/patch/delete name("/path/:param") { ... }` for custom JSON routes - `payment created`, `payment confirmed`, `payment failed` for payment events - `$secret.NAME as "Label";` and `$private.name as "Label";` for admin-filled private values FRL naming: - Names start with a letter or `_` - Names then use letters, numbers, or `_` - Hyphen is not allowed because `-` is subtraction - Use `as "Readable Label"` for display labels - Aliases do not rename stored fields, endpoint names, permission paths, or FQL paths - Do not use `__proto__`, `prototype`, or `constructor` as names, object keys, or path properties - Do not declare duplicate functions, post types, endpoints, fields, object keys, user rules, payment events, or endpoint dedupe rules Schema types: - `string`, `email`, `phone`, `number`, `money`, `boolean`, `datetime`, `serverTime` - `enum` with `enums` - `object` with `fields` - `array` with `of` or `items` - `file` - `post` with `postType` - `user` - `anyOf` for a controlled union Use `money` for every monetary value, including balances, product prices, package ranges, and checkout amounts. Money amounts are exact decimal strings in a multi-currency `values` map. Read and calculate them with `money.getAmount`, `money.getCurrency`, `money.format`, `money.add`, `money.subtract`, `money.compare`, `money.convertTo`, and `money.allocate` in FRL. In FQL, register theme functions such as `moneyAmount` and call the corresponding `Frontbacked.money.*` helper inside them. Monetary values do not belong in editable `$settings`; keep them on typed posts instead. Schema rules include `required`, `default`, `min`, `max`, `pattern`, `enums`, `minKeys`, `maxKeys`, `maxDepth`, `maxChars`, `unique`, `maxSize`, `mimeTypes`, `maxDurationSeconds`, `postType`, `exists`, `validateExists`, `visibility`, `immutable`, `editableIf`, and `includeInPostSummary`. Use `includeInPostSummary: true` on up to four suitable scalar or user-reference fields to make referenced posts easy to recognize in admin pickers. `pattern` accepts either a regular-expression string or `{ segments: [{ pattern, name?, placeholder? }, ...], separator }` for structured values. Common rule variables: - `$post.current.id`, `$post.current.data` - `$post.pending.id`, `$post.pending.data` - `$actor` - `$admin` - `$request` - `$secret` - `$private` - `$page` - `$currency` - `$this` - `$context` - `$local` - `$payment` - `$action` Common FRL helpers: - `must(expression, message?, status?)` - `mustNot(expression, message?, status?)` - `str.lower`, `str.upper`, `str.trim`, `str.includes`, `str.replace`, `str.join`, `str.slug`, `str.id` (`str.id` is a compatibility alias; prefer `random.id`) - `arr.size`, `arr.map`, `arr.join` - `math.min`, `math.max` - `time.now`, `time.add` - `random.id(length?)`, `random.uuid()`, `random.int(min, max)`, `random.number(min?, max?)`, `random.string(lengthOrOptions?, options?)` - `crypto.hash(algorithm, value, options?)`, `crypto.sha256(value, options?)`, `crypto.sha384(value, options?)`, `crypto.sha512(value, options?)`, `crypto.hmac(algorithm, secret, payload, options?)`, `crypto.safeEqual(left, right, options?)`, `crypto.verifyHmac(options)` - `posts.findOne(args)`, `posts.findAll(args)`, `posts.count(args)` - `post.createAsActor(type, data)`, `post.createAsSystem(type, data)`, `post.edit(id, data)`, `post.delete(id)`, `post.increment(id, data)` - `http.get(url, options?)`, `http.post(url, options?)`, `http.put(url, options?)`, `http.patch(url, options?)`, `http.delete(url, options?)`, `http.request(method, url, options?)` - `chargeUser(args)` - `console.log(...values)` Endpoint example: ```frl endpoint.post joinWaitlist("/waitlist") as "Join Waitlist" { must($request.body.email, "Email is required", 422); dedupe `waitlist:${str.lower(str.trim($request.body.email))}`; post.createAsSystem("leads", { email: $request.body.email, name: $request.body.name }); return { ok: true, message: "You are on the list." }; } ``` Theme JavaScript calls it by name: ```js const result = await Frontbacked.endpoints.call("joinWaitlist", { allowGuest: true, body: { email: "ada@example.com", name: "Ada" } }); ``` ## Prefer - Keep page FQL close to the UI it powers. - Keep FRL schemas explicit; unknown fields should not become accepted data by accident. - Use `allow list(limit=..., max=...)` for every listable post type. - Use `$post.current` for edit/delete ownership checks. - Use `immutable` for ids, owner fields, slugs, and external references that should not change. - Use `editableIf` for workflow fields such as status, role, approval, or featured flags. - Use endpoints for custom request/response shapes, calculators, webhook-style callbacks, and repeat-safe action buttons. - Use `dedupe` whenever retries or double-clicks could duplicate work. - Use `Frontbacked.state.check()` before writes so visitors get fast, helpful validation. ## Avoid - Do not invent FQL attributes, FRL keywords, helper names, or method names not listed in the docs. - Do not expose private values in FQL or page JavaScript; use `$secret` or `$private` inside FRL. - Do not trust client-submitted owner ids, status values, prices, or roles without FRL rules. - Do not use hyphenated FRL names; use snake_case names and `as` aliases for readable labels. - Do not call an FRL endpoint with a raw route from theme JavaScript when `Frontbacked.endpoints.call()` can call it by name. - Do not use random dedupe keys; use stable values tied to the real action. FRL rejects `random.*` in `allow` rules, endpoint guards, list guards, and `dedupe` keys.