Skip to main content
View as Markdown

Anti-Spam, Attribution & Analytics

A public form is a public write endpoint. Left open it collects bot submissions, and the useful ones drown. Three small blocks handle that class of concern: antiSpam keeps junk out, submitter governs how one person interacts with a form over time, and analytics decides whether the form's submissions feed aggregate reporting at all.

forms:
  - id: 1
    name: contact
    title: Contact Sales
    submitTo: { table: leads }
    antiSpam:
      honeypot: true
      rateLimit: { perIp: 5, perForm: 500, windowSeconds: 60 }
    analytics:
      enabled: false
    fields:
      - { kind: table-field, column: email, required: true }

Anti-Spam Properties

Every property is optional, and every default is protective — a form with no antiSpam block is already defended.

Property Description Default
honeypot Render a hidden decoy input. A submission that fills it is classified as spam. true
rateLimit.perIp Submissions accepted from one IP per window. 10
rateLimit.perForm Submissions accepted for the whole form, across all IPs, per window. 1000
rateLimit.windowSeconds Length of the rolling window, in seconds. 60
captcha Name of a connections[] entry providing CAPTCHA verification. None

The honeypot is a conventionally-named hidden input carrying every marker that keeps a human out of it — aria-hidden, tabindex="-1", autocomplete="off", and no visible box. A person never sees it or tabs into it; a naive bot that fills every input in the document trips it immediately.

captcha is accepted by the schema and reserved for a connection-backed verifier. Naming a connection that does not exist produces a startup warning, not a boot failure.

How a Rejection Looks

Trigger Response Ledger status / status_reason
Honeypot filled 400 { error: 'invalid request' } spam / honeypot
perIp exceeded 429 with a Retry-After header spam / rate_limit_per_ip
perForm exceeded 429 with a Retry-After header spam / rate_limit_per_form

The client is told nothing specific — a bot that learns why it was refused learns how to get past. The reason is recorded on the ledger row instead, where a moderator can review it. A spam-flagged submission never writes to the bound table, never fires the submitTo automation, and never counts against availability.maxSubmissions.

The submitter's IP is stored only as a salted SHA-256 hash. The raw address is never persisted.

Submitter Options

submitter collects the rules about the same person meeting the same form more than once.

Property Description Default
saveAndResume Let submitters save partial progress and return to it later. false
allowEditAfterSubmit Let submitters change their answers after submitting. false
requireUniqueSubmission Reject a second submission from a submitter who has already answered. false

These three are accepted and validated by the schema; the submission pipeline does not enforce them yet. Do not rely on requireUniqueSubmission for correctness today — enforce uniqueness with a unique constraint on the bound table's column.

Attribution itself does work: when a session is present the ledger records the submitter's user id and a bound table's created-by column is populated. See Access Control.

Analytics Opt-Out

Every successful submission feeds the aggregate analytics stream by default, which is what makes the admin Responses surface able to chart submissions per day and drop-off per step.

Property Description Default
enabled Set false to exclude this form from aggregates and the event stream. true

Set it to false on forms carrying data that must not appear in cross-submission aggregates — health intake, payment details, anything under legal hold. Per-submission inspection in the admin console is unaffected; the opt-out removes the form from the aggregate, not from your records.

Aggregation is gated three times over: an operator-level environment switch, the app-wide analytics block, and this per-form flag. Any one of them off is enough to keep a form out.

  • Availability — the submission cap spam is excluded from.
  • Submissions — the ledger where the spam status is recorded.
  • Access Control — signed-in submissions and how they are attributed.
  • Analytics — the app-wide block this opt-out sits under.

Last updated July 27, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta — contributions and corrections are welcome.

Built with Sovrium