Appearance
Connected Mode
Connected mode turns FormDSL into a full form platform. Add an apiKey prop and your forms get:
- Auto-save drafts — every keystroke saved, users resume where they left off
- Server-side validation — the same rules enforced on submit
- Submissions dashboard — view, search, and export all submissions
- File uploads — S3-backed with MIME validation and size limits
- Webhooks — notify your backend on submission events
- Encryption — field-level encryption for sensitive data
- Input audit log — timestamped log of every field change
Quick start
1. Run the server
bash
docker run -d --name formdsl \
-p 3000:3000 \
-e DATABASE_URL=postgresql://user:pass@host:5432/formdsl \
-e ENCRYPTION_KEY=$(openssl rand -hex 32) \
-e ADMIN_EMAIL=you@example.com \
-e ADMIN_PASSWORD=changeme \
-e APP_URL=https://forms.yoursite.com \
formdsl/serverThe server auto-runs database migrations on first boot and creates your admin account.
For a complete setup including PostgreSQL and S3, see the self-hosting guide.
2. Get your API key
Open the dashboard at http://localhost:3000 (or your deployed URL), log in with your admin credentials, and go to Settings > API Keys. Create a new publishable key — it starts with fpk_.
3. Add the API key to your form
tsx
import { Form, Section, Question } from '@formdsl/react'
import '@formdsl/react/styles'
export default function ContactForm() {
return (
<Form
id="contact"
version="1.0"
apiKey="fpk_your_publishable_key"
serverUrl="https://forms.yoursite.com"
>
<Section title="Contact">
<Question id="name" type="text" required label="Name" />
<Question id="email" type="email" required label="Email" />
<Question id="message" type="textarea" required label="Message" rows={4} />
</Section>
</Form>
)
}No onSubmit needed — the form auto-submits to the server.
4. View submissions
All submissions appear in the dashboard. Search, filter, view individual entries, and export as CSV.
How it works
When a form has an apiKey:
- On mount — the form creates a draft on the server and receives a session ID
- On input — changes auto-save to the draft (debounced, configurable)
- On submit — the server validates against the manifest, stores the submission, fires webhooks
- On revisit — if the user returns, their draft is restored automatically
The form manifest (validation rules, field definitions) is extracted from your JSX at build time and deployed to the server. The server uses this manifest for server-side validation.
Deploy your form definition
Before a connected form can accept submissions, deploy its manifest to the server:
bash
# React
npx formdsl-react deploy ./src/forms/ContactForm.tsx
# SolidJS
npx formdsl deploy ./src/forms/ContactForm.tsxThis extracts the form definition from your JSX (without a browser) and uploads it to the server. Run this after changing your form structure.
Auto-save options
tsx
<Form
id="long-form"
apiKey="fpk_..."
autoSave {/* enable auto-save (on by default in connected mode) */}
autoSaveDelay={2000} {/* debounce in ms (default: 1500) */}
saveIndicator="checkmark" {/* "checkmark" | "toast" | "none" */}
>Identity verification
For authenticated users, pass a signed JWT so submissions are linked to a user identity:
tsx
<Form
id="application"
apiKey="fpk_..."
identityToken={jwt} {/* signed by your backend with the signing secret */}
>Generate the token in your backend using the signing secret from the dashboard:
js
// Node.js / Express example
import jwt from 'jsonwebtoken'
app.get('/api/form-token', (req, res) => {
const token = jwt.sign(
{ sub: req.user.id, email: req.user.email, name: req.user.name },
process.env.FORMDSL_SIGNING_SECRET,
{ expiresIn: '1h' }
)
res.json({ token })
})php
// Laravel example
use Firebase\JWT\JWT;
Route::get('/api/form-token', function (Request $request) {
$token = JWT::encode([
'sub' => $request->user()->id,
'email' => $request->user()->email,
'name' => $request->user()->name,
'exp' => now()->addHour()->timestamp,
], config('services.formdsl.signing_secret'), 'HS256');
return response()->json(['token' => $token]);
});Webhooks
Configure webhooks in the dashboard to notify your backend when submissions happen:
Events:
submission.created— new submission receivedsubmission.updated— submission edited
Payload:
json
{
"event": "submission.created",
"formId": "contact",
"submissionId": "sub_abc123",
"answers": {
"name": "Jane Doe",
"email": "jane@example.com",
"message": "Hello!"
},
"timestamp": "2026-04-03T12:00:00Z"
}Webhooks include an HMAC signature header (X-FormDSL-Signature) for verification. The webhook secret is shown when you create the webhook in the dashboard.
File uploads
File fields work automatically in connected mode — files upload directly to S3-compatible storage:
tsx
<Question id="resume" type="file" required label="Resume"
accept=".pdf,.doc,.docx" maxSize={10485760} />
<Question id="photos" type="file" label="Photos"
accept="image/*" multiple />The server validates MIME types and file sizes server-side. Configure your S3 bucket in the server environment variables.
Callbacks
tsx
<Form
id="contact"
apiKey="fpk_..."
onSubmitSuccess={(result) => {
// Called after successful server submission
console.log('Submitted:', result.submissionId)
router.push('/thank-you')
}}
onFormStateReady={(formState) => {
// Called when form state is initialized (draft loaded)
console.log('Form ready, draft:', formState.draftId)
}}
>CORS
In production, set the CORS_ORIGIN environment variable on the server to your frontend's origin:
CORS_ORIGIN=https://yoursite.com,https://app.yoursite.comWhat's next
- Getting Started — Frontend-only mode (no server)
- Self-Hosting — Full deployment guide with Docker Compose
- Laravel Integration — Use the manifest with your Laravel backend
- Python Integration — Use the manifest with Django/Flask/FastAPI