Files
adr-sml/docs/05-backend-spec.md
T
Pouya Lajevardi 19f7226661
Build and deploy / build-and-deploy (push) Failing after 5s
chore: project scaffold, specs, and working record
2026-08-26 08:51:16 -04:00

7.4 KiB
Raw Blame History

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 810. 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.

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 mediatorparty 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-1Canadian 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#<uuid>
sk <ISO-8601 timestamp>
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.

SES must have SPF, DKIM, and DMARC aligned on smlcompany.ca or these land in spam. The guide covers domain verification; DMARC needs confirming (Q3).

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 3045 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 <booking-provider>; form-action 'self' <api-endpoint>;
  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 mediatorparty 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