A form-making and sharing application. Project-organized, conditionally-routed, properly analyzed.
Built by Hemang · developer.coehemang.dev
For a tour of the code itself, jump to DEVELOPMENT.md.
- Editor: drag-and-drop form builder with live preview, branching rules, multi-page flows, themed presentation, density and typography presets.
- Public form runtime: anonymous and signed-in submissions, file uploads, password gating, response caps, close dates, geolocation with consent.
- Owner surfaces: per-form responses list with filters, per-question analytics, response export, webhook subscriptions, personal access tokens.
- Anti-abuse: per-IP, per-(form, IP), per-token rate limits, honeypot, timing checks, payload size and nesting caps, file MIME and magic-byte validation, SSRF guard on webhook delivery.
React + JSX on Vite, Express + JSX on Node ESM, Tailwind CSS v4, shadcn/ui, Framer Motion, Prisma (SQLite in dev, Postgres in prod), pnpm workspaces. No TypeScript.
pnpm install
cp packages/db/.env.example packages/db/.env
cp apps/web/.env.example apps/web/.env
pnpm db:migrate # applies dev migrations to SQLite
pnpm dev # web on :5173, API on :4000
The API process reads packages/db/.env via Node's --env-file flag, so a
single file drives both the Prisma CLI and the API runtime. See
apps/api/.env.example for the full list of variables the API supports.
apps/
web/ React frontend (Vite). Marketing, dashboard, editor, runtime.
api/ Express backend. JSON API at /api/v1/*.
packages/
db/ Prisma schema + migrations + singleton client.
forms/ Shared validators + the conditional-rule evaluator.
ui/ shadcn-style primitives (Button, Card, Accordion).
config/ Shared ESLint config.
scripts/ Helper scripts not shipped to prod.
Ideology/ Product, design, and code-organization source-of-truth docs.
For a per-file walkthrough see DEVELOPMENT.md.
| Command | What it does |
|---|---|
pnpm dev |
Web + API in parallel. |
pnpm dev:web |
Web only. |
pnpm dev:api |
API only. |
pnpm test:api |
Run the API vitest suite (44 tests). |
pnpm lint |
Lint every workspace. |
| Command | What it does |
|---|---|
pnpm build:web |
Builds the web bundle to apps/web/dist. |
pnpm preview:web |
Serves the built bundle locally (sanity check). |
| Command | What it does |
|---|---|
pnpm build:api |
prisma generate against the prod schema. Runs in heroku-postbuild. |
pnpm release:api |
prisma migrate deploy against the prod schema. Runs in the Heroku release phase. |
pnpm start:api |
Boots the API server. What Heroku runs to start the web dyno. |
pnpm heroku-postbuild |
Alias for build:api. Heroku auto-detects this. |
| Command | What it does |
|---|---|
pnpm db:migrate |
Apply migrations to the local SQLite DB. |
pnpm db:migrate:prod |
Apply migrations to the prod (Postgres) DB. Same script the release phase uses. |
pnpm db:studio |
Open Prisma Studio against the dev DB. |
pnpm db:generate |
Regenerate the Prisma client (dev schema). |
- Connect the repo in the Cloudflare Pages dashboard.
- Build configuration:
- Build command:
pnpm install --frozen-lockfile && pnpm build:web - Build output directory:
apps/web/dist - Root directory (advanced): leave blank
- Node version: 20 or later (set
NODE_VERSION=20or use.nvmrc)
- Build command:
- Environment variables (Settings -> Environment variables):
VITE_API_BASE_URL->https://<your-api>.herokuapp.com/api/v1VITE_GOOGLE_CLIENT_ID-> your Google OAuth client ID, optional
The web bundle hits the API via cross-origin XHR with credentials: 'include'.
That requires SameSite=None cookies on the API side, see below.
- Create the app and add the Heroku Postgres add-on:
heroku create your-formdash-api heroku addons:create heroku-postgresql:essential-0 - Set env vars (everything the API reads is documented in
apps/api/.env.example):heroku config:set NODE_ENV=production heroku config:set WEB_ORIGIN=https://your-web.pages.dev heroku config:set FORMDASH_COOKIE_SAMESITE=none heroku config:set FORMDASH_STORAGE_DRIVER=s3 heroku config:set S3_BUCKET=... S3_REGION=... S3_ENDPOINT=... heroku config:set AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... heroku config:set RESEND_API_KEY=... MAIL_FROM='Formdash <auth@your.tld>' heroku config:set GOOGLE_CLIENT_ID=...DATABASE_URLis set automatically by the Heroku Postgres add-on. - Push:
Heroku runs
git push heroku mainpnpm install, thenheroku-postbuild(prisma generate), then thereleasephase from the Procfile (prisma migrate deploy), then boots thewebprocess (pnpm run start:api).
The Procfile at the repo root drives the dyno:
release: pnpm run release:api
web: pnpm run start:api
When the web and API live on different origins (the Cloudflare Pages + Heroku setup above), three things have to line up:
- API allowlist:
WEB_ORIGINmatches the Pages origin (withhttps://). - Cookie policy:
FORMDASH_COOKIE_SAMESITE=noneon the API. The session cookie is thenSameSite=None; Secure, which is what cross-origin XHR withcredentials: 'include'requires. - Web env:
VITE_API_BASE_URLpoints at the API origin including the/api/v1suffix.
If the web and API share a domain (behind the same reverse proxy, for example), all three can be left at defaults.
pnpm test:api # vitest, single pass, ~450ms
Coverage focuses on the critical paths: SSRF guard, webhook HMAC roundtrip, rate-limit math, submit honeypot tolerance, file magic-byte sniffing, and the health endpoints.
For a tour of the actual files you'll edit, see
DEVELOPMENT.md.
Hemang · developer.coehemang.dev
Open to feedback, bug reports, and feature ideas. Reach out via the portfolio site.
Private project. Not open source at this point.