Skip to content

12 — Forms and Data Collection

PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-04
Related docs: 06_Content_Modeling_Umbraco.md, 08_API_Contracts.md


This document outlines the architecture for dynamic forms and data collection across the Savoy Signature platform. It defines how forms are created in the CMS, rendered in the headless frontend, validated, submitted safely, and how notifications/integrations are handled.


We use Umbraco Forms combined with a custom GraphQL/REST endpoint that exposes the form definition to Next.js.

Mailjet/SendGridNext.js ClientCustom Form APIUmbraco EditorMailjet/SendGridNext.js ClientCustom Form APIUmbraco EditorBuild form (drag-and-drop fields)Add Form Module to pageFetch form definition (schema)JSON Form Schema (fields, validation, layout)Render UI Component dynamicallyClient-side validationPOST /api/forms/{id}/submit (Data + Token)Server-side validationSave to Umbraco Forms DB (optional)Dispatch email workflowSuccess/Error Response
Form NameCommon PlacementPrimary ActionData Destination
Contact UsGeneric contactPageEmail to specific hotel departmentUmbraco DB + Email
Newsletter SignupFooter (Global)Subscribe to listUmbraco DB + CRM
Event EnquiryeventsPageRFP to Sales teamUmbraco DB + Email
Wedding EnquirycelebrationsPageDetailed RFP to Events teamUmbraco DB + Email
Spontaneous ApplicationcareersPageUpload CVUmbraco DB + Email

The frontend component receives a JSON schema defining the form and renders it using react-hook-form + zod for validation.

{
"formId": "contact-savoy-palace",
"name": "Contact Us",
"submitLabel": "Send Message",
"successMessage": "Thank you. Your message has been sent.",
"fields": [
{
"alias": "firstName",
"label": "First Name",
"type": "text",
"required": true,
"cssClass": "col-span-12 md:col-span-6",
"validation": { "maxLength": 50 }
},
{
"alias": "email",
"label": "Email Address",
"type": "email",
"required": true,
"cssClass": "col-span-12 md:col-span-6",
"validation": { "pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" }
},
{
"alias": "department",
"label": "Department",
"type": "dropdown",
"required": false,
"options": [
{ "value": "reservations", "text": "Reservations" },
{ "value": "concierge", "text": "Concierge" },
{ "value": "events", "text": "Events & Groups" }
]
},
{
"alias": "message",
"label": "Your Message",
"type": "textarea",
"required": true,
"cssClass": "col-span-12",
"validation": { "minLength": 10 }
},
{
"alias": "consent",
"label": "I agree to the Privacy Policy",
"type": "checkbox",
"required": true
}
]
}
packages/modules/src/m17-form-module/FormModule.tsx
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { generateZodSchema } from './form-schema-generator';
export function FormModule({ schema, siteKey, locale }) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [status, setStatus] = useState<'idle'|'success'|'error'>('idle');
// Dynamically generate validation based on CMS rules
const zodSchema = generateZodSchema(schema.fields);
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(zodSchema)
});
const onSubmit = async (data) => {
setIsSubmitting(true);
// Inject anti-spam token (reCAPTCHA v3 or Turnstile)
const token = await window.grecaptcha.execute();
const res = await fetch(`/api/forms/${schema.formId}/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, token, siteKey, locale }),
});
setStatus(res.ok ? 'success' : 'error');
setIsSubmitting(false);
};
if (status === 'success') {
return <div className="form-success">{schema.successMessage}</div>;
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="dynamic-form">
{/* Dynamic rendering of fields based on schema.fields[] */}
</form>
);
}

Form endpoints are highly susceptible to abuse. The platform employs a multi-layered defense.

LayerImplementationPurpose
1. Honeypot FieldHidden &lt;div&gt; with an input field named websiteBots fill hidden fields; humans don’t. Reject if filled.
2. Rate LimitingNext.js API Route + Cloudflare WAFMax 5 submissions per IP per 5 minutes.
3. reCAPTCHA v3Invisible token generation on onSubmitScore-based validation (e.g., block if score < 0.5). Requires no friction for real users.
4. Server ValidationUmbraco API endpointEnsure payload matches field definitions (no injected fields, correct lengths).
5. CSRF ProtectionNext.js + CORSEnsure request originates from Savoy domains.

Umbraco Workflows trigger emails via the external SMTP service defined in ADR-005.

  • Provider: Mailjet or SendGrid (configured in Azure App Settings).
  • Sender Address: [email protected] (authenticated via SPF, DKIM, DMARC on Cloudflare DNS).
  • Reply-To: The email address submitted in the form.
  • Routing: Contact forms route to different inboxes based on the selected “Department” field or the siteRoot configuration.

For forms accepting files (e.g., CV uploads on Careers):

  1. User uploads file (max 5MB, PDF/DOCX only).
  2. Frontend converts file to Base64 or FormData.
  3. API endpoint receives file, scans (if applicable), and saves to a secure Azure Blob Storage container (/form-uploads).
  4. The email notification contains a secure link to download the file (requires CMS authentication or expiring SAS token).
  5. DO NOT attach large files directly to emails.

RequirementImplementation
ConsentAbsolute requirement for a mandatory checkbox linking to the Privacy Policy.
Data RetentionUmbraco Forms configured to auto-delete submissions older than 90 days from the database.
Right to ErasureUser requests handled by DPO via Umbraco backoffice.
ExportCMS editors can export form data to Excel/CSV for reporting.

  • Form schemas fetched dynamically from Umbraco API.
  • Unknown field types gracefully ignored by frontend renderer.
  • Client-side validation blocks submission of invalid emails or required fields.
  • Honeypot field successfully blocks automated bot submissions silently (returns fake 200).
  • reCAPTCHA v3 token validated server-side.
  • Email sent securely via centralized SMTP without exposing sender reputation issues.
  • File uploads do not attach to emails, but store securely on Azure Blob.
  • GDPR mandatory consent checkbox configured on all forms.
  • Rate limits actively drop excessive API calls.

Next document: 13_Media_and_Image_Pipeline.md