feat: SES production access and monitoring; §7 as single source of operational truth

Q19 is closed — SES production access granted in ca-central-1, confirmed in
writing. Nothing now blocks /contact/.

The structural change is the important one. Specs in docs/ carried their own
copies of resource IDs, regions, DNS records and service state. AGENTS.md §7
is now the single source of truth for operational facts and docs/ cite it
rather than restating it, with the rule recorded in CLAUDE.md under
Conventions.

The reason is the previous commit's DKIM inversion, generalised: the same
fact lived in §7 and docs/05, a correction reached one of them, and the stale
copy told an operator to delete the records that authenticate outbound mail.
A duplicated fact is one that will eventually be wrong in one place, and the
copy that goes stale is the one nobody re-reads. Verified by grep over
docs/*.md — no operational identifier remains.

Also in this change:

- §7 records the SES monitoring: SNS topic ses-alerts, alarms
  SES-BounceRate-High (>= 0.03) and SES-ComplaintRate-High (>= 0.001), and
  the deliberate choice of email feedback forwarding over an SNS feedback
  topic at this volume. The ses-alerts email subscription is stamped PENDING
  CONFIRMATION — the alarms currently notify nobody, now tracked as R9 and on
  the cutover checklist.
- docs/05 records why those alarms are a real control: SES suspends above
  roughly a 5% bounce rate, and under 100 messages a month five bounces
  crosses it.
- Q29: the deploy guard now covers AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
  (emptiness only, never echoed) and INTAKE_ENDPOINT, promoted to job-level
  env. An empty intake endpoint ships a live form posting to nothing, which
  is worse than a failed build. Executed under sh -e across four input
  states; fails closed, leaks nothing.
- docs/06: account ID removed from the backup-bucket callout, pointing at §10
  instead, as README already does.
- astro.config.mjs: prefetch removed entirely. Any setting ships Astro's
  prefetch script to every page against the zero-JS convention. Recorded as a
  decision; revisit against real Lighthouse numbers.

AGENTS.md entry (r) records the full reasoning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XquaEq4BgWMCwUqLEyNkF
This commit is contained in:
Pouya Lajevardi
2026-08-26 11:47:17 -04:00
co-authored by Claude Opus 5
parent 6bf1167624
commit 2b6176e4d7
6 changed files with 244 additions and 70 deletions
+46 -23
View File
@@ -73,10 +73,11 @@ Client-side validation is a convenience. **The Lambda re-validates everything.**
## 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. `AGENTS.md` §7 records
`adr-intake-submissions` in `ca-central-1` `[verified 2026-08-26]`.
DynamoDB, in the region `AGENTS.md` §7 records. **Canadian data residency is
worth stating in the privacy policy**: parties describing a live dispute are
handing over sensitive material, and where it comes to rest is a fair question
for them to ask. Confirm the existing table's region and migrate if it is
elsewhere — §7 has the table name and region.
| Attribute | |
|---|---|
@@ -109,30 +110,52 @@ SES on submission:
of the no-retainer language, and a link to the privacy policy. This email is
the reason the form beats a `mailto:` link.
**SES production access is granted** (Q19, 2026-08-26) — mail reaches unverified
recipients, so the inquirer confirmation works. See §7 for the account state.
### Bounce and complaint monitoring
Configured 2026-08-26; the resource names, thresholds and current state are in
`AGENTS.md` §7. What matters here is why it is a real control rather than a
formality:
**At this volume a single bad address is a threshold event.** SES suspends
sending above roughly a 5% bounce rate. Under 100 messages a month, five bounces
crosses it — and an intake form is exactly where mistyped addresses arrive. The
alarms sit well below that line so there is room to react.
**Bounces and complaints are handled by SES email feedback forwarding**, which is
on by default, **not** by an SNS feedback topic. That is deliberate: at this
volume there is nothing to consume a programmatic feed, and an unused SNS topic
is one more thing to keep correct. Revisit when code needs to *act* on a
bounce — suppression lists, retry logic, marking a record undeliverable.
> **The alarms currently notify nobody.** §7 records the `ses-alerts` email
> subscription as **pending confirmation**. An unconfirmed SNS subscription
> drops every message, so until the confirmation link is clicked the alarms
> fire into nothing. This is the first thing to check if `/contact/` ships.
**Email authentication — in place as of 2026-08-26 (Q20).**
SPF and DMARC were both added by Pouya and independently verified on
2026-08-26. Mail is on Google Workspace (MX `1 smtp.google.com`) with Google
DKIM configured, and the SES domain identity is verified for sending in
`ca-central-1`. **An earlier version of this spec said neither record existed;
that was true when written and is no longer.** The records now live are:
SPF and DMARC are both live and independently verified (Q20); mail is on Google
Workspace with Google DKIM configured, and the SES domain identity is verified
for sending. **The record values, the MX, and the region are in `AGENTS.md` §7 —
not restated here.** An earlier version of this spec asserted that neither SPF
nor DMARC existed; that was true when written and is no longer, which is the
whole argument for citing §7 rather than copying it.
| 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` |
**What is already in place.** `AGENTS.md` §7 is the record — resource IDs, DNS
records, DKIM token sets, and their verification state all live there and are not
restated here. Read §7 before touching DNS.
**What is already in place** (Namecheap DNS and the SES console, both inspected
2026-08-26):
Two points from §7 that this spec depends on, cited rather than copied:
| Record | Status |
|---|---|
| SES DKIM — `f5puwearz…`, `jdue2r22c…`, `kznn3cklv…` `._domainkey` | **LIVE. Never delete.** All three resolve (NOERROR) and back the healthy `ca-central-1` SES identity — `DkimStatus: SUCCESS`. These are the records DMARC alignment rests on `[verified 2026-08-26 — DNS, AGENTS.md §7]` |
| SES DKIM — `3zsnvsjg…`, `jejgp7na3…`, `xpiwyftpo…` `._domainkey` | **BROKEN and inert.** Entered into Namecheap with the full name in the Host field, so the zone doubled the domain; they answer NXDOMAIN at the correct name. They belong to a stray `us-east-1` identity this project does not use. Harmless where they are — leaving them is the low-risk choice (Q21) `[verified 2026-08-26 — DNS, AGENTS.md §7]` |
| `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 |
- **Only one of the two SES DKIM token sets resolves.** §7 names both sets and
marks which is which. The resolving set is what DMARC alignment rests on;
deleting it breaks intake mail authentication silently. The other set is
NXDOMAIN and inert. **Do not act on any DKIM list that is not §7's.**
- **SES has no custom MAIL FROM**, so SPF is unaligned and SES satisfies DMARC
through DKIM alone.
Notes that mattered when these were added, kept because they matter again on
any future edit: a domain may publish **only one** `v=spf1` record, so both senders go in one