Skip to content

External references & submission policy

Forms-Engine never authenticates respondents. When you need to connect a response to your notion of who filled it in, you pass an external reference — an opaque string your application vouches for:

<forms-engine
public-id="q_a1b2c3d4e5"
api-base="https://forms.your-domain.com"
external-ref="user-4821">
</forms-engine>

A reference is 1–128 characters, captured once at mount (mid-session changes are ignored), stored on the response, filterable in the responses browser, and a column in CSV export. It can be a user id, an order number, an invite code — whatever joins responses to your records.

Per questionnaire (editor → Embed panel) you choose how repeat submissions from the same reference are handled:

  • Multiple (default) — the ref is stored for correlation only; submit as often as you like.
  • One per reference — one completed response per reference. Creating a new session or completing one is rejected once a completed same-ref response exists; the embed then shows an “already submitted” state and emits fe-already-submitted. In-progress responses never block anything — an abandoned draft can’t lock someone out. Completion is enforced atomically at the database level, so two racing sessions can’t both complete.

Under one-per-reference, embedding without an external-ref renders a loud configuration error by design — a missing reference should fail on your first test, not silently accept anonymous submissions.

On mount with a reference, the embed checks GET /public/v1/questionnaires/{publicId}/ref-status?ref=…, which returns only {"status":"NONE"} or {"status":"COMPLETED"} — never a response id, answers, or an in-progress signal — so a guessable reference (a sequential user id, an email) can never be escalated into reading someone’s answers. Under the Multiple policy the status is passed through to your page on fe-loaded as refStatus, letting you build your own “you already did this” UX without the hard block.

⚠️ What this honestly is — and isn’t

Section titled “⚠️ What this honestly is — and isn’t”

One-per-reference is honor-system deduplication. The reference comes from your page’s markup; anyone who opens devtools can change it. It deduplicates cooperative users — logged-in users of your own app, invitees pasting their code — and that covers most real use. It is not suitable for contests, votes, or any adversarial setting until signed references exist (they’re on the roadmap).

Two further consequences to design around:

  1. ref-status necessarily reveals whether a given reference has completed to anyone who queries it. It’s rate-limited separately and more strictly than other endpoints, but the boolean itself is the feature. Where “did user X respond?” is sensitive, pass opaque random tokens rather than meaningful ids.
  2. Cross-device resume deliberately doesn’t exist. A ref alone must never unlock a session — otherwise guessing a reference would read someone’s in-progress answers — so resume stays same-device (sessionStorage) until signed references ship.