Forms
Free · MITv0.1.0Build forms in your admin, publish them at /f/[slug] and collect validated, spam-protected submissions with CSV export and email alerts.
terminalnpx ajolla add formsWhat you get
- Form builder with 12 field types, validation rules and reordering
- Public, accessible form pages at /f/[slug]
- Spam protection: honeypot, minimum fill time and per-IP rate limits
- Submission limits, closing dates, sign-in and one-response-per-user rules
- Paginated submission inbox with CSV-injection-safe export
- Email alerts through Ajolla Notifications and a forms.submitted event
Routes it adds
/admin/forms/admin/forms/[formId]/admin/forms/[formId]/submissions/admin/forms/[formId]/submissions/[submissionId]/f/[slug]
/api/ajolla/forms/[formId]/export
Plus 1 Supabase migration with row-level security, and an AGENTS.md your coding agent reads after install.
Installs with
Kernel (added automatically).
Configuration
- FORMS_SIGNING_SECRET (required)
- Random server-only secret (32+ characters) that signs form timestamp tokens and salts the stored IP hashes. Generate with `openssl rand -base64 32`.
Events
Other modules (and your own code) can react to these:
forms.submittednotifications.requested
Use it from any stack
Ajolla Hosted runs the same code as a multi-tenant API. Create an app in your dashboard, enable Forms, and call /api/m/forms/v1 with your app key.
OpenAPI document — feed it to Ajolla to generate SDKs in 7 languages and an MCP server.
AGENTS.md
Read the instructions installed with this module
markdown# Ajolla Forms
Build forms in your admin, publish them at `/f/<slug>`, and collect validated, spam-protected submissions.
## What was installed
**Routes**
- `/f/[slug]` — public page for a published form. Anonymous visitors can submit unless the form requires sign-in. Hidden fields can be pre-filled from the URL (`/f/contact?source=newsletter`).
- `/admin/forms` — list forms with submission counts, create a form (`forms.manage`).
- `/admin/forms/[formId]` — builder: title, slug, description, fields (add, edit, reorder, remove), success message, redirect, notification emails, response limit, closing time, sign-in rules; publish / close / reopen / duplicate / delete.
- `/admin/forms/[formId]/submissions` — paginated submissions; `/[submissionId]` shows one and deletes it.
- `GET /api/ajolla/forms/[formId]/export` — CSV export (max 10,000 rows, formula-injection safe).
**Code**
- `lib/ajolla/forms/core.ts` — framework-free logic (create/update/publish forms, `submitForm`, pagination, CSV export). The same code runs in Ajolla Hosted.
- `lib/ajolla/forms/schema.ts` — zod schemas: `fieldDefinitionsSchema`, `formSettingsSchema`, `buildSubmissionSchema(fields)`. Client-safe.
- `lib/ajolla/forms/spam.ts` — signed timestamp tokens, IP hashing. `lib/ajolla/forms/csv.ts` — CSV writer.
- `components/ajolla/forms/*` — field renderer, status badge, CSS module.
- `lib/ajolla/admin/forms.ts` — admin page registration.
**Tables** (migration `0001_forms.sql`)
- `ajolla_forms_forms` — title, unique slug, description, status (`draft|published|closed`), `fields` jsonb, public `settings` jsonb.
- `ajolla_forms_form_private` — notification recipients (kept apart because published forms are publicly readable).
- `ajolla_forms_submissions` — `data` jsonb, `user_id`, `submitter_email`, `ip_hash` (sha256 of IP + secret; raw IPs are never stored), `user_agent`.
RLS: anyone reads published forms and may insert submissions into published, open forms (status, closing time, response limit and sign-in rules are re-checked by the `ajolla_forms_accepts_submission` policy helper). Only `forms.manage` reads drafts, notification recipients and submissions. Nobody can edit a submission.
**Events**
- `forms.submitted` — `{ formId, submissionId, formTitle }`.
- `notifications.requested` — one per notification address, with a plain-text summary of the answers. Delivered by Ajolla Notifications if installed; harmless otherwise.
**Env**
- `FORMS_SIGNING_SECRET` (required, server-only) — signs the form's timestamp token and salts IP hashes. Generate with `openssl rand -base64 32`.
## Setup
1. Add `FORMS_SIGNING_SECRET` to `.env.local` (and your host's env).
2. Apply migrations: `supabase db push`.
3. Give yourself `forms.manage` (an admin; see the kernel's AGENTS.md), open `/admin/forms` and create a form.
4. Add fields, publish, and share `/f/<slug>`.
## Spam protection
- Honeypot input `company-website`: any value rejects the submission.
- Minimum fill time: the page embeds a signed token; submissions within 3 s of rendering, or with a token older than 24 h, are rejected.
- Rate limit: 5 submissions per minute per IP hash per form (`DEFAULT_RATE_LIMIT` in core).
The public action counts submissions and reads notification recipients with the service-role client (visitors can't read them), records events with it (anonymous visitors can't write the outbox), and inserts the submission itself as the visitor through RLS.
## Customising
- Field types and limits: `lib/ajolla/forms/schema.ts` (`TEXT_LIMITS`, `MAX_FIELDS`, messages).
- Look: override the kit's `--aj-*` CSS variables, or edit `components/ajolla/forms/forms.module.css`.
- Field markup: `components/ajolla/forms/form-fields.tsx`.
- Notification text: `submissionEvents()` in core.
- Field keys are the keys of stored answers. Renaming a key after responses arrive leaves old answers under the old key (they still show, by key, in the submission view and export).