How to Build a Self-Hosted Typeform & Tally Replacement with Your AI Coding Agent
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).
Live Blueprint Demo // Conversational Survey Runtime & Conditional Logic Jumps
1280×720 • Port 3000
Forward-only DAG active: If satisfaction == "unsatisfied" → Jump to what_went_wrong. Includes publish snapshots in form_versions and SSRF-hardened webhooks.
npm run db:up && npm run db:migrate && npm run db:seed && npm run dev
Playwright E2E + axe-core Verified
npm run db:up && npm run db:migrate && npm run db:seed && npm run dev
1. Why Blueprint v2 Succeeds Where Naive Prompts Fail
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.
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.
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).
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)
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.
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.
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.
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.
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.
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.
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.
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
Time Tracker & Invoicing Engine →
UTC clock drift recovery & sequential invoice locks.
Replaces Calendly & Cal.comMulti-Timezone Booking Engine →
PostgreSQL advisory locks & RFC 5545 .ics calendar invites.
Replaces Mermaid.ai & Eraser.ioVisual Flowchart Canvas →
Sandboxed Web Worker rendering & 300 DPI PNG/SVG export.