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
- Navigate to Admin β Forms
- Click New Form
- Enter a name (machine-friendly, lowercase with underscores) and display name
- 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 Type | Collection Schema Type |
|---|---|
| textfield, textarea, password, url | string |
string with format: 'email' | |
| number, currency | number |
| checkbox | boolean |
| select, radio | select with enum values |
| datetime, date, time | string with format: 'date-time' |
| file, signature | string |
| address | object |
Layout components (panels, fieldsets, columns, tabs) are skipped β only their nested field components are extracted.
Bootstrap Sync
On application startup, syncAllFormCollections() runs automatically to:
- Create or update shadow collections for all active forms
- 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
- User fills out a public form
POST /api/forms/{identifier}/submitis called- All string values are recursively HTML-entity encoded (XSS prevention)
- A
form_submissionsrecord is created - A
contentrecord is created in the shadow collection (dual-write) - 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/submissionsshows a table of all submissions - Content API β Query the shadow collection like any other content
Form Submissions API
Note (v3): The
contentIdfield 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_submissionsto 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
contentIdfield 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
/forms/:nameReturns a complete HTML page with the Form.io renderer, Turnstile integration, and client-side submission handling.
Get Form Schema
/api/forms/:identifier/schemaAccepts 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
/api/forms/:identifier/submitSubmit 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
/api/forms/:identifier/turnstile-configReturns: 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:
- Configure the Turnstile plugin with your site key and secret
- Enable Turnstile in individual form settings, or use the global default
- The public form renderer automatically includes the Turnstile widget
Access Control
- Public submissions β attributed to a
system-form-submissionsystem 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
Form submissions are exempt from CSRF protection since they are public endpoints. Use Turnstile CAPTCHA for bot protection.