Forms as Collections

Build forms with a drag-and-drop builder. Forms are automatically stored as collections and submissions become queryable content items via the Content API.

Overview

SonicJS treats forms as first-class content. When you create a form, the system automatically generates a "shadow collection" that mirrors the form's fields. Every submission is dual-written to both the form_submissions table and the content table, making form data queryable through the standard Content API.

🎨

Form Builder

Drag-and-drop Form.io builder with 20+ field types

πŸ“¦

Collections Sync

Forms automatically create shadow collections with derived schemas

πŸ”

Content API

Query submissions using the same API as any other content

πŸ›‘οΈ

Security

XSS sanitization, Turnstile CAPTCHA, and input validation


Creating Forms

Admin UI

  1. Navigate to Admin β†’ Forms
  2. Click New Form
  3. Enter a name (machine-friendly, lowercase with underscores) and display name
  4. Click Create to open the Form Builder

The Form Builder provides a full drag-and-drop interface powered by Form.io:

  • Text fields, textareas, emails, phone numbers, URLs
  • Numbers, checkboxes, selects, radio buttons
  • Date/time pickers, file uploads, signatures
  • Layout components: panels, fieldsets, columns, tabs
  • Address fields with Google Maps integration

Form Settings

Each form supports:

  • Category β€” contact, survey, registration, or custom
  • Public/Private β€” control whether the form is publicly accessible
  • Turnstile CAPTCHA β€” per-form or global bot protection
  • Success Message β€” custom message shown after submission
  • Email Notifications β€” optional notification on submission

How It Works

Shadow Collections

When a form is created or updated, SonicJS automatically derives a collection schema from the Form.io definition:

Form.io Schema β†’ Shadow Collection Schema β†’ Content Table

Naming convention: A form named contact creates a collection named form_contact with display name "Contact (Form)".

Field mapping:

Form.io TypeCollection Schema Type
textfield, textarea, password, urlstring
emailstring with format: 'email'
number, currencynumber
checkboxboolean
select, radioselect with enum values
datetime, date, timestring with format: 'date-time'
file, signaturestring
addressobject

Layout components (panels, fieldsets, columns, tabs) are skipped β€” only their nested field components are extracted.

Bootstrap Sync

On application startup, syncAllFormCollections() runs automatically to:

  1. Create or update shadow collections for all active forms
  2. Backfill any submissions that don't yet have content records

This ensures the content table stays in sync even if records were created before the sync feature was added.


Submissions

Submission Flow

  1. User fills out a public form
  2. POST /api/forms/{identifier}/submit is called
  3. All string values are recursively HTML-entity encoded (XSS prevention)
  4. A form_submissions record is created
  5. A content record is created in the shadow collection (dual-write)
  6. The submission is linked to the content via content_id

Submission Data Structure

Each content item created from a submission includes:

{
  "id": "content-uuid",
  "title": "John Doe",
  "slug": "submission-abc12345",
  "status": "published",
  "collectionId": "form_contact-collection-id",
  "data": {
    "title": "John Doe",
    "name": "John Doe",
    "email": "john@example.com",
    "message": "Hello!",
    "_submission_metadata": {
      "submissionId": "submission-uuid",
      "formId": "form-uuid",
      "formName": "contact",
      "email": "john@example.com",
      "ipAddress": "192.168.1.1",
      "userAgent": "Mozilla/5.0...",
      "submittedAt": 1706000000000
    }
  }
}

The title is automatically derived from submission data using this priority: name β†’ fullName β†’ firstName + lastName β†’ email β†’ subject β†’ fallback "Form Name - date".

Viewing Submissions

  • Admin Forms UI β€” /admin/forms/:id/submissions shows a table of all submissions
  • Content API β€” Query the shadow collection like any other content

Form Submissions API

Note (v3): The contentId field is no longer populated. Form submissions are standalone documents; the shadow collection / Content API integration described below is not available in v3. Use /api/form_submissions to access submission records.

List Submissions

Query Form Submissions

curl "http://localhost:8787/api/collections/form_contact/content?limit=25"

Filter Submissions

Use standard Content API filters:

# Search by email
curl "http://localhost:8787/api/collections/form_contact/content?filter[data.email][equals]=john@example.com"

# Get recent submissions
curl "http://localhost:8787/api/collections/form_contact/content?sort=-created_at&limit=10"

Get Single Submission

Note (v3): The contentId field is no longer populated. Form submissions are standalone documents; cross-referencing via the Content API is not available. Access individual submissions via /api/form_submissions.

List Form Collections

Form-sourced collections have source_type: 'form' in the collections API:

curl "http://localhost:8787/api/collections"

Public Form Endpoints

Render Form

GET/forms/:name
Render a public HTML form page

Returns a complete HTML page with the Form.io renderer, Turnstile integration, and client-side submission handling.

Get Form Schema

GET/api/forms/:identifier/schema
Get Form.io schema as JSON for headless frontends

Accepts form ID or name. Only returns active, public forms.

{
  "id": "form-uuid",
  "name": "contact",
  "displayName": "Contact Form",
  "description": "Get in touch",
  "category": "contact",
  "schema": { "components": [...] },
  "settings": { ... },
  "submitUrl": "/api/forms/contact/submit"
}

Submit Form

POST/api/forms/:identifier/submit
Submit form data

Submit Form

curl -X POST http://localhost:8787/api/forms/contact/submit \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "name": "John Doe",
      "email": "john@example.com",
      "message": "Hello!"
    }
  }'

Turnstile Config

GET/api/forms/:identifier/turnstile-config
Get Turnstile CAPTCHA configuration for a form

Returns: enabled, siteKey, theme, size, mode, appearance. Supports per-form settings with global fallback.


Security

XSS Prevention

All submitted string values are recursively HTML-entity encoded before storage. This includes nested objects and arrays. Characters like <, >, ", ', and & are escaped.

Turnstile CAPTCHA

Enable Cloudflare Turnstile on a per-form basis or globally:

  1. Configure the Turnstile plugin with your site key and secret
  2. Enable Turnstile in individual form settings, or use the global default
  3. The public form renderer automatically includes the Turnstile widget

Access Control

  • Public submissions β€” attributed to a system-form-submission system user
  • Authenticated submissions β€” attributed to the logged-in user
  • Submission content β€” published with status published, access controlled by user role
  • Form management β€” requires authentication via requireAuth() middleware

Next Steps

Was this page helpful?