Skip to content

Retire Resend and require Amazon SES - #684

Merged
thomasluizon merged 4 commits into
mainfrom
feature/ticket-943-retire-resend
Oct 2, 2026
Merged

thomasluizon merged 4 commits into
mainfrom
feature/ticket-943-retire-resend

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

A missing email provider setting previously selected Resend even though both APIs use SES. The API now registers only SesEmailService and rejects missing or blank SES settings during startup.

The change removes the provider implementation, options and four test files; replaces the registration tests in EmailProviderConfigurationTests.cs; and updates the complete infrastructure fixtures in StartupConfigValidationTests.cs. Registration and validation stay in ServiceCollectionExtensions.Infrastructure.cs, using the existing required-setting guard and build-time OpenAPI detection. appsettings.json drops both obsolete blocks, and the suppression allowlist drops the removed URL suppression.

infra/configuration.tf, variables.tf and example.tfvars drop the switch and vendor environment values and SSM data-source references. cloudflare.tf and check-dns-cutover.sh retire the six legacy email DNS declarations. The repository and infrastructure documentation describe SES and the remaining release operations. Keeping the existing SES transport and configuration preserves message composition, retries and environment-specific event routing.

Closes thomasluizon/orbit-tickets#943. Carry into redesign/main through the standing thomasluizon/orbit-tickets#746 sync after merge.

Assumptions

  • Required SES settings means credentials, region, both senders, support address, both configuration sets and event topic; rejected validating only credentials while accepting other explicitly empty settings.
  • The grep criterion includes documentation and legacy vendor DNS declarations; rejected retaining those references outside the named service files. DNS cleanup has a separate plan so the API environment-group plan remains restricted to two updates.
  • The retired SSM parameters are referenced through data sources, not Terraform-managed resources; rejected adding resource ownership just to destroy them. Their live deletion follows release verification in the manual steps.
  • OpenAPI document generation uses the existing BuildTimeDocumentGeneration.IsActive exception because it has no infrastructure; rejected requiring live credentials for builds. Runtime registration still fails without required SES settings.
  • The standing #746 sync carries the merged change into redesign/main; rejected opening a second PR or merging either branch in this worker.

Test evidence

  • Before changing tests or implementation, dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~EmailProviderConfigurationTests passed all 4 existing cases with the fallback present.
  • After replacing those tests but before changing implementation, the same command failed all 13 cases. EmailAlwaysUsesSesWithoutProviderSetting found ResendEmailService; MissingSesSettingFailsDuringRegistration, BlankSesCredentialsFailDuringRegistration and MissingSesSectionFailsDuringRegistration found that no exception was thrown.
  • After the fix, dotnet test tests/Orbit.Infrastructure.Tests --no-build --filter 'FullyQualifiedName~EmailProviderConfigurationTests|FullyQualifiedName~SesEmailServiceTests|FullyQualifiedName~SesEventProcessorTests|FullyQualifiedName~SnsMessageVerifierTests' passed all 33 cases.
  • The first full run found two storage startup fixtures without SES configuration. After updating those fixtures, dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~StartupConfigValidationTests|FullyQualifiedName~EmailProviderConfigurationTests' passed all 26 cases.
  • dotnet build Orbit.slnx completed with 0 errors. The final dotnet test passed all 7,050 tests: 32 analyzer, 640 domain, 3,763 application and 2,615 infrastructure tests, with no failures or skips.
  • node --test infra/check-web-plan.test.mjs infra/ses-isolation.test.mjs infra/google-play-isolation.test.mjs infra/staging-pinger.test.mjs passed all 34 tests. Terraform formatting and validation, shell syntax, suppression and commit-hook checks passed. Architecture maps were regenerated and remain ignored as required.
  • git grep -i resend contains only ordinary send-again terms, OAuth button code and incidental identifier matches; no vendor configuration, implementation or documentation remains.

Terraform plan evidence

Both saved plans were generated against the live state, not fixtures. No plan was applied.

terraform -chdir=infra plan -input=false -var-file=local.tfvars -target=render_env_group.production_api -target=render_env_group.staging_api -out=email-cleanup.tfplan succeeded. Inspecting the real terraform -chdir=infra show -json email-cleanup.tfplan response confirmed resource_changes[].address, type, and change.actions, plus change.before.env_vars and change.after.env_vars as maps. The only non-no-op resource actions are:

Resource Action Removed keys Added or changed remaining values
render_env_group.production_api update Email__Provider, Resend__ApiKey, Resend__FromEmail, Resend__SupportEmail none
render_env_group.staging_api update Email__Provider, Resend__ApiKey, Resend__FromEmail, Resend__SupportEmail none

A separate email-dns-cleanup.tfplan, targeted at the six removed cloudflare_dns_record.existing instances, succeeded and contains exactly six deletes: resend_send_dkim_txt, resend_updates_dkim_txt, resend_send_spf_txt, resend_updates_spf_txt, send_send_mx, and send_updates_mx. SES DNS resources remain unchanged. Plans are ignored because they contain decrypted secrets; regenerate them with the same targets and inspect the actual JSON before applying.

Manual steps

  1. Release first. After merge and the thomasluizon/orbit-tickets#746 sync, use orbit-api GitHub Actions > Release API (release.yml), dispatched from main: select environment=production, branch=main, then environment=staging, branch=redesign/main. Confirm both APIs are healthy and each delivers a sign-in code with AWS CloudWatch > Metrics > AWS/SES > Send and Delivery for its transactional configuration set (orbit-transactional or orbit-staging-transactional). Production can use the SES mailbox simulator. Until both releases pass, change no email setting in Render and deploy no build from before this change: an old build falls back to Resend when Email__Provider is absent, and Render's Save and deploy redeploys the existing build with the changed variables.
  2. After both releases pass, in the operator Terraform shell, remove production_email_provider and staging_email_provider from infra/local.tfvars, regenerate and review the API group plan in infra/README.md, then apply that saved plan. In Render > Environment Groups > orbit-production-api and orbit-staging-api, confirm Email__Provider and every Resend__* key are absent and all existing Ses__* values are preserved.
  3. In each Render API service > Environment, remove any direct Email__Provider and Resend__* overrides, including Resend__ApiKey, Resend__FromEmail, Resend__SupportEmail and any Resend__MarketingFromEmail override. The running SES-only build ignores them. Confirm neither service has retired email keys overriding its linked group.
  4. Then delete /orbit/production/api/Resend__ApiKey and /orbit/staging/api/Resend__ApiKey in AWS Systems Manager > Parameter Store in us-east-2. Confirm both exact names are absent; removing data-source references alone does not delete them.
  5. Then regenerate, review and apply the separate six-record DNS plan. In Cloudflare > useorbit.org > DNS > Records, confirm the TXT records resend._domainkey.send.useorbit.org and resend._domainkey.updates.useorbit.org, and the MX/TXT pairs at send.send.useorbit.org and send.updates.useorbit.org, are removed. Confirm the SES DKIM CNAMEs and bounce.send.useorbit.org / bounce.updates.useorbit.org MX/TXT records remain.

After both releases pass, the owner may permanently delete the Resend account through their own account deletion action.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

The cleanup sequence can disable passwordless sign-in by redeploying the old API build after removing the settings it still requires. Release the SES-only binaries before destructive configuration cleanup, or explicitly prevent old-build redeploys during the transition.

Reviewed changes Reviewed the SES-only registration, startup validation, removed transport and tests, Terraform environment/DNS cleanup, and release instructions.

  • SES registration: IEmailService now always resolves to SesEmailService, with nine required settings checked outside build-time OpenAPI generation.
  • Transport retirement: Removes the Resend implementation, options, appsettings blocks, vendor-specific tests, and obsolete suppression while retaining shared composition and SES event handling.
  • Infrastructure cleanup: Removes both environments' provider variables and vendor settings plus six legacy DNS records, preserving SES resources and environment-specific routing.
  • Validation: All 46 selected email/startup/SES unit tests and 34 infrastructure regression tests passed in this review.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment thread infra/README.md Outdated
An old build falls back to Resend when Email__Provider is absent, and
Render's Save and deploy redeploys the existing build, so the cleanup
now runs only after both environments run and verify the SES-only build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed commit 11e27c55 since the prior Pullfrog review, with the full PR diff and resolved discussion as context.

  • Moved cleanup after release: Required verified SES-only releases in both environments before removing legacy email settings, service overrides, SSM credentials, or DNS records.
  • Aligned operator instructions: Updated infra/README.md and the PR manual steps to prohibit email configuration changes and old-build deploys until both releases pass health and SES delivery verification.

The prior rollout concern is addressed. Tests were not rerun because the incremental change affects documentation only.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 2026

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 310a552 into main Oct 2, 2026
27 checks passed
@thomasluizon
thomasluizon deleted the feature/ticket-943-retire-resend branch October 2, 2026 00:12
thomasluizon added a commit that referenced this pull request Oct 2, 2026
* chore(infra): delete the GitHub staging keepalive now that the Cloudflare pinger is proven (#682)

The orbit-staging-pinger Worker ran every five minutes through a full
observation hour with all seven independent probes under two seconds,
so the scheduled GitHub workflow is redundant.

Refs thomasluizon/orbit-tickets#1009

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c04451f)

* fix: control reminder scheduler test clocks (#1018) (#683)

(cherry picked from commit b2af4c8)

* Retire Resend and require Amazon SES (#684)

* Remove retired email transport and require SES at startup

* Retire obsolete email configuration and DNS declarations

* Configure SES in infrastructure startup test fixtures

* docs(infra): release the SES-only build before retiring email settings

An old build falls back to Resend when Email__Provider is absent, and
Render's Save and deploy redeploys the existing build, so the cleanup
now runs only after both environments run and verify the SES-only build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 310a552)

# Conflicts:
#	tests/Orbit.Infrastructure.Tests/Services/ResendEmailServiceTests.cs

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant