Design: two prerequisites before any tenant-attributed send
Found while building step 2. Domains provisioned before the association work have none, and only domains add creates them, so the first send carrying the tenant header would fail SES validation for every existing sender. And the control-plane role needs the two association actions, or teardown returns before it releases the global domain claim.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 file changed,
+25
−0
docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md+25 −0
diff --git a/docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md b/docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md| index 614acc3bc..5590d8a03 100644 |
| --- a/docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md |
| +++ b/docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md |
| @@ -175,6 +175,31 @@ than silently "resumed" by CARLOS on the next day roll. |
| `ses:caller-identity: ["spike-tenant-user"]`, the bare IAM user name, |
| which is what §8's attribution reads. |
| +### 4.2 Two prerequisites before any tenant-attributed send |
| + |
| +Found while building step 2, and both bite at roll time rather than build |
| +time. |
| + |
| +1. **Existing domains have no tenant association, and nothing will ever |
| + create one.** Step 2 associates an identity and the account's |
| + configuration set inside `EnsureDomain`, which is reached only from |
| + `carlos email domains add`. `VerifyDomain` does not associate. So every |
| + domain already in the estate keeps a verified identity with no |
| + association, and the first send that carries `X-SES-TENANT` fails SES's |
| + validation. Before step 3 or step 5 rolls, either a backfill pass |
| + associates every recorded identity and configuration set, or |
| + `VerifyDomain` learns to associate too. This is the failure the standing |
| + warning in `aws.go` exists to prevent, so it is not optional. |
| +2. **The control-plane role needs two new IAM actions**, |
| + `ses:CreateTenantResourceAssociation` and |
| + `ses:DeleteTenantResourceAssociation`. That policy does not live in this |
| + repo. If the grant is missing, `AccessDenied` is neither `AlreadyExists` |
| + nor `NotFound`, so it is not swallowed: `domains add` fails after the |
| + record and the DNS are already written, and `Teardown` returns before |
| + `DeleteIdentity` and before releasing the global domain claim, which |
| + leaves that claim stranded until the permission lands. Confirm the grant |
| + before rolling. |
| + |
| ## 5. The sender |
| A **sender** owns a From address, a credential, a cap, a pause and a set |