Technical Reference
Forms

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.

ColumnTypeDescription
iduuidPrimary key
workspace_iduuidTenant isolation — FK → workspaces.id (ON DELETE CASCADE)
nametextDisplay name of the form
allowed_domainstext[]Whitelisted browser origin hostnames (empty = open mode)
api_keytextSecret hash for server-to-server auth
success_redirect_urltextOptional redirect target after native form post
captcha_requiredbooleanFlag to enforce bot verification (default false)
require_verificationbooleanFlag to enforce email verification for existing contacts (default true)
created_attimestamptzRecord creation timestamp
updated_attimestamptzLast updated timestamp

form_submissions

Logs verified form submissions and payloads.

ColumnTypeDescription
iduuidPrimary key
workspace_iduuidTenant isolation — FK → workspaces.id (ON DELETE CASCADE)
form_iduuidOrigin form — FK → forms.id (ON DELETE CASCADE)
contact_iduuidCreated or matched contact — FK → contacts.id (ON DELETE CASCADE)
payloadjsonbComplete submitted request body
ip_addressinetIP address of the submitter
submission_typetextSubmission method: 'direct', 'verified_email', 'api_key' (default 'direct')
created_attimestamptzRecord creation timestamp

pending_form_submissions

Staging table used to hold payloads for existing contacts awaiting email verification.

ColumnTypeDescription
iduuidPrimary key
workspace_iduuidTenant isolation — FK → workspaces.id (ON DELETE CASCADE)
form_iduuidOrigin form — FK → forms.id (ON DELETE CASCADE)
emailtextContact email address
payloadjsonbStaged request body
tokentextUnique verification hash
expires_attimestamptzToken expiration timestamp (default: 24 hours from creation)
created_attimestamptzStaging 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 Response

1. Origin and API Key Authentication

The API supports two authentication modes depending on the client environment:

  • Browser-Side (Domain Whitelist): Matches the incoming Origin or Referer headers against the form's allowed_domains array. Wildcards are supported (e.g., *.domain.com matches sub.domain.com). If allowed_domains is 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-key request header or the api_key URL 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_url parameter, the API halts processing immediately and returns a 200 OK JSON 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_submissions with 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_subscribed and opt_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 notes or message payload 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_subscribed to true and records the current timestamp as opt_in_timestamp.
  • Saves the submitting IP as opt_in_ip and the form ID as opt_in_source.
  • Clears any existing unsubscribed_at timestamp.
  • 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-Origin header matches the requesting Origin only if it is found in allowed_domains.
  • If the form is in Open Mode (empty whitelist), the header echoes the requesting Origin back unconditionally.

Success Redirects

Upon successful processing, responses depend on the form configuration:

SettingResponse
success_redirect_url configured302 Redirect to the target URL.
success_redirect_url empty200 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 with ssr: false.
  • Sync Gates: Features isFirstQueryRef mount guards and initialWorkspaceIdRef workspace sync gates.

Row-Level Security (RLS)

All tables enforce tenant-level isolation using workspace scoping.

TableSELECTINSERTUPDATEDELETE
formsWorkspace membersAdmin+Admin+Admin+
form_submissionsWorkspace membersSystem APIN/AAdmin+
pending_form_submissionsWorkspace membersSystem APIN/AWorkspace members