Skip to content

Managed email domain deletion and Cloudflare DNS import UX#1442

Open
mantrakp04 wants to merge 1 commit into
devfrom
feat/managed-email-domain-delete-cloudflare-dns
Open

Managed email domain deletion and Cloudflare DNS import UX#1442
mantrakp04 wants to merge 1 commit into
devfrom
feat/managed-email-domain-delete-cloudflare-dns

Conversation

@mantrakp04
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 commented May 19, 2026

Summary

  • Add an admin-only delete endpoint and SDK method to remove managed email domains, with Resend/DNSimple cleanup and a guard against deleting domains currently in use for sending.
  • Add dashboard UI to remove unused managed domains (with confirmation) and improve the DNS setup step with Cloudflare detection, zone file download, and import instructions.
  • Add E2E coverage for delete auth, success, in-use rejection, post-switch deletion, and 404 cases.

Test plan

  • Run pnpm test run managed-email-onboarding
  • In dashboard email settings, add a managed domain and verify Cloudflare hint appears when NS records point to Cloudflare
  • Remove an unused managed domain and confirm it disappears from the list
  • Verify active (in-use) managed domains cannot be deleted until email provider is switched away

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Managed email domains can now be deleted from the dashboard email settings
    • Enhanced domain setup experience with automatic Cloudflare DNS detection
    • Added informational banner with Cloudflare DNS links when applicable
    • Introduced delete confirmation flow with notifications for domain removal

Review Change Stack

Let projects remove unused managed domains with upstream cleanup, and streamline Cloudflare NS record import during onboarding.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings May 19, 2026 23:19
@mantrakp04 mantrakp04 self-assigned this May 19, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 19, 2026 11:25pm
stack-auth-mcp Ready Ready Preview, Comment May 19, 2026 11:25pm
stack-auth-skills Ready Ready Preview, Comment May 19, 2026 11:25pm
stack-backend Ready Ready Preview, Comment May 19, 2026 11:25pm
stack-dashboard Ready Ready Preview, Comment May 19, 2026 11:25pm
stack-demo Ready Ready Preview, Comment May 19, 2026 11:25pm
stack-docs Ready Ready Preview, Comment May 19, 2026 11:25pm
stack-preview-backend Ready Ready Preview, Comment May 19, 2026 11:25pm
stack-preview-dashboard Ready Ready Preview, Comment May 19, 2026 11:25pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

📝 Walkthrough

Walkthrough

This PR implements end-to-end deletion for managed email domains. The changes add database helpers, backend service logic with DNSimple zone cleanup, API routes and admin interface methods, dashboard UI with Cloudflare DNS detection, and comprehensive E2E tests.

Changes

Managed Email Domain Deletion

Layer / File(s) Summary
Database operations for managed domains
apps/backend/src/lib/managed-email-domains.tsx
Add deleteManagedEmailDomainById to delete a domain row and return the entity or null, and countManagedEmailDomainsBySubdomainExcludingId to count active records sharing a subdomain while excluding a specific ID.
Backend deletion service and DNSimple integration
apps/backend/src/lib/managed-email-onboarding.tsx
Import updated database helpers; add deleteDnsimpleZoneByName to delete DNSimple zones; implement deleteManagedEmailProvider that validates tenancy, rejects if domain is in use, cleans up Resend, conditionally deletes DNSimple zone, and removes the domain row.
API route and admin interface
apps/backend/src/app/api/latest/internal/emails/managed-onboarding/delete/route.tsx, packages/stack-shared/src/interface/admin-interface.ts, packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts, packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
Expose deletion through POST route handler that validates admin auth; define deleteManagedEmailDomain in StackAdminInterface; implement mapping in admin-app implementation; add method to StackAdminApp interface.
DNS and Cloudflare detection helpers
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
Add DNS-over-HTTPS lookup utilities (lookupAuthoritativeNameServers, findZoneApex, downloadCloudflareZoneFile); create CloudflareIcon component; add state and effect to detect Cloudflare nameservers in stage 2.
Dialog styling and DNS records UI
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
Refactor sender local part input with width constraints; adjust DialogContent overflow; add conditional Cloudflare detection banner with buttons to open DNS and download zone file; update DNS records grid row rendering for better truncation.
Domain deletion UI and confirmation
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
Add confirmDelete state; update managed-domain list items with tooltip; add delete button for non-active domains; implement ActionDialog calling deleteManagedEmailDomain, showing success toast, and refreshing the domain list.
End-to-end test coverage
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts
Add tests covering: client access rejection (401); successful deletion of unused domain; rejection when in use (409); deletion after config switch; and 404 for non-existent domain.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • nams1570
  • N2D4

Poem

🐰 A domain's farewell, graceful and clean,
DNSimple zones and Cloudflare serene,
Delete confirmed, no sender left behind,
Tests pass, and users rejoice in mind! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.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
Title check ✅ Passed The title 'Managed email domain deletion and Cloudflare DNS import UX' directly and accurately summarizes the main changes: adding domain deletion functionality and Cloudflare DNS features.
Description check ✅ Passed The PR description provides a clear summary of changes, test plan with checkbox items, and useful context. While the template is minimal, the author provided substantive content covering objectives, testing approach, and implementation scope.
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.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/managed-email-domain-delete-cloudflare-dns

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds managed email domain deletion support end-to-end (backend endpoint + SDK + dashboard UX), plus Cloudflare-specific DNS setup affordances and E2E coverage to validate deletion behavior and authorization.

Changes:

  • Added an internal admin-only delete endpoint for managed email domains, including provider-side cleanup and “in-use” rejection.
  • Exposed the delete operation through the admin SDK/template layer and surfaced it in the dashboard UI with confirmation.
  • Improved DNS setup UX with Cloudflare detection, zone file download, and import instructions; added E2E tests for delete flows.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts Adds deleteManagedEmailDomain to the admin app interface.
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts Implements the new admin SDK method and maps parameters to the interface request payload.
packages/stack-shared/src/interface/admin-interface.ts Adds a StackAdminInterface method to call the internal delete endpoint.
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts Adds E2E tests for auth, success, in-use rejection, post-switch deletion, and 404 deletion cases.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx Adds dashboard UI for deleting unused managed domains and Cloudflare DNS import UX.
apps/backend/src/lib/managed-email-onboarding.tsx Implements managed domain deletion logic with Resend/DNSimple cleanup and in-use guard.
apps/backend/src/lib/managed-email-domains.tsx Adds DB helpers to delete a managed domain row and count remaining references to a subdomain.
apps/backend/src/app/api/latest/internal/emails/managed-onboarding/delete/route.tsx Introduces the internal admin delete endpoint route.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +16
body: yupObject({
resend_domain_id: yupString().defined(),
}).defined(),
Comment on lines +492 to +501
async deleteManagedEmailDomain(data: {
resend_domain_id: string,
}): Promise<{ status: "deleted" }> {
const response = await this.sendAdminRequest("/internal/emails/managed-onboarding/delete", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(data),
}, null);
checkManagedEmailStatus(options: { domainId: string, subdomain: string, senderLocalPart: string }): Promise<ManagedEmailProviderStatus>,
listManagedEmailDomains(): Promise<ManagedEmailProviderListItem[]>,
applyManagedEmailProvider(options: { domainId: string }): Promise<{ status: "applied" }>,
deleteManagedEmailDomain(options: { resendDomainId: string }): Promise<{ status: "deleted" }>,
Comment on lines +694 to +696
async deleteManagedEmailDomain(options: { resendDomainId: string }): Promise<{ status: "deleted" }> {
return await this._interface.deleteManagedEmailDomain({
resend_domain_id: options.resendDomainId,
size="sm"
variant="outline"
className="gap-1.5"
onClick={() => { window.open(`https://dash.cloudflare.com/?to=/:account/${encodeURIComponent(cloudflareApex)}/dns/records`, "_blank", "noopener,noreferrer"); }}
Comment on lines +113 to +119
// eslint-disable-next-line @next/next/no-img-element
<img
src="https://www.cloudflare.com/favicon.ico"
alt=""
aria-hidden
className={className}
/>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR adds a managed email domain delete flow end-to-end: a new admin-only POST /internal/emails/managed-onboarding/delete route, Resend and DNSimple cleanup with a shared-zone guard, an SDK method, and a dashboard confirmation dialog. It also improves the DNS setup step with Cloudflare auto-detection (via DoH), a zone-file download, and import instructions.

  • Backend: deleteManagedEmailProvider checks in-use status, tears down Resend and DNSimple resources (skipping zone deletion when other tenancies share the subdomain), then removes the DB row; the operation is effectively idempotent on retry.
  • Dashboard: Adds a trash-can button (hidden for active domains) that opens a destructive ActionDialog; the Cloudflare hint fires a best-effort DoH lookup and, when detected, surfaces a zone-file download and step-by-step import popover.
  • Tests: Five new e2e cases covering auth rejection, success, in-use 409, post-switch success, and 404.

Confidence Score: 3/5

Safe to merge after fixing error handling in the delete confirmation dialog; the backend logic and SDK wiring are solid.

The delete confirmation dialog's async okButton.onClick has no error handling and neither does ActionDialog's button wrapper, so any API failure during deletion is silently swallowed — the dialog closes, no toast appears, and the domain remains in the list with the user none the wiser. The backend, SDK, and e2e layers are well-implemented.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx — the ActionDialog delete handler needs error handling before this ships.

Important Files Changed

Filename Overview
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx Adds delete confirmation dialog and Cloudflare DNS UX; ActionDialog's okButton.onClick is async but lacks error handling, meaning API failures are silently swallowed with no user feedback.
apps/backend/src/lib/managed-email-onboarding.tsx Adds deleteManagedEmailProvider with in-use guard, Resend/DNSimple cleanup, and shared-zone safety check; external cleanup runs before DB delete to avoid orphaned provider resources.
apps/backend/src/lib/managed-email-domains.tsx Adds deleteManagedEmailDomainById and countManagedEmailDomainsBySubdomainExcludingId helpers; both are straightforward and correct.
apps/backend/src/app/api/latest/internal/emails/managed-onboarding/delete/route.tsx New admin-only delete endpoint using SmartRouteHandler with correct auth schema, passes resend_domain_id to deleteManagedEmailProvider.
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts Five new e2e cases covering auth rejection, successful delete, in-use rejection, post-switch delete, and 404; inline snapshots and assertions are correct.
packages/stack-shared/src/interface/admin-interface.ts Adds deleteManagedEmailDomain method consistent with existing patterns.
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts Thin adapter bridging camelCase SDK interface to snake_case wire format; no issues.
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts Adds deleteManagedEmailDomain to the StackAdminApp interface type; consistent with existing methods.

Sequence Diagram

sequenceDiagram
    participant Dashboard
    participant AdminApp as Admin SDK
    participant Backend as DELETE /managed-onboarding/delete
    participant DB as Database
    participant Resend
    participant DNSimple

    Dashboard->>AdminApp: "deleteManagedEmailDomain({ resendDomainId })"
    AdminApp->>Backend: POST (admin auth)
    Backend->>DB: getManagedEmailDomainByResendDomainId()
    DB-->>Backend: domain row
    Backend->>Backend: isManagedEmailDomainInUseForTenancy()?
    alt domain is in use
        Backend-->>Dashboard: 409 Conflict
    else not in use
        Backend->>Resend: "DELETE /domains/{id}"
        Resend-->>Backend: 200 / 404
        Backend->>DB: countManagedEmailDomainsBySubdomainExcludingId()
        DB-->>Backend: remaining count
        alt "remaining == 0"
            Backend->>DNSimple: "DELETE /zones/{zoneName}"
            DNSimple-->>Backend: deleted / not_found
        end
        Backend->>DB: deleteManagedEmailDomainById()
        DB-->>Backend: deleted row
        Backend-->>Dashboard: "200 { status: deleted }"
        Dashboard->>Dashboard: refreshDomains() + toast
    end
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx:1182-1190
**Silent error swallowing on domain delete**

`ActionDialog`'s button wraps `okButton.onClick` with `await okButton.onClick?.()` but has no `try/catch` (see `action-dialog.tsx` line 131). If `deleteManagedEmailDomain` throws — for example due to a network failure or an unexpected server error — the error is swallowed and the user receives zero feedback: the dialog closes, the domain still appears in the list, and no alert or toast is shown. Per the repo rule, async button handlers should use `runAsynchronouslyWithAlert` so errors are automatically surfaced to users.

Reviews (1): Last reviewed commit: "Add managed email domain deletion and Cl..." | Re-trigger Greptile

Comment on lines +1182 to +1190
okButton={{
label: "Remove",
onClick: async () => {
if (!confirmDelete) return;
await stackAdminApp.deleteManagedEmailDomain({ resendDomainId: confirmDelete.domainId });
toast({ title: "Domain removed", description: `${confirmDelete.senderLocalPart}@${confirmDelete.subdomain} was removed.`, variant: "success" });
await refreshDomains();
},
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Silent error swallowing on domain delete

ActionDialog's button wraps okButton.onClick with await okButton.onClick?.() but has no try/catch (see action-dialog.tsx line 131). If deleteManagedEmailDomain throws — for example due to a network failure or an unexpected server error — the error is swallowed and the user receives zero feedback: the dialog closes, the domain still appears in the list, and no alert or toast is shown. Per the repo rule, async button handlers should use runAsynchronouslyWithAlert so errors are automatically surfaced to users.

Rule Used: Use runAsynchronouslyWithAlert from `@stackframe... (source)

Learned From
stack-auth/stack-auth#943

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
Line: 1182-1190

Comment:
**Silent error swallowing on domain delete**

`ActionDialog`'s button wraps `okButton.onClick` with `await okButton.onClick?.()` but has no `try/catch` (see `action-dialog.tsx` line 131). If `deleteManagedEmailDomain` throws — for example due to a network failure or an unexpected server error — the error is swallowed and the user receives zero feedback: the dialog closes, the domain still appears in the list, and no alert or toast is shown. Per the repo rule, async button handlers should use `runAsynchronouslyWithAlert` so errors are automatically surfaced to users.

**Rule Used:** Use `runAsynchronouslyWithAlert` from `@stackframe... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))

**Learned From**
[stack-auth/stack-auth#943](https://github.com/stack-auth/stack-auth/pull/943)

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

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: 3

🧹 Nitpick comments (2)
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts (1)

205-206: ⚡ Quick win

Replace fixed sleeps with condition-based polling to reduce E2E flakiness.

Line 205 and Line 243 use wait(1500) as a timing assumption for verification readiness. This can intermittently fail under slower CI timing; poll /check until "verified" with a bounded timeout instead.

♻️ Proposed fix
 import { wait } from "`@stackframe/stack-shared/dist/utils/promises`";
 import { describe } from "vitest";
 import { it } from "../../../../../helpers";
 import { Project, niceBackendFetch } from "../../../../backend-helpers";
 
+async function waitForManagedDomainVerified(domainId: string, subdomain: string, senderLocalPart: string) {
+  const timeoutAt = performance.now() + 10_000;
+  while (true) {
+    const checkResponse = await niceBackendFetch("/api/v1/internal/emails/managed-onboarding/check", {
+      method: "POST",
+      accessType: "admin",
+      body: {
+        domain_id: domainId,
+        subdomain,
+        sender_local_part: senderLocalPart,
+      },
+    });
+    if (checkResponse.body.status === "verified") return;
+    if (performance.now() > timeoutAt) {
+      throw new Error(`Timed out waiting for managed domain ${domainId} to become verified`);
+    }
+    await wait(250);
+  }
+}
+
 describe("managed email onboarding internal endpoints", () => {
 ...
-    await wait(1500);
+    await waitForManagedDomainVerified(setupResponse.body.domain_id, "mail.example.com", "noreply");
 ...
-    await wait(1500);
+    await waitForManagedDomainVerified(setupResponse.body.domain_id, "mail.example.com", "noreply");

Also applies to: 243-244

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts`
around lines 205 - 206, Replace the fixed wait(1500) sleeps used before
verification with a condition-based polling loop that repeatedly calls the
/check endpoint until it returns a response indicating "verified" or a bounded
timeout elapses; update the test that uses wait(1500) (and the other occurrence)
to poll at short intervals (e.g., 200–500ms) for a maximum duration (e.g., 10s),
assert success when the /check response contains "verified", and fail the test
if the timeout is reached—use the existing wait helper only for the poll
interval and keep the polling logic next to the verification steps so the test
no longer depends on a fixed sleep.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx (1)

326-341: ⚡ Quick win

Key the Cloudflare lookup off the subdomain, not the whole setup object.

Line 341 depends on setupState, so every setSetupState({ ...setupState, status }) during verification re-runs the same DoH lookup even though the queried domain did not change. That adds an extra third-party request to each verification click for no benefit.

♻️ Proposed fix
+  const setupSubdomain = setupState?.subdomain;
+
   useEffect(() => {
-    if (!props.open || stage !== 2 || !setupState) {
+    if (!props.open || stage !== 2 || setupSubdomain == null) {
       setCloudflareApex(null);
       return;
     }
     let cancelled = false;
     runAsynchronously(async () => {
-      const zone = await findZoneApex(setupState.subdomain);
+      const zone = await findZoneApex(setupSubdomain);
       if (cancelled || !zone) return;
       const usesCloudflare = zone.nameServers.some((n) => /(^|\.)cloudflare\.com$/i.test(n));
       if (usesCloudflare) setCloudflareApex(zone.apex);
     });
     return () => {
       cancelled = true;
     };
-  }, [props.open, stage, setupState]);
+  }, [props.open, stage, setupSubdomain]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
around lines 326 - 341, The effect that runs findZoneApex is being re-triggered
on every change to the entire setupState object; change its dependency to the
specific subdomain so the DoH lookup only runs when the queried domain changes:
in the useEffect that calls findZoneApex (and updates setCloudflareApex),
replace the dependency on setupState with the subdomain value (e.g.,
setupState?.subdomain) and guard the early-return checks against that subdomain
variable instead of the whole setupState to avoid unnecessary lookups when
setSetupState({...setupState, status}) is called.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/lib/managed-email-domains.tsx`:
- Around line 244-249: The COUNT query in globalPrismaClient.$queryRaw against
"ManagedEmailDomain" counts archived/inactive rows, causing the deletion flow to
think a zone is still referenced; update the WHERE clause in that query (the one
using options.subdomain and options.excludeId) to exclude archived/inactive
records (for example add AND "archivedAt" IS NULL or AND "isArchived" = false
depending on the actual schema column used for soft-deletes) so only live
domains are counted; make sure to use the same archived/active column and
semantics used elsewhere in the codebase.

In `@apps/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 720-729: The current "last reference" check uses
countManagedEmailDomainsBySubdomainExcludingId followed separately by
deleteDnsimpleZoneByName, which is racy; serialize cleanup for a given subdomain
by acquiring a per-subdomain critical section (e.g., a DB advisory lock or mutex
keyed by domain.subdomain) and perform the count-and-delete inside the same
transaction/locked section so the decision is atomic. Update the logic around
countManagedEmailDomainsBySubdomainExcludingId and deleteDnsimpleZoneByName (or
the caller createOrReuseDnsimpleZone flow) to: acquire lock for
domain.subdomain, re-check count (excluding the row being deleted) inside the
lock/transaction, call deleteDnsimpleZoneByName only if count === 0, then
release the lock; ensure lock acquisition/release errors are handled and do not
leave the zone orphaned.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx:
- Around line 1085-1093: The icon-only delete button (the button with
onClick={() => setConfirmDelete(d)} and the <Trash /> icon) lacks an accessible
name; add an explicit aria-label (for example aria-label={`Remove domain
${d.domain}` or a generic "Remove domain") on that button to provide a clear
label for screen readers while keeping the existing title; ensure the aria-label
text conveys the destructive action and identifies the domain when possible.

---

Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx:
- Around line 326-341: The effect that runs findZoneApex is being re-triggered
on every change to the entire setupState object; change its dependency to the
specific subdomain so the DoH lookup only runs when the queried domain changes:
in the useEffect that calls findZoneApex (and updates setCloudflareApex),
replace the dependency on setupState with the subdomain value (e.g.,
setupState?.subdomain) and guard the early-return checks against that subdomain
variable instead of the whole setupState to avoid unnecessary lookups when
setSetupState({...setupState, status}) is called.

In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts`:
- Around line 205-206: Replace the fixed wait(1500) sleeps used before
verification with a condition-based polling loop that repeatedly calls the
/check endpoint until it returns a response indicating "verified" or a bounded
timeout elapses; update the test that uses wait(1500) (and the other occurrence)
to poll at short intervals (e.g., 200–500ms) for a maximum duration (e.g., 10s),
assert success when the /check response contains "verified", and fail the test
if the timeout is reached—use the existing wait helper only for the poll
interval and keep the polling logic next to the verification steps so the test
no longer depends on a fixed sleep.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 535d9443-4e74-49ef-8313-e3dde8def862

📥 Commits

Reviewing files that changed from the base of the PR and between 0848a1a and d3caf61.

⛔ Files ignored due to path filters (1)
  • apps/dashboard/public/assets/cloudflare-import-dns.png is excluded by !**/*.png
📒 Files selected for processing (8)
  • apps/backend/src/app/api/latest/internal/emails/managed-onboarding/delete/route.tsx
  • apps/backend/src/lib/managed-email-domains.tsx
  • apps/backend/src/lib/managed-email-onboarding.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts
  • packages/stack-shared/src/interface/admin-interface.ts
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts

Comment on lines +244 to +249
const rows = await globalPrismaClient.$queryRaw<{ count: bigint }[]>(Prisma.sql`
SELECT COUNT(*)::bigint AS count
FROM "ManagedEmailDomain"
WHERE "subdomain" = ${options.subdomain}
AND "id" <> ${options.excludeId}
`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Exclude inactive rows from the sibling count.

This query currently counts every row with the same subdomain. If an archived/inactive ManagedEmailDomain remains, the deletion flow will think the zone is still referenced and skip DNSimple cleanup even after the last live domain is removed.

Suggested fix
   const rows = await globalPrismaClient.$queryRaw<{ count: bigint }[]>(Prisma.sql`
     SELECT COUNT(*)::bigint AS count
     FROM "ManagedEmailDomain"
     WHERE "subdomain" = ${options.subdomain}
       AND "id" <> ${options.excludeId}
+      AND "isActive" = true
   `);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rows = await globalPrismaClient.$queryRaw<{ count: bigint }[]>(Prisma.sql`
SELECT COUNT(*)::bigint AS count
FROM "ManagedEmailDomain"
WHERE "subdomain" = ${options.subdomain}
AND "id" <> ${options.excludeId}
`);
const rows = await globalPrismaClient.$queryRaw<{ count: bigint }[]>(Prisma.sql`
SELECT COUNT(*)::bigint AS count
FROM "ManagedEmailDomain"
WHERE "subdomain" = ${options.subdomain}
AND "id" <> ${options.excludeId}
AND "isActive" = true
`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/lib/managed-email-domains.tsx` around lines 244 - 249, The
COUNT query in globalPrismaClient.$queryRaw against "ManagedEmailDomain" counts
archived/inactive rows, causing the deletion flow to think a zone is still
referenced; update the WHERE clause in that query (the one using
options.subdomain and options.excludeId) to exclude archived/inactive records
(for example add AND "archivedAt" IS NULL or AND "isArchived" = false depending
on the actual schema column used for soft-deletes) so only live domains are
counted; make sure to use the same archived/active column and semantics used
elsewhere in the codebase.

Comment on lines +720 to +729
// createOrReuseDnsimpleZone lets multiple ManagedEmailDomain rows share a zone (when
// two tenancies pick the same subdomain). Only delete the zone if this row is the
// last one referencing it.
const remaining = await countManagedEmailDomainsBySubdomainExcludingId({
subdomain: domain.subdomain,
excludeId: domain.id,
});
if (remaining === 0) {
await deleteDnsimpleZoneByName(domain.subdomain);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Serialize shared-zone cleanup for a subdomain.

This remaining check happens before the current row is removed and without any lock. If two sibling domains for the same subdomain are deleted concurrently, each request can observe one remaining row and both skip DNSimple zone deletion, leaking the shared zone permanently. This needs a per-subdomain critical section (for example, an advisory lock plus delete/count in one DB transaction) so the “last reference” decision is made atomically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 720 - 729,
The current "last reference" check uses
countManagedEmailDomainsBySubdomainExcludingId followed separately by
deleteDnsimpleZoneByName, which is racy; serialize cleanup for a given subdomain
by acquiring a per-subdomain critical section (e.g., a DB advisory lock or mutex
keyed by domain.subdomain) and perform the count-and-delete inside the same
transaction/locked section so the decision is atomic. Update the logic around
countManagedEmailDomainsBySubdomainExcludingId and deleteDnsimpleZoneByName (or
the caller createOrReuseDnsimpleZone flow) to: acquire lock for
domain.subdomain, re-check count (excluding the row being deleted) inside the
lock/transaction, call deleteDnsimpleZoneByName only if count === 0, then
release the lock; ensure lock acquisition/release errors are handled and do not
leave the zone orphaned.

Comment on lines +1085 to +1093
{!isInUse && (
<button
type="button"
title="Remove domain"
onClick={() => setConfirmDelete(d)}
className="shrink-0 p-1.5 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors"
>
<Trash className="h-3.5 w-3.5" />
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Give the delete icon button an explicit accessible name.

Line 1088 only provides title. For an icon-only destructive control, screen readers need an aria-label; otherwise the action is hard to discover and operate.

♿ Proposed fix
                           {!isInUse && (
                             <button
                               type="button"
+                              aria-label={`Remove ${d.senderLocalPart}@${d.subdomain}`}
                               title="Remove domain"
                               onClick={() => setConfirmDelete(d)}
                               className="shrink-0 p-1.5 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors"
                             >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
around lines 1085 - 1093, The icon-only delete button (the button with
onClick={() => setConfirmDelete(d)} and the <Trash /> icon) lacks an accessible
name; add an explicit aria-label (for example aria-label={`Remove domain
${d.domain}` or a generic "Remove domain") on that button to provide a clear
label for screen readers while keeping the existing title; ensure the aria-label
text conveys the destructive action and identifies the domain when possible.

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.

2 participants