The Problem
HeyForm addresses the pain of building conversational forms without custom UI work. Teams needing surveys, quizzes, or polls typically face three problems: limited input types, fragile conditional logic, and integrations that require custom glue code. The project claims to solve this with a form builder that supports versatile inputs, smart conditional logic, and webhook/analytics integrations—but the codebase shows structural debts that affect long-term maintainability.
What This Does
HeyForm is a full-stack form builder split across seven packages under packages/: answer-utils (form submission utils), embed (JavaScript library for embedding forms), form-renderer (form rendering library with 60+ block components), shared-types-enums, utils (common utilities), server (NestJS server), and webapp (React webapp).
Execution starts at packages/server/src/main.ts:18 (bootstrap), which reaches 70 functions and is called from one place. The form renderer lives in packages/form-renderer/—its src/utils/index.ts is a hub module: 53 other modules import it, it imports 7, and it participates in a circular dependency cycle. The form-renderer also contains outsized files: packages/form-renderer/src/consts/country.ts (948 lines), packages/form-renderer/src/consts/date.ts, and packages/server/src/common/graphql/form.graphql.ts. Conditional logic and validation lives in packages/answer-utils/—files like fields-to-validate-rules.ts have 35 branch points over 91 lines, and six files contain empty catch {} blocks that silently discard errors. The embed package at packages/embed/src/index.ts exports closePopup, openPopup, and toggleModal, each reaching 34–40 internal functions but called by nothing else in the repo—effectively dead internal entries.
How It Is Wired
The import graph resolves 713 of 713 code files (TypeScript 700, JavaScript 13), with 1116 import edges and 12 modules in circular dependencies. The most connected module is packages/server/src/resolver/index: 1 module imports it, it imports 83, giving it instability 0.99. A high-blast-radius hub is packages/form-renderer/src/utils/index.ts: 53 modules depend on it, instability 0.12, and it's part of a 12-module import cycle involving packages/form-renderer/src/utils/index.ts, packages/form-renderer/src/store.ts, and packages/form-renderer/src/components/index.ts.
Traced external effects from entry points:
bootstrap→loadRuntimeConfig→fetch→list[network viaaxios.get]closePopup→close→closeModal→set[db viathis.redis.set]openPopup→open[filesystem viainstance.open]toggleModal→toggle[filesystem viathis.open]
Representative call graph edges: useTranslation called from 135 places; isValid from 61; isEmpty from 53; create from 34; User from 32. The packages/webapp/src/utils/hook.ts defines useQuery, useParam, getRouterURL, useRouter, useFormState and reads/writes files. packages/server/src/service/app.service.ts defines findAll, findById, AppService and reads/writes a database. packages/utils/src/helper.ts defines isBoolean, isString, isNumber, isValidArray, isNan and is called from 97 other files.
How To Use It
The README documents two paths: the cloud service at my.heyform.net or self-hosting. No explicit npm install or pnpm build commands are evidenced in the repo structure beyond standard package.json files; the root package.json and each package's package.json would define scripts, but their scripts fields are not detailed in the analysis. Environment variables or config files are not mapped to specific files in the measured data—these would reside where the bootstrap function or loadRuntimeConfig references them, but the analysis does not name them. A Dockerfile and docker-compose.test.yml exist at root, indicating containerized deployment is supported, but exact docker run or docker-compose commands are not provided in the analyzed data.
Real-World Use
A product team wants to collect customer feedback via a survey embedded in their web app. They use packages/embed to load the form JavaScript, configure it with openPopup or toggleModal, and pipe submitted answers to packages/server/src/service/app.service.ts via the database. Conditional logic in the form renderer (packages/form-renderer/src/blocks/) shows if/else branching that could be expressed with the packages/answer-utils condition-validation rules. When the form submits, bootstrap fetches runtime config from an external endpoint via axios.get, then answers flow through packages/server/src/resolver/index to storage. The team would hit the circular-import cycle if they tried to add a new field type that imports from both form-renderer/src/utils/index.ts and form-renderer/src/store.ts without extracting shared types first.
Code Health & Issues
- HIGH - Pin third-party GitHub Actions to a commit SHA:
.github/workflowsusedocker/login-action@v3,docker/metadata-action@v5,docker/setup-qemu-action@v3,docker/setup-buildx-action@v3; tags can move, risking secret exposure. - HIGH - Make CI invoke the test suite: 59 test files exist but no
testcommand appears in any workflow; green checks may never execute assertions. - MEDIUM - Declare least-privilege permissions for GITHUB_TOKEN: 1 workflow declares no permissions; the token inherits repository defaults, allowing injected steps to push or mint releases.
- MEDIUM - Enable Dependabot or Renovate: 8 manifests have no update bot configured; published advisories go unpatched.
- MEDIUM - Pin container base image by digest: Dockerfile uses
node:18.20.0-alpine3.19without a digest; mutable tags change the libc and CVEs between builds. - MEDIUM - Gate pull requests on dependency vulnerability scan: no dependency scan in CI; a known-vulnerable package could reach production.
- MEDIUM - Add non-root USER to the image: Dockerfile has no
USERdirective; process runs as root against every mounted volume. - MEDIUM - Review install lifecycle script and disable scripts in CI:
postinstallinpackage.jsoncould fetch binaries at build time; CI should either setignore-scriptsor run the work explicitly. - LOW - Set
timeout-minuteson workflow jobs: 1 workflow has no job timeout, risking overlap on a two-hourly schedule.
The Bottom Line
HeyForm is a functional form-builder framework with a clear feature set and a working self-hosting path via Docker. The codebase is large and interconnected—form-renderer/src/utils/index.ts is a hub with 53 dependents, and 12 modules live in circular import cycles that will slow any feature addition involving the form renderer or store. The CI gap (no test command in workflows) and mutable GitHub Action pins are the most urgent risks; they undermine confidence in the green checks. Teams that need a quick-to-deploy form solution with basic conditional logic and webhook integration will find value here, but engineers should expect to refactor the form-renderer dependency graph and shore up the CI/CD hygiene before running this in production.