Skip to content

Forms Public Submissions

Public form submissions are anonymous by default and use a separate abuse protection path from authenticated app routes.

  • IP addresses are never stored in plaintext. The submission path stores only a salted ipHash in form_responses.respondent_meta.
  • userAgent and ipHash are retained for 90 days for abuse investigation.
  • form_respondent_meta_retention_cleanup runs daily and removes ipHash and userAgent from responses older than 90 days, preserving the response body and adding retentionRedactedAt.
  • Cloudflare Turnstile verification is always part of public submission. The web container must receive the browser-visible WEB_PUBLIC_TURNSTILE_SITE_KEY runtime key; the same environment-neutral image renders the widget with the staging or production value from its HTML bootstrap, sends the single-use token, and resets the widget after every submit attempt. Production requires CLOUDFLARE_TURNSTILE_SECRET_KEY or TURNSTILE_SECRET_KEY at startup. Staging without either credential logs a deployment warning and rejects submissions with 503 FORMS_CAPTCHA_UNAVAILABLE; local tests inject a fake verifier at the verification seam rather than bypassing captcha behavior.

Forms use conservative defaults and allow environment overrides:

  • FORMS_MAX_PER_USER: active forms per owner, default 50.
  • FORMS_MAX_FIELDS_PER_FORM: active fields per form, default 100.
  • FORMS_MAX_RESPONSES_PER_FORM: hard cap per form, default 10000.
  • FORMS_PUBLIC_RESPONSE_BODY_LIMIT_BYTES: public submission body limit, default 65536.

Display-only image fields count toward the active field limit. They must have one managed imageAssetId, cannot be required, never accept an answer, and are omitted from response CSV, Workbook, and response-PDF columns. A form may also have one managed cover image. Blank-form PDF export embeds the cover and display images from server-read bytes.

Public form publishing is part of the normal form permission model and is not controlled by a deployment or build flag. Only the owner can publish, close, reopen, or rotate the public token. A form is anonymously readable only while it is published, unarchived, within its optional open/close schedule, and below its response cap. Closing, archiving, or rotating the token invalidates the old public entry point.

Immutable publication and response meaning

Section titled “Immutable publication and response meaning”

The normalized Form and field rows are an editable draft. Publish creates an immutable form_revisions definition and atomically advances forms.published_revision_id. Public GET and answer validation read that revision, so editing the draft after publication does not change the live responder contract.

Every accepted response stores the exact published form_revision_id in the same transaction as validation and response-count advancement. Response detail, CSV, Workbook, PDF, analytics, and field-label resolution prefer that bound definition. respondent_meta.fieldSnapshot remains a compatibility fallback for legacy-incomplete evidence and is not removed by this rollout.

Republish creates a new immutable revision. Closing/reopening does not rewrite it. Restoring an older revision changes only the draft and keeps the current published revision live until an explicit Publish; no restore rewrites an existing response. See Office version history for backfill and retention semantics.

Cover and display-image IDs are part of the immutable definition hash. Each revision also pins its referenced assets in form_revision_image_assets, so replacing a draft image cannot break the current public form or a historical revision. Anonymous image delivery uses the public image-asset route but grants access only when the asset belongs to the form scope, appears in the current published revision, and the form passes the same publication, archive, schedule, and response-cap checks as the public schema endpoint.

Authenticated editors use GET /api/forms/{id}/preview?revision=draft|published|<revision UUID>. The response has the public schema shape plus bounded preview metadata. The Web route /office/forms/{id}/preview and the public page share the same respondent renderer. Preview submission is local-only, never inserts form_responses, and does not increment public request or rejection metrics. Legacy-incomplete response revisions are rejected rather than rendered as a misleading form.

Public forms expose bounded-label Prometheus counters for launch dashboards and alerts:

  • arinova_public_form_requests_total{endpoint,outcome} tracks schema and submit traffic for QPS and success/reject/discard ratios.
  • arinova_public_form_rejects_total{reason} tracks rejection reasons without form IDs or user identifiers.
  • arinova_public_form_honeypot_hits_total tracks silent honeypot discards.
  • arinova_public_form_response_rate_alerts_total increments when a single form crosses the per-minute abnormal response-rate threshold; the server also emits a public_form_response_rate_alert warning with the form ID for incident triage.

Build a7f47a5ca54ddcf7806cd48b81ce1b9827042766