Forms & Workflows Blueprint // v2.0.0 Replaces: Typeform ($29–$99/mo) • Tally Pro • Jotform Next.js App Router • Dockerized Postgres • S1–S15 Security Spec

How to Build a Self-Hosted Typeform & Tally Replacement with Your AI Coding Agent

→ View entire prompt (469 lines • .plan.md)

Paying $59–$99/month just to unlock conditional logic jumps and avoid response limits is unnecessary. Blueprint v2 provides Claude Code, Cursor, Windsurf, and Devin with a 100% locked tech stack, a zero-cloud Docker PostgreSQL setup, forward-only DAG logic branching, immutable publish version snapshots, and 15 numbered security test specs (S1–S15).

View entire prompt →
Download .plan.md ↓ Download Quickstart README ↓

Live Blueprint Demo // Conversational Survey Runtime & Conditional Logic Jumps

1280×720 • Port 3000
SURVEY ENGINE // TYPEFORM & TALLY CLONE v2 Zero Cloud Accounts Required
Question 3 of 5 • Press A–Z or Enter ↵

Forward-only DAG active: If satisfaction == "unsatisfied" → Jump to what_went_wrong. Includes publish snapshots in form_versions and SSRF-hardened webhooks.

⚡ Local Setup: npm run db:up && npm run db:migrate && npm run db:seed && npm run dev Playwright E2E + axe-core Verified
SURVEY ENGINE // TYPEFORM & TALLY CLONE v2 Zero Cloud Accounts Required • Playwright E2E + axe-core Verified
⚡ Local Setup: npm run db:up && npm run db:migrate && npm run db:seed && npm run dev

1. Why Blueprint v2 Succeeds Where Naive Prompts Fail

Invariant 01 // Forward-Only DAG

Cycle-Free Logic Branching

Every jump target must point to a question later in array order, or to "end". This turns form flows into a Directed Acyclic Graph (DAG) by construction, making infinite loops mathematically impossible.

Invariant 02 // Publish Snapshots

Immutable form_versions

Publishing copies the draft into an immutable snapshot row in form_versions. Editing drafts never alters active survey runs or corrupts historical submission interpretations.

Invariant 03 // SSRF-Hardened Webhooks

Connect-Time IP Validation

Protects against SSRF and DNS rebinding attacks via an undici Agent with ipaddr.js, rejecting loopback (127.0.0.1, ::1), private subnets (10.x, 192.168.x), and AWS metadata (169.254.169.254).

Invariant 04 // Accessible Keyboard Shortcuts

Collision-Free A–Z Choice Keys

Choice options bind to A–Z only when focus is outside text/number inputs, eliminating input collisions. Tab remains native focus navigation, satisfying WCAG 2.1.2.

2. Complete PostgreSQL Schema (DDL v2)

Multi-tenant schema with built-in auth, immutable publish versioning, HMAC-hashed IP tracking, and audit logging.

CREATE TABLE users (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email         TEXT NOT NULL UNIQUE CHECK (email = lower(email)),
  password_hash TEXT NOT NULL,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE sessions (
  id         CHAR(64) PRIMARY KEY,            -- sha256 hex of the cookie token
  user_id    UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  expires_at TIMESTAMPTZ NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_sessions_user ON sessions(user_id);

CREATE TABLE forms (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id          UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title             VARCHAR(255) NOT NULL,
  slug              VARCHAR(64) NOT NULL UNIQUE CHECK (slug ~ '^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$'),
  draft_schema      JSONB NOT NULL,
  theme             JSONB NOT NULL DEFAULT '{"bg":"#000000","accent":"#e53539"}',
  published_version INT,                      -- NULL = not live
  webhook_url       TEXT,
  webhook_secret    TEXT,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_forms_owner ON forms(owner_id, updated_at DESC);

CREATE TABLE form_versions (                  -- immutable snapshot taken on publish
  form_id      UUID NOT NULL REFERENCES forms(id) ON DELETE CASCADE,
  version      INT NOT NULL,
  schema       JSONB NOT NULL,
  theme        JSONB NOT NULL,
  published_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (form_id, version)
);

CREATE TABLE form_submissions (
  id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  form_id      UUID NOT NULL,
  form_version INT NOT NULL,
  answers      JSONB NOT NULL,
  ip_hmac      CHAR(64),                      -- HMAC-SHA256(IP_HASH_SECRET, ip), never a plain hash
  completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  FOREIGN KEY (form_id, form_version) REFERENCES form_versions(form_id, version) ON DELETE CASCADE
);
CREATE INDEX idx_submissions_form ON form_submissions(form_id, completed_at DESC);

CREATE TABLE webhook_deliveries (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  submission_id UUID NOT NULL REFERENCES form_submissions(id) ON DELETE CASCADE,
  status_code   INT,
  error         TEXT,
  attempted_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

3. Modular File System Architecture (Hard 200-Line Cap)

docker-compose.yml          # postgres:16, creates app + app_test databases
.env.example  DECISIONS.md  README.md
drizzle.config.ts  vitest.config.ts  playwright.config.ts
.github/workflows/ci.yml
scripts/seed.ts
src/
  proxy.ts                              # nonce CSP + /dashboard redirect only
  app/
    layout.tsx  page.tsx
    (auth)/login/page.tsx  (auth)/signup/page.tsx
    dashboard/page.tsx                  # form list + create
    dashboard/forms/[id]/edit/page.tsx
    dashboard/forms/[id]/responses/page.tsx
    f/[slug]/page.tsx                   # server: loads published version, renders player
    api/auth/{signup,login,logout}/route.ts
    api/forms/route.ts
    api/forms/[id]/route.ts
    api/forms/[id]/{publish,unpublish}/route.ts
    api/forms/[id]/submissions/route.ts
    api/submissions/route.ts
  components/
    player/  form-player.tsx  question-stage.tsx  question-input.tsx
             question-choice.tsx  progress-bar.tsx  nav-footer.tsx
    builder/ builder-shell.tsx  question-list.tsx  question-editor.tsx
             logic-editor.tsx  form-settings.tsx  publish-bar.tsx
    ui/      button.tsx  modal.tsx  field.tsx
  hooks/     use-form-runner.ts  use-player-keyboard.ts  use-draft-storage.ts
  lib/
    form/    schema.ts  logic.ts  validate-answers.ts  validate-logic.ts  runner-reducer.ts
    server/  env.ts  db.ts  auth.ts  session.ts  forms-dal.ts  submissions-dal.ts
             http.ts  rate-limit.ts  client-ip.ts
    webhooks/ ssrf-guard.ts  sign.ts  dispatch.ts
  db/        schema.ts  migrations/
tests/
  unit/  integration/  e2e/  helpers/

4. Phase-by-Phase Implementation Prompts (Phases 0–6)

Step 0 // Operating Rules & Kickoff
I have attached PromptScale's Typeform & Tally Replacement blueprint v2. Read the whole document before writing code. Follow the 8 operating rules: do not ask clarifying questions, work strictly phase by phase (Section 7), never weaken a test to make it pass, commit after each phase, stop and report if Docker/environment blocks you, never commit secrets, add no runtime dependencies without DECISIONS.md entry, and enforce the 200-line hard file limit. Begin with Phase 0.
Phase 0: Scaffold and Tooling
Execute Phase 0: Scaffold Next.js App Router with TypeScript strict mode, Docker Compose (app and app_test databases), Drizzle ORM config, env.ts validation, Vitest, Playwright, CI workflow, and npm run verify script. Definition of Done: npm run db:up && npm run verify must exit 0.
Phase 1: Schema Contract & Logic Engine (Pure, Test-First)
Execute Phase 1: Implement Section 2 in src/lib/form/ (schema.ts, logic.ts, validate-answers.ts, validate-logic.ts, runner-reducer.ts). Write unit tests first covering: operator/type matrix, first-match ordering, unanswered question semantics, fallback destinations, maxRemainingSteps, and abandoned branch answer exclusion. Definition of Done: npm run typecheck && npm run lint && npm test must exit 0.
Phase 2: Database, Auth, Data Access, and Seed
Execute Phase 2: Create Drizzle migrations for users, sessions, forms, form_versions, form_submissions, and webhook_deliveries with DEFAULT now(). Implement argon2id auth (S1, S2), rate-limiting (S5), body size caps (S6), owner-scoped DALs, and the seed script (customer-feedback form). Definition of Done: npm run db:migrate && npm run db:seed && npm test must exit 0.
Phase 3: Forms API & Builder UI
Execute Phase 3: Build the form routes from Section 4 and the dashboard pages. Implement @dnd-kit question reordering, question editor, logic jump modal (forward-only destinations), theme picker, debounced autosave, publish version snapshotting into form_versions, and S3 authorization tests. Definition of Done: npm test && npm run build must exit 0.
Phase 4: Respondent Player
Execute Phase 4: Implement /f/[slug] player loading the published version snapshot. Implement keyboard navigation: Enter to advance, Shift+Enter for newlines in long_text, A-Z for choices outside text inputs, native Tab focus, aria-live question announcements, and sessionStorage draft storage. Definition of Done: npm run typecheck && npm run lint && npm test && npm run build must exit 0.
Phase 5: Submissions Pipeline, Webhooks & Responses View
Execute Phase 5: Implement POST /api/submissions with server-side logic path replay (S7), honeypot, and 409 stale-version handling. Implement ssrf-guard.ts with ipaddr.js connect-time unicast verification (S9), HMAC-SHA256 signing (S11), and the paginated responses table. Definition of Done: npm test && npm run build must exit 0.
Phase 6: End-to-End & Acceptance Verification
Execute Phase 6: Write Playwright E2E tests verifying: Path A keyboard-only, Path B logic diversion, branch changing with answer pruning, draft recovery on reload, reduced motion transitions, axe-core a11y scans, and CSP headers. Complete Section 8 Acceptance Checklist. Definition of Done: npm run verify must pass from a clean clone.

Frequently Asked Questions: Building a Typeform & Tally Replacement

How can I build a Typeform or Tally replacement using AI coding tools without cloud accounts?

Use PromptScale's Typeform & Tally Replacement Blueprint v2 in Planning Mode inside Claude Code, Cursor, or Windsurf. It runs on Docker Compose PostgreSQL (with isolated app and app_test databases) requiring zero third-party accounts, with an airtight Zod schema contract and a 200-line file cap.

Why is publish versioning required in a conversational form engine?

If forms are edited directly in-place, changing questions or logic rules breaks the meaning of previous submissions. PromptScale v2 takes an immutable snapshot in a form_versions table whenever a form is published, and submissions always link to the exact version snapshot the respondent saw.

How do you protect outgoing webhooks against SSRF in a self-hosted form builder?

Validate the webhook destination at save time and connect time via an undici Agent using ipaddr.js to ensure the resolved IP address is unicast. This blocks internal loopback (127.0.0.1, ::1), private RFC 1918 subnets (10.x, 192.168.x), AWS metadata (169.254.169.254), and prevents DNS rebinding.

Explore More SaaS Replacement Blueprints

Copied to Clipboard!