strapi-plugin-form-builder-cms
A Strapi plugin that lets teams create and manage forms directly inside the CMS — no custom schemas, endpoints, or admin UI required.
Overview
A production Strapi v5 plugin that turns the CMS into a full form platform: a drag-and-drop builder, a visual theming engine, a self-contained embed runtime, server-side spam protection, and a submission inbox — shipped as a single installable package.
→ https://www.npmjs.com/package/strapi-plugin-form-builder-cms
Architecture
The plugin follows the Strapi v5 plugin split — an admin bundle (React) and a server bundle (Node) — built with @strapi/sdk-plugin and published as ESM + CJS.
Data model. Two content types drive everything:
form— acollectionTypewithdraftAndPublishenabled. Fields live in ajsoncolumn (an array of typed field objects), settings in anotherjsoncolumn, and apublishedDatajsoncolumn stores an immutable snapshot captured at publish time.form-submission— one row per submission, with adatapayload, a frozenfieldssnapshot (so exports stay accurate even if the form later changes), plus IP/user-agent and a status enum (new/read/archived).
Draft / publish as snapshots. The public page, the embed, and the submit endpoint always read publishedData, never the working draft. Editing and saving a draft — fields or styles — never affects a live form; a publish overwrites the snapshot. This is the same mechanism used for both structure and theme.
The embed runtime. The server serves a self-contained IIFE from GET /embed.js (cached, zero dependencies). On the host page it reads data-form-id, fetches the form's published schema cross-origin, and mounts the form into a target <div> — rendering fields, wiring validation messages, honeypot, CAPTCHA widgets, hidden-field prefill, and submission. CORS is handled by Strapi, so the same one-line snippet works on any domain. A server-rendered hosted page and headless REST endpoints (/schema, /submit) cover the non-embed cases.
Key implementation details
Single-source theming. A theme resolves — in one pure function — to a small set of --sfb-* CSS custom properties (plus a data-sfb-fields attribute for structural variants). The same stylesheet string and resolved variables drive both the admin's live preview and the public embed, so there is no duplicated styling logic to keep in sync. Colour palettes are editable and persisted with the form; the resolved variables are baked at edit time and applied verbatim by the client.
Security at the trust boundary.
- CAPTCHA (Cloudflare Turnstile / reCAPTCHA v2) is verified server-side and fail-closed — an invalid, missing, or unreachable token rejects the submission, with a request timeout so a stalled provider can't hang the submit.
- The CAPTCHA secret key is stripped from the public schema — only the public site key reaches the browser.
- The post-submit redirect URL is validated to
http(s)/relative only, so ajavascript:/data:value can never execute (no open-redirect/XSS). - The public page relaxes its CSP just enough to load the chosen CAPTCHA provider — only when one is configured.
Validation & anti-spam. A dedicated validation service runs the same rule set the builder configures (required, min/max length, min/max value, email, URL, regex, field matching) against the submitted payload; honeypot and per-IP hourly rate limiting run before persistence.
Engineering decisions & trade-offs
- JSON schema over per-field content types — keeps a form a single editable entity and avoids a content-type explosion, at the cost of hand-written validation instead of Strapi's model layer.
- Baked theme variables vs re-resolving on read — the resolved
--sfb-*values are stored with the form (like the publish snapshot), so the embed stays a dumb consumer; the trade-off is that changing a preset definition doesn't retroactively re-theme saved forms. - Light DOM embed, not shadow DOM — the form is stylable from the host site (CSS variables + documented
.sfb-*classes + per-field CSS hooks) at the cost of full isolation; an iframe path is reserved for pixel-perfect isolation. - Shared CSS across admin + server — one stylesheet imported by both bundles, deliberately avoiding the common "keep in sync" duplication between preview and runtime.
Testing & release
- Unit tests (Vitest) target the security and correctness paths: CAPTCHA verification (fail-closed), public-schema secret-key redaction, validation rules, theme resolution, and the tour tooltip placement.
- CI runs type-checks (server + admin), the test suite, a build, and a publish dry-run on every push and PR.
- Releases are version-gated and published to dist-tags (
alpha/beta/rc/latest) frommain, with tags and GitHub releases generated automatically.
Tech stack
- Strapi v5 · Node.js · TypeScript
- React + Strapi Design System (admin)
@dnd-kit(drag-and-drop), vanilla IIFE embed runtime- Vitest, GitHub Actions CI/CD