Anti-Spam, Attribution & Analytics
A public form is a public write endpoint. Left open it collects bot submissions, and the useful ones drown. Two small blocks handle that class of concern: antiSpam keeps junk out, and analytics decides whether the form's submissions feed aggregate reporting at all. Attribution — who submitted — needs no configuration 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 |
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.
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.
Anti-spam is on by default, and a partial block does not turn it off. Omitting antiSpam entirely still renders the honeypot and still enforces 10 submissions per IP per minute. Writing antiSpam: { rateLimit: { perIp: 5, windowSeconds: 60 } } does not replace the honeypot default — it adds to it. To genuinely disable the trap you must say so: honeypot: false. Test forms that post repeatedly from one address are the usual way this surprises people.
Submitter Attribution
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.
There is no configuration for "one submission per person". To enforce that, put a unique constraint on the bound table's column — the database is the only place a uniqueness rule cannot be raced.
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.
Related Pages
- 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 September 1, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.