Skip to content

Origins & content security

Each questionnaire carries a list of origins allowed to embed and submit it, edited in the editor’s Embed panel (https://app.example.com — scheme

  • host, optional port, no path). An empty list allows every origin — the editor shows a warning badge so openness is always a visible choice, and the literal * is rejected as redundant.

Enforcement is server-side, per request, against the browser-sent Origin header:

  • Mutating requests (creating a session, saving answers, completing, uploading) from a disallowed origin are rejected with 403.
  • Reads return data but withhold the CORS Access-Control-Allow-Origin header, so the browser blocks the embedding page from reading it — the embed shows its generic load error.
  • Requests without an Origin header (curl, server-to-server) are allowed — origin checking is a browser-context control, not authentication. The security model is honest about this: origins keep other websites from embedding your questionnaire, nothing more.

Matching is exact and case-sensitive — https://app.example.com and https://www.app.example.com are different origins; list each one you serve from. The origin and user agent of the creating request are also stored on each response (visible in the management API) for forensics.

If your embedding page sets a CSP, the embed needs:

script-src https://unpkg.com # or your own host if you self-host the bundle
connect-src https://forms.your-domain.com # your api-base
  • script-src — wherever forms-engine.esm.js loads from. Self-hosting the bundle on your own origin (copy the file next to your page) removes the third-party entry entirely and pins the version — reasonable hardening for production.
  • connect-src — the api-base, for all runtime calls including uploads.
  • No frame-src, style-src additions, or cookies are needed: the component is not an iframe, styles live in its shadow DOM, and the API is cookie-less (the response id in sessionStorage is the only session state).

Serve the backend over HTTPS in production. Browsers block mixed content, so an embed on an https:// page cannot call an http:// api-base at all — terminate TLS in front of the backend (see Reverse proxy & exposure).