Skip to content

fix(domains): unblock delete when domain was removed from SES out-of-band#394

Open
Bluestart83 wants to merge 3 commits intousesend:mainfrom
Bluestart83:pr/delete-domain-list
Open

fix(domains): unblock delete when domain was removed from SES out-of-band#394
Bluestart83 wants to merge 3 commits intousesend:mainfrom
Bluestart83:pr/delete-domain-list

Conversation

@Bluestart83
Copy link
Copy Markdown

@Bluestart83 Bluestart83 commented Apr 28, 2026

The bug

When a domain identity is deleted from AWS SES outside useSend (manual cleanup in the AWS console, an expired sandbox identity, an account migration, etc.), useSend gets stuck:

  1. The DB still holds the Domain record.
  2. deleteDomain() in apps/web/src/server/aws/ses.ts does not catch NotFoundException from DeleteEmailIdentityCommand / DeleteTenantResourceAssociationCommand. So the existing "Delete domain" button on /domains/[id] throws and aborts before deleting the DB row — the orphan is impossible to remove from the UI.
  3. If the orphaned domain is in isVerifying=true state, the detail page itself becomes inaccessible: getDomain() calls refreshDomainVerification(), which calls ses.getDomainIdentity() unconditionally — that throws on a missing SES identity and the tRPC query errors out. So even reaching the Delete button can be blocked.
  4. The only escape today is to manually edit the database (DELETE FROM "Domain" WHERE id = ...), which is unacceptable for self-hosters and not really an option for cloud users at all.

This is purely a useSend/SES integration issue — the SES side is consistent (identity gone), it's the app that doesn't handle a recoverable state.

The fix

Two complementary changes:

  1. Make deleteDomain() idempotent against an already-removed SES identity (the actual fix):

    • DeleteTenantResourceAssociation and DeleteEmailIdentity catch NotFoundException, logger.warn (with { domain, region, sesTenantId }), and continue. The DB row gets removed in that case — exactly what an admin trying to recover wants.
    • All other SES errors are now logged with full error context ({ err, domain, region, sesTenantId }) before rethrow — previously the error was thrown bare with no log, so root cause was invisible.
    • The UI mutation gains an onError toast so failures actually surface instead of silently leaving the dialog open with no feedback.
  2. Add a "Delete domain" button on the /domains list page so the action is reachable even when the detail page itself is broken (case 3 above). Reuses the existing DeleteDomain component (same typed-confirmation dialog, same mutation), no behavior change on the happy path.

After this PR, an admin who lost the SES identity out-of-band can recover entirely from the UI: hit Delete on the row in /domains, confirm, done. No DB surgery.

Linked issues

None — drive-by fix while recovering from a manual SES cleanup. Happy to file an issue first if maintainers prefer.

Migration notes

None — no schema changes, no env changes, no infra changes.

Screenshot

/domains list with the new "Delete domain" button (domain name redacted):

Verification steps

  • Click "Delete domain" on a row in /domains, type the domain name, confirm → row disappears, success toast (regular happy-path delete still works exactly like before).
  • Repeat on a domain whose SES identity was deleted out-of-band → previously stuck (mutation throws, no UI escape); now: DB row removed, success toast, server log shows [ses.deleteDomain] identity already gone on SES, continuing.
  • Trigger a real SES error (e.g. invalid creds) → toast surfaces the error message and server log has the full error context.
  • pnpm tsc --noEmit clean on apps/web.

Summary by CodeRabbit

  • New Features

    • Added a per-domain delete action directly in the domain list for streamlined domain management.
  • Bug Fixes

    • Improved error handling for domain deletion with user-facing failure notifications.
    • Enhanced backend resilience and handling of edge cases during domain removal operations.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

@ali-nld is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Walkthrough

This change adds explicit error handling for domain deletion across the stack: the UI mutation now reports deletion failures via an onError callback that displays the thrown error message; the domain list UI mounts a per-domain DeleteDomain control for each row; and the server-side SES deleteDomain function introduces guarded try/catch blocks that treat NotFoundException as warnings, rethrow other errors, and validate SES HTTP status codes before reporting success.

Possibly related PRs

  • feat: add ses tenant support for teams #193: Modifies the same deleteDomain implementation in apps/web/src/server/aws/ses.ts to add sesTenantId and tenant-association deletion logic; strongly related at the code level.

Suggested reviewers

  • KMKoushik
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main fix: handling domain deletion when the SES identity was removed externally, making the operation idempotent instead of failing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/app/(dashboard)/domains/domain-list.tsx (1)

12-12: Use the ~/ alias for the new import.

This file lives under apps/web/src, so the new relative path should follow the repo import convention instead of reaching across the directory tree.

♻️ Proposed fix
-import { DeleteDomain } from "./[domainId]/delete-domain";
+import { DeleteDomain } from "~/app/(dashboard)/domains/[domainId]/delete-domain";

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use the ~/ alias for imports from src in apps/web.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/`(dashboard)/domains/domain-list.tsx at line 12, Import uses
a relative path that crosses the directory tree; update the import of
DeleteDomain to use the apps/web src alias. Replace the existing import of
DeleteDomain with the repo alias form (use the ~/ prefix) so the symbol
DeleteDomain from "./[domainId]/delete-domain" is imported via
"~/app/(dashboard)/domains/[domainId]/delete-domain" (adjust path under src as
needed) to comply with apps/web src import conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/app/`(dashboard)/domains/domain-list.tsx:
- Line 12: Import uses a relative path that crosses the directory tree; update
the import of DeleteDomain to use the apps/web src alias. Replace the existing
import of DeleteDomain with the repo alias form (use the ~/ prefix) so the
symbol DeleteDomain from "./[domainId]/delete-domain" is imported via
"~/app/(dashboard)/domains/[domainId]/delete-domain" (adjust path under src as
needed) to comply with apps/web src import conventions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1c21feff-561c-40df-ade8-9b895a4e176b

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9788e and e2f5be6.

📒 Files selected for processing (3)
  • apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx
  • apps/web/src/app/(dashboard)/domains/domain-list.tsx
  • apps/web/src/server/aws/ses.ts

@Bluestart83 Bluestart83 changed the title feat(domains): delete button in domain list + idempotent SES deleteDomain fix(domains): unblock delete when domain was removed from SES out-of-band Apr 28, 2026
Reuse DeleteDomain component from detail page; same typed
confirmation dialog. Adds delete affordance to /domains list rows.
- aws/ses.ts: catch NotFoundException on DeleteTenantResourceAssociation
  and DeleteEmailIdentity, log warn and continue (record can still be
  removed from DB when the domain was already deleted on SES). Other
  SES errors are now logged with full context before rethrow.
- delete-domain.tsx: add onError toast so failures surface in the UI
  instead of silently leaving the dialog open.
@Bluestart83 Bluestart83 force-pushed the pr/delete-domain-list branch from e2f5be6 to 3521883 Compare April 28, 2026 11:11
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/server/aws/ses.ts`:
- Around line 187-193: The DeleteEmailIdentity flow currently returns false
silently when response.$metadata.httpStatusCode !== 200; update the code around
the DeleteEmailIdentityCommand handling (the block that calls
sesClient.send(command)) to log the full SES response and context (including the
domain/email identity and response metadata/body) before returning false so
downstream code has the SES response details; keep the existing catch/error
logging intact but ensure non-200 responses are explicitly logged (e.g., log
response and response.$metadata.httpStatusCode) prior to returning false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 06da3fd9-d1e0-4cae-b981-f35736d17c3f

📥 Commits

Reviewing files that changed from the base of the PR and between e2f5be6 and 3521883.

📒 Files selected for processing (3)
  • apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx
  • apps/web/src/app/(dashboard)/domains/domain-list.tsx
  • apps/web/src/server/aws/ses.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/app/(dashboard)/domains/domain-list.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx

Comment thread apps/web/src/server/aws/ses.ts
@Bluestart83
Copy link
Copy Markdown
Author

image Snapshot of UI change

Address CodeRabbit review: when DeleteEmailIdentity returns a non-200
status without throwing, the function silently returned false, leaving
callers with no SES context. Now logs domain/region/response before
returning false, consistent with the rest of this PR's no-silent-failures
philosophy.
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/server/aws/ses.ts (1)

192-197: Prefer throwing on non-200 DeleteEmailIdentity for consistent failure semantics.

This branch currently returns false while other SES error paths throw. Throwing here keeps error propagation consistent and better aligns with UI onError flows.

Suggested patch
     if (response.$metadata.httpStatusCode !== 200) {
       logger.error(
         { domain, region, response },
         "[ses.deleteDomain] non-200 from DeleteEmailIdentity"
       );
-      return false;
+      throw new Error("Failed to delete email identity");
     }
     return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/server/aws/ses.ts` around lines 192 - 197, Replace the
inconsistent return false branch in the DeleteEmailIdentity handling with a
thrown error so failures propagate like other SES error paths: when
response.$metadata.httpStatusCode !== 200 (inside the DeleteEmailIdentity /
[ses.deleteDomain] block), log the error as currently done (logger.error with {
domain, region, response }) and then throw a new Error that includes a clear
message (e.g., "DeleteEmailIdentity failed with non-200 status") and relevant
response metadata so callers (and UI onError flows) receive an exception instead
of a boolean.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/server/aws/ses.ts`:
- Around line 192-197: Replace the inconsistent return false branch in the
DeleteEmailIdentity handling with a thrown error so failures propagate like
other SES error paths: when response.$metadata.httpStatusCode !== 200 (inside
the DeleteEmailIdentity / [ses.deleteDomain] block), log the error as currently
done (logger.error with { domain, region, response }) and then throw a new Error
that includes a clear message (e.g., "DeleteEmailIdentity failed with non-200
status") and relevant response metadata so callers (and UI onError flows)
receive an exception instead of a boolean.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9274514d-e7d8-4857-8ac1-6fe684915985

📥 Commits

Reviewing files that changed from the base of the PR and between 3521883 and c911604.

📒 Files selected for processing (1)
  • apps/web/src/server/aws/ses.ts

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