Docs

Pages, sections, field types, routing, themes, attachments.

Forms Builder

The builder lives at /admin/forms. Every action the UI offers maps one-to-one onto the server contract — there's no "save, then hope" step. Here's the feature surface.

Field types

TypeNotes
text, textareamax_length honored client + server
numbermin / max enforced; optional step
emailRFC 5322 light validation
dateISO-8601 strings
checkboxBoolean
select, radiooptions: string[]
multi_selectArray of enum values
fileUp to 20 MiB, scanned server-side (see below)
heading, section, staticLayout only — not validated

Pages + sections

The builder's left rail shows pages; each page has one or more sections; each section owns an ordered list of field keys. Moving a field between pages is a one-click drag. Single-page forms render without a stepper — the public renderer short-circuits when pages.length === 1.

Conditional routing

Open a page's Routing panel to add rules:

  • Simple mode{field} {op} {value} → goto page. Covers most cases.
  • Advanced mode — raw JSONLogic for complex conditions. The save path validates every rule: no dangling goto_page_id, no unknown field refs, no cycles, no unreachable pages.

goto_page_id: "__end__" short-circuits to the submit state — use it for branches that skip the remaining pages.

File attachments

Drag in a File upload field and configure:

  • accepted_mime (e.g. image/*, application/pdf)
  • max_size in MiB (capped at the server's 20 MiB hard limit)
  • multiple — whether the field accepts an array

The public renderer uploads files before the submit POST. Each upload lands in form_submission_attachments with scan_status = 'pending'. The three-layer scanner (MIME magic → VT hash → LLM content) runs asynchronously; the renderer polls until scan_status = 'clean' or displays the rejection reason. The submit handler then claims all referenced attachments atomically in one transaction with the submission INSERT.

Themes + branding

The Theme panel controls form-level appearance:

  • Pick any tenant theme (built-in or custom) or leave blank to use the tenant's active brand.
  • Upload a logo + hero image (sanitized via the entity-file scanner) — both are served at /api/v1/public/form-assets/:id with 1h cache.
  • Title / subtitle overrides render in the form header.

Publishing

A form in draft status is visible in the admin but has no public endpoints. Clicking Publish flips the status and creates an immutable version row. You can always edit the draft, but the public invite tokens continue to serve the last published version until you re-publish.