# 05 — Intake, booking, and data handling Authority: `AGENTS.md` §3 D10 — rebuilt intake form plus calendar booking. Existing infrastructure is documented in `AWS-Hosting-Guide.md` Parts 8–10. **Read that guide before changing anything**; the resources already exist and were built by hand in the console. --- ## What exists today API Gateway (HTTP API) → Lambda → DynamoDB, with SES for notification email and a verified sender on `smlcompany.ca`. `[verified 2026-08-25 — AWS-Hosting-Guide.md]` The shape is right. This is a hardening and rework pass, not a replacement. ## What this data actually is The form collects, in a live legal dispute: the inquirer's identity and contact details, the names of opposing parties and their counsel, the nature of the dispute, and often the amounts at issue. That is **personal information about identifiable third parties who have not consented and do not know the submission happened.** It is more sensitive than a typical contact form by a wide margin, and it is potentially conflict-relevant. Design accordingly. Nothing in this section is optional. --- ## Form fields | Field | Type | Required | Notes | |---|---|---|---| | Name | text | yes | | | Email | email | yes | Validated server-side, not only in the browser | | Phone | tel | no | | | Role | select | yes | Counsel · In-house · Party · Institution · Other | | Firm / organisation | text | no | | | Process sought | select | yes | Mediation · Arbitration · Med-Arb · ENE · Not sure | | Practice area | select | yes | The six areas plus Other | | Other parties | text | no | Surfaced for conflicts screening | | Opposing counsel | text | no | Same | | Matter summary | textarea | yes | 2000 char cap. Hint: *no privileged or confidential detail* | | Timing | select | no | Urgent · 30 days · 90 days · Exploring | | Preferred contact | radio | no | Email · Phone | | Consent | checkbox | **yes** | Explicit, unchecked by default, links to `/legal/privacy/` | **Do not collect** dollar amounts, document uploads, or anything the inquirer might reasonably treat as privileged. The intake call is for that. ### Consent text > I consent to Pouya Lajevardi storing and using the information in this form to > respond to my inquiry and to run a conflicts check. I understand that > submitting this form does not create a retainer, does not appoint a neutral, > and does not itself establish a mediator–party relationship. ## Validation and abuse control Client-side validation is a convenience. **The Lambda re-validates everything.** - Required fields present; email well-formed; lengths within bounds - Reject any field over its cap rather than truncating silently - **Honeypot** field, hidden from sighted and screen-reader users, must be empty - **Timestamp check** — reject submissions completed in under 3 seconds - **Rate limit** by source IP at API Gateway: 5 requests / 5 minutes - No CAPTCHA. It is a third-party script on a page collecting legal information, and the two controls above stop the traffic that matters - CORS restricted to `https://adr.smlcompany.ca` — no wildcard - Strip HTML from every field before storage and before it enters an email body ## Storage DynamoDB, `ca-central-1` — **Canadian data residency is a real selling point for a Canadian legal practice, and the privacy policy will say so.** Confirm the existing table's region and migrate if it is elsewhere (**Q10**). | Attribute | | |---|---| | `pk` | `INTAKE#` | | `sk` | `` | | fields | as above | | `sourceIp`, `userAgent` | abuse investigation only | | `ttl` | epoch seconds — **automatic deletion** | **Encryption at rest** with a customer-managed KMS key. **Point-in-time recovery on.** Table access limited to the Lambda role and one named administrative principal. ### Retention **24 months, enforced by DynamoDB TTL.** Not a policy someone remembers — a mechanism that runs whether anyone remembers or not. Rationale: long enough to serve conflicts screening across a normal matter lifecycle; short enough to be defensible under PIPEDA's requirement to retain personal information only as long as necessary. Whatever number ships must match `/legal/privacy/` exactly. ## Notification SES on submission: - **To Pouya:** the full submission, plainly formatted, replyable to the inquirer. - **To the inquirer:** confirmation of receipt, expected response time, a repeat of the no-retainer language, and a link to the privacy policy. This email is the reason the form beats a `mailto:` link. **Email authentication — verified 2026-08-26, and it is not in place.** A DNS query of `smlcompany.ca` found **no SPF record and no DMARC record**. Mail is on Google Workspace (MX `1 smtp.google.com`) with Google DKIM configured, and the SES domain identity reports verified for sending — but neither SPF nor DMARC exists. **What is already in place** (Namecheap DNS and the SES console, both inspected 2026-08-26): | Record | Status | |---|---| | SES DKIM — `3zsnvsjg…`, `jejgp7na3…`, `xpiwyftpo…` `._domainkey` | **Live.** Matches SES exactly. Never delete | | SES DKIM — `f5puwearz…`, `jdue2r22c…`, `kznn3cklv…` `._domainkey` | Orphans from an earlier verification. Inert. **Leave them** — deleting the wrong three breaks DKIM | | `google._domainkey` TXT | Google Workspace DKIM. Never delete | | Two CNAMEs → `jkddzztszm.acm-validations.aws` | ACM certificate validation. **Never delete** — breaks HTTPS at the next renewal | | `adr` CNAME → `d26v23dhgsp2ta.cloudfront.net` | The site | | Custom MAIL FROM | **Not configured.** Optional; would add SPF alignment | Add both of these; neither conflicts with anything above: | Host | Type | Value | |---|---|---| | `@` | TXT | `v=spf1 include:_spf.google.com include:amazonses.com ~all` | | `_dmarc` | TXT | `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` | A domain may publish **only one** `v=spf1` record, so both senders go in one string. Namecheap TXT values take **no surrounding quotes** — quoting them stores the quotes literally and breaks the record. **Correction to an earlier version of this spec.** SPF is not what authenticates SES here. Without a custom MAIL FROM domain, SES uses an envelope sender at `amazonses.com`, so its SPF pass is not *aligned* with `smlcompany.ca` and does not satisfy DMARC. **SES satisfies DMARC through DKIM alignment** — that is what the six CNAMEs above are doing, and it already works. The SPF record's real job is authenticating **Google Workspace** mail, which currently has no SPF at all. `include:amazonses.com` is harmless and becomes useful if a custom MAIL FROM domain is configured later. Start DMARC at `p=none` — it collects reports without affecting delivery. Move to `quarantine` only after reports come back clean. Reports arrive as XML attachments, so filter them in Gmail, or drop `rua=` entirely and accept having no visibility. **Do not delete the ACM validation CNAMEs.** They are how the certificate for `adr.smlcompany.ca` auto-renews. Removing them breaks HTTPS at the next renewal — silently, months later (Q20). Failure handling: SES failure must never lose the submission. Write to DynamoDB first, then send. A dead-letter queue on the Lambda, and a CloudWatch alarm on DLQ depth ≥ 1. ## Booking An embedded scheduler for the 30–45 minute confidential intake call (**Q5** — tool not yet chosen). - Prefer a provider with Canadian or EU data residency and no advertising business. Cal.com self-hosted is the strongest privacy posture; Cal.com cloud or Calendly are acceptable. - **Lazy-load behind a click.** No third-party iframe on first paint, and no third-party script on any other page. - Provide a plain link fallback that works with JavaScript disabled. - The booking page must carry the same no-retainer language. - Disclose the provider by name in `/legal/privacy/`. ## Security headers Set at CloudFront via a response-headers policy: ``` Strict-Transport-Security: max-age=63072000; includeSubDomains; preload X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=() Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; frame-src ; form-action 'self' ; base-uri 'self'; frame-ancestors 'none' ``` Tighten CSP once the booking provider is chosen. `unsafe-inline` on styles is tolerable for critical CSS; `unsafe-inline` on scripts is not — use a hash or nonce for the reveal script. ## Privacy policy must state Written to match what is actually built, not what is typical: What is collected · why · lawful basis (consent) · where it is stored (DynamoDB, region, encrypted at rest) · **the retention period and that deletion is automatic** · who can access it · third parties involved (AWS, SES, the booking provider, analytics if any) · how to request access or deletion and the address to use · that submitting the form creates no retainer and no mediator–party relationship · cookie and analytics disclosure · last-updated date. If analytics ship, prefer a cookieless privacy-preserving tool (Plausible, Fathom). GA4 on a page collecting legal-dispute information is a poor fit for a practice whose privacy posture is part of its offer (**Q11**). ## Definition of done - [ ] Server-side validation independent of the client - [ ] Honeypot and timing checks live; rate limit configured - [ ] CORS restricted to the production origin - [ ] TTL set and verified by test record - [ ] KMS encryption and PITR enabled - [ ] Both emails send; SPF/DKIM/DMARC aligned; inbox-tested, not spam-tested - [ ] DLQ and CloudWatch alarm configured - [ ] Form usable by keyboard only; errors announced with `role="alert"` - [ ] Form degrades to a `mailto:` fallback with JavaScript disabled - [ ] Privacy policy matches the implementation line for line