Forms — Technical Reference
This page covers the data models, security pipelines, and integration mechanics behind the Forms module.
Data Models
forms
Stores configuration metadata and access keys for lead capture forms.
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | Tenant isolation — FK → workspaces.id (ON DELETE CASCADE) |
name | text | Display name of the form |
allowed_domains | text[] | Whitelisted browser origin hostnames (empty = open mode) |
api_key | text | Secret hash for server-to-server auth |
success_redirect_url | text | Optional redirect target after native form post |
captcha_required | boolean | Flag to enforce bot verification (default false) |
require_verification | boolean | Flag to enforce email verification for existing contacts (default true) |
created_at | timestamptz | Record creation timestamp |
updated_at | timestamptz | Last updated timestamp |
form_submissions
Logs verified form submissions and payloads.
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | Tenant isolation — FK → workspaces.id (ON DELETE CASCADE) |
form_id | uuid | Origin form — FK → forms.id (ON DELETE CASCADE) |
contact_id | uuid | Created or matched contact — FK → contacts.id (ON DELETE CASCADE) |
payload | jsonb | Complete submitted request body |
ip_address | inet | IP address of the submitter |
submission_type | text | Submission method: 'direct', 'verified_email', 'api_key' (default 'direct') |
created_at | timestamptz | Record creation timestamp |
pending_form_submissions
Staging table used to hold payloads for existing contacts awaiting email verification.
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | Tenant isolation — FK → workspaces.id (ON DELETE CASCADE) |
form_id | uuid | Origin form — FK → forms.id (ON DELETE CASCADE) |
email | text | Contact email address |
payload | jsonb | Staged request body |
token | text | Unique verification hash |
expires_at | timestamptz | Token expiration timestamp (default: 24 hours from creation) |
created_at | timestamptz | Staging record creation timestamp |
The Form Submission Pipeline
Browser-based submissions route to:
POST https://app.gordoncrm.com/api/forms/{FORM_ID}Every submission passes through security checks in sequence:
Request Received
→ 1. Form ID Lookup
→ 2. Rate Limiting Check (Fail-Open)
→ 3. Origin / API Key Authentication
→ 4. Request Body Parsing (JSON/Urlencoded)
→ 5. Honeypot check (website_url)
→ 6. Email Presence Validation
→ 7. Double Opt-In Verification Check
→ Success Response1. Origin and API Key Authentication
The API supports two authentication modes depending on the client environment:
- Browser-Side (Domain Whitelist): Matches the incoming
OriginorRefererheaders against the form'sallowed_domainsarray. Wildcards are supported (e.g.,*.domain.commatchessub.domain.com). Ifallowed_domainsis empty (Open Mode), all origins are accepted. - Server-Side (API Key): Incoming server requests skip origin checks if they authenticate with the form's raw secret key. This key can be supplied via the
x-api-keyrequest header or theapi_keyURL query parameter.
2. Honeypot Validation
To block automated browser spambots:
- Browser-facing forms contain a hidden input named
website_url. - If the submission includes a non-empty
website_urlparameter, the API halts processing immediately and returns a200 OKJSON response ({"success": true}). This tricks bots into recording a successful submit while discarding the spam payload.
3. Rate Limiting
The system limits submission rates to 5 requests per 60 seconds per form per IP:
- Rate limit state is tracked in a database table.
- Evaluation runs via a PostgreSQL RPC function.
- If the RPC encounters an error, the rate limiter fails open, allowing the request to proceed rather than blocking legitimate traffic.
4. Verification Settings & Contact Merging
To prevent email impersonation attacks where unauthorized third parties overwrite profile details, the system applies context-aware verification rules depending on the form's require_verification setting:
A. Verification Required (Default)
When a form is submitted using an email address that already exists in the workspace:
- Staging: The payload is stored in
pending_form_submissionswith a cryptographic token. - Opt-In Link: The API dispatches a verification email containing a link to:
GET https://app.gordoncrm.com/api/forms/verify?token={TOKEN} - Execution: Clicking the link updates the contact, sets the submission type to
verified_email, and fires any linked automations. Pending records expire automatically after 24 hours.
B. Verification Bypassed (Frictionless Mode & API Key Submissions)
Double opt-in verification is bypassed if the form has require_verification = false or if the submission is authenticated via an API key (setting submission type to api_key).
- Non-Destructive Merge: The system updates the existing contact record instantly, but to protect data integrity, it will only write to empty fields. It will never overwrite existing PII (first name, last name, phone number).
- Consent Overwrites: Marketing subscription preferences (
is_subscribedandopt_in_source) always overwrite existing contact values to ensure consent updates are accurately registered. - Race Condition Safety: If the pipeline attempts to create a new contact but fails due to a Postgres unique violation error (
code: 23505) because the contact was created concurrently, the system catches the exception and recovers by executing the same non-destructive merge logic on the conflicting contact. - Timeline Notes: If the submission includes a custom message or note (provided via the
notesormessagepayload keys), the pipeline creates a text note and appends it to the contact's timeline feed. Redundant payload dumps are omitted to avoid cluttering the timeline notes.
Subscription Consent Engine
When the payload contains a truthy is_subscribed parameter (true, "true", or "on"), the engine updates consent parameters on the contact record:
- Sets
is_subscribedtotrueand records the current timestamp asopt_in_timestamp. - Saves the submitting IP as
opt_in_ipand the form ID asopt_in_source. - Clears any existing
unsubscribed_attimestamp. - Clears non-complaint suppressions (such as past bounces or manual suppressions), allowing future marketing deliveries.
- Spam complaint suppressions are never cleared by a form opt-in, protecting your domain's sending reputation.
CORS & Success Responses
CORS Headers
The API responds with dynamic CORS headers based on the form's whitelisted domains:
- If whitelisted domains exist, the
Access-Control-Allow-Originheader matches the requestingOriginonly if it is found inallowed_domains. - If the form is in Open Mode (empty whitelist), the header echoes the requesting
Originback unconditionally.
Success Redirects
Upon successful processing, responses depend on the form configuration:
| Setting | Response |
|---|---|
success_redirect_url configured | 302 Redirect to the target URL. |
success_redirect_url empty | 200 OK with JSON: {"success": true, "contact_id": "uuid"}. |
Performance & Hydration Optimization
Server/Client Page Split & Lazy Loading
- Server Component (
pages/forms/page.tsx): Pre-fetches page 1 forms to prevent page hydration waterfalls. - Client Component (
forms-client.tsx): Coordinates forms table listings, searches, and pagination. - Dynamic imports: The form creation dialogue (
CreateFormDialog) is extracted and lazily loaded on click withssr: false. - Sync Gates: Features
isFirstQueryRefmount guards andinitialWorkspaceIdRefworkspace sync gates.
Row-Level Security (RLS)
All tables enforce tenant-level isolation using workspace scoping.
| Table | SELECT | INSERT | UPDATE | DELETE |
|---|---|---|---|---|
forms | Workspace members | Admin+ | Admin+ | Admin+ |
form_submissions | Workspace members | System API | N/A | Admin+ |
pending_form_submissions | Workspace members | System API | N/A | Workspace members |