How to use Dorna Forms
This service is a shared form inbox. It is not part of Dorna Ads Manager. Any static website can send leads here.
1. Two hostnames
forms.dorna.dev is the website and API. Point it at Cloud Run with a managed certificate. If the zone is on Cloudflare, the record must be DNS-only (grey cloud), not proxied, so Google can issue and renew the certificate. Do not put a global load balancer in front.
notifications.dorna.dev is mail identity only. Add Resend’s SPF, DKIM, and DMARC records there. Do not point this name at the app. All customer sites send from forms@notifications.dorna.dev— one sending domain, not one per customer.
2. Create a site
The easiest path is the admin UI: paste your secret key, create a site, add a form, copy the snippet. The same key works on the API below if you prefer curl. The response includes a public site key that is safe to paste into HTML.
store_content defaults to true (you keep the message in Firestore). Set it to false for a client site: the owner still gets the email, but this server keeps only metadata (id, form, time, mail status) — not name, email, or message body. Change later with PATCH /v1/admin/sites/:id and { "store_content": false }.
curl -s https://forms.dorna.dev/v1/admin/sites \
-H "authorization: Bearer $FORMS_ADMIN_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Dorna Studio",
"allowed_origins": ["https://studio.dorna.dev"],
"notify_emails": ["leads@studio.dorna.dev"],
"store_content": true
}'curl -s https://forms.dorna.dev/v1/admin/sites \
-H "authorization: Bearer $FORMS_ADMIN_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Client site",
"allowed_origins": ["https://client.example"],
"notify_emails": ["owner@client.example"],
"store_content": false
}'curl -s https://forms.dorna.dev/v1/admin/sites/SITE_ID/forms \
-H "authorization: Bearer $FORMS_ADMIN_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Contact",
"fields": [
{ "name": "name", "type": "text", "label": "Name", "required": true, "maxLength": 120 },
{ "name": "email", "type": "email", "label": "Email", "required": true, "maxLength": 254 },
{ "name": "message", "type": "textarea", "label": "Message", "required": true, "maxLength": 5000 },
{ "name": "consent", "type": "consent", "label": "I agree", "required": true, "maxLength": 8 }
]
}'3. Paste the snippet
Mark the form with data-dorna-form and load /v1/sdk.js. The SDK submits without a page reload, adds a honeypot, runs Turnstile, and sends a unique event id so a double-click is one lead.
<form data-dorna-form="form_YOUR_FORM_ID">
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<label>
<input name="consent" type="checkbox" value="true" required />
I agree to be contacted
</label>
<button type="submit">Send</button>
</form>
<script
src="https://forms.dorna.dev/v1/sdk.js"
data-site-key="pk_YOUR_SITE_KEY"
></script>4. What the owner receives
- From: Dorna Forms <forms@notifications.dorna.dev>
- To: the site’s notify emails (plus any extra addresses on the form)
- Reply-To: the visitor’s email when present
- Subject: [Site name] New lead — Contact form
- Body: fields, page URL, time, submission id
List later with GET /v1/admin/sites/:id/submissions.
Rules we enforce
- Unknown or disabled public site key → reject.
- Origin must be on the site’s allow list.
- Form must exist and be active. Pause switch is honored.
- Only defined fields. Length capped. Junk keys dropped.
- Consent required when the form says so.
- Turnstile must pass. Filled honeypot is stored as spam and not emailed.
- Rate limit per visitor + form.
- Same event id twice → one save, no second email.
- Save first, then email. Mail failure is recorded; the visitor can still see success.
- No file uploads in v1.
Secrets and local mail
Resend key, Turnstile secret, signing secret, and the admin bootstrap key live in Google Secret Manager. They are never written into git or generated HTML.
Without RESEND_API_KEY the gateway stores the notification and marks it sent via dry-run — useful locally and in this preview. With a key, mail goes out through Resend and bounce/complaint webhooks update the submission.
Admin API (minimal)
- POST /v1/admin/sites
- GET /v1/admin/sites
- PATCH /v1/admin/sites/:id
- POST /v1/admin/sites/:id/forms
- GET /v1/admin/sites/:id/forms
- PATCH /v1/admin/forms/:id (pause with active: false)
- GET /v1/admin/sites/:id/submissions
- GET /v1/admin/submissions/:id
- POST /v1/admin/keys
Header: Authorization: Bearer sk_…
Later, not now
Later we can optionally forward submissions into Dorna’s lead inbox. This product does not import Ads Manager code.