Skip to content

Managed email provider#1222

Merged
BilalG1 merged 13 commits intodevfrom
managed-emails-provider
Mar 10, 2026
Merged

Managed email provider#1222
BilalG1 merged 13 commits intodevfrom
managed-emails-provider

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented Feb 25, 2026

Summary by CodeRabbit

  • New Features
    • Managed email domain onboarding: setup, DNS provisioning, verification, status checks, and apply flow (Resend-backed).
  • UI
    • Project email settings: managed-provider setup dialog, managed sender fields, status display, and test-send mapping.
  • Integrations
    • DNS provider automation and Resend webhook handling for domain status updates; scoped keys for sending.
  • API
    • Admin endpoints / client APIs to setup, check, list, and apply managed email domains.
  • Tests
    • End-to-end tests covering the full onboarding flow.
  • Chores
    • Added environment variables and config schema support for Resend and DNS integrations.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 25, 2026

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

Project Deployment Actions Updated (UTC)
stack-backend Ready Ready Preview, Comment Mar 10, 2026 2:32am
stack-dashboard Ready Ready Preview, Comment Mar 10, 2026 2:32am
stack-demo Ready Ready Preview, Comment Mar 10, 2026 2:32am
stack-docs Ready Ready Preview, Comment Mar 10, 2026 2:32am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds managed email provider support: DB schema and Prisma model, backend onboarding APIs and Resend webhook handling, orchestration with Resend and DNSimple, dashboard UI for setup, shared schema/SDK/template extensions, E2E tests, and new env vars for Resend/DNSimple integration.

Changes

Cohort / File(s) Summary
Environment
apps/backend/.env.development
Added Resend/DNSimple env vars: STACK_RESEND_WEBHOOK_SECRET, STACK_DNSIMPLE_API_TOKEN, STACK_DNSIMPLE_ACCOUNT_ID, STACK_DNSIMPLE_API_BASE_URL (plus existing key).
Database / Prisma
apps/backend/prisma/migrations/.../migration.sql, apps/backend/prisma/schema.prisma
New enum ManagedEmailDomainStatus and ManagedEmailDomain table/model with tenancy FK (ON DELETE CASCADE), JSONB nameServerRecords, status, timestamps, unique/index constraints, and Tenancy relation.
Backend — API routes
apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx, apps/backend/src/app/api/latest/internal/emails/managed-onboarding/*/route.tsx
Added Resend webhook POST route with signature validation; added internal onboarding endpoints: setup, check, list, apply — auth, validation, structured responses.
Backend — Data layer
apps/backend/src/lib/managed-email-domains.tsx
New data module: types, row mapping, JSONB parsing, and CRUD-like functions (create, get by tenancy/subdomain or resendDomainId, list, update webhook status, mark applied).
Backend — Orchestration
apps/backend/src/lib/managed-email-onboarding.tsx
Orchestration flows: setup/check/list/apply integrating Resend and DNSimple, DNS zone/NS/record upserts, scoped API key handling (dev mock), webhook processing mapping provider statuses to internal statuses, and persistence.
Backend — Email config
apps/backend/src/lib/emails.tsx, apps/backend/src/lib/config.tsx, apps/backend/src/lib/projects.tsx
Support provider: "managed": validation for managedSubdomain & managedSenderLocalPart; emitted SMTP config uses smtp.resend.com with composed sender when managed; config defaults extended.
Dashboard UI
apps/dashboard/src/app/.../emails/page-client.tsx, apps/dashboard/src/components/form-dialog.tsx
Added ManagedEmailSetupDialog and UI flows for managed provider; render managed subdomain/sender; managed setup trigger in EmailServerCard; centralized okButton helper for dialogs.
Shared schema & SDK
packages/stack-shared/src/config/schema.ts, .../schema-fuzzer.test.ts, packages/stack-shared/src/interface/admin-interface.ts
Schema now accepts provider: "managed" with conditional validation for managed fields/password; defaults updated; admin SDK methods added for setup/check/list/apply managed domains.
Template / App interfaces
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts, .../implementations/admin-app-impl.ts
Added public types and implementations mapping API responses to camelCase: setupManagedEmailProvider, checkManagedEmailStatus, listManagedEmailDomains, applyManagedEmailProvider.
Tests
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts
New E2E tests covering permission checks, full onboarding flow, status checks, and apply operation with config assertions.
Misc — Tests / Formatting
packages/stack-shared/src/config/schema-fuzzer.test.ts, apps/dashboard/.../page-client.tsx
Fuzzer entries for managed fields; UI and form-dialog consistency changes.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    actor Client as Dashboard Client
    end
    rect rgba(200,255,200,0.5)
    participant Backend as Backend API
    participant DB as Database
    end
    rect rgba(255,200,200,0.5)
    participant Resend as Resend API
    participant DNSimple as DNSimple API
    end

    Client->>Backend: POST /internal/emails/managed-onboarding/setup (subdomain, sender)
    Backend->>Resend: create domain / request verification
    Resend-->>Backend: domainId, nameServerRecords
    Backend->>DNSimple: ensure zone, upsert NS/TXT records
    DNSimple-->>Backend: records updated
    Backend->>DB: insert ManagedEmailDomain (status, nameServerRecords)
    DB-->>Backend: stored
    Backend-->>Client: { domain_id, status, name_server_records }
Loading
sequenceDiagram
    participant Resend as Resend Webhook
    participant Backend as Backend API
    participant DB as Database
    participant Config as Email Config

    Resend->>Backend: POST /integrations/resend/webhooks (domain.updated)
    Backend->>Backend: validate signature (STACK_RESEND_WEBHOOK_SECRET)
    Backend->>DB: updateManagedEmailDomainWebhookStatus(resendDomainId, providerStatusRaw, status, lastError)
    DB-->>Backend: domain updated
    Backend->>Config: refresh/emit config changes if status applied
    Backend-->>Resend: { received: true }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Remove legacy tenancy config #802 — Refactors backend email config access; directly intersects with new managed-provider logic in apps/backend/src/lib/emails.tsx.
  • Email outbox backend #1030 — Modifies backend email handling and endpoints that overlap with webhook/onboarding routes.
  • resend api key config #851 — Updates email-provider schema and dashboard components; overlaps with schema/UI changes introducing provider: "managed".

Suggested reviewers

  • N2D4
  • Developing-Gamer

Poem

🐇 I hopped through DNS and Resend’s gate,
I planted records and waited for a state.
Webhooks chimed, the DB wrote the line,
Managed mail took root — neat and fine.
Carrots and domains, verified and great!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is empty except for a template comment reminding to read CONTRIBUTING.md guidelines. No actual implementation details, context, or explanation of changes are provided. Add a comprehensive description explaining the feature: what managed email provider does, how it integrates with existing systems, any breaking changes, testing approach, and links to related issues or documentation.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Managed email provider' is concise and directly describes the main feature being added, though it could be slightly more specific about the scope.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch managed-emails-provider

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 25, 2026

Greptile Summary

Implemented managed email provider feature that automates DNS setup for Resend email domains through DNSimple integration. Added database schema for tracking managed domains, API endpoints for setup/check/list/apply operations, webhook handler for Resend domain verification events, and dashboard UI for the onboarding flow. Users can now delegate a subdomain via nameservers and have Stack automatically configure Resend with scoped API keys.

Confidence Score: 4/5

  • Safe to merge with one missing environment variable that needs to be added
  • Well-structured implementation with comprehensive tests, proper error handling, and follows established patterns. Missing webhook secret environment variable will cause runtime errors in production when webhooks are triggered.
  • apps/backend/.env.development needs STACK_RESEND_WEBHOOK_SECRET added

Important Files Changed

Filename Overview
apps/backend/src/lib/managed-email-onboarding.tsx Implemented managed email setup with Resend and DNSimple integration, DNS record management
apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx Resend webhook handler for domain verification status updates with signature verification
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx Added managed email setup UI with domain tracking, status monitoring, and async button handlers
apps/backend/.env.development Added mock credentials for Resend and DNSimple, missing webhook secret
packages/stack-shared/src/config/schema.ts Extended email config schema with managed provider and conditional validation

Sequence Diagram

sequenceDiagram
    participant User
    participant Dashboard
    participant Backend
    participant Resend
    participant DNSimple
    participant DB

    User->>Dashboard: Start Managed Setup
    Dashboard->>Backend: POST /managed-onboarding/setup
    Backend->>Resend: Create Domain
    Resend-->>Backend: Domain ID + DNS Records
    Backend->>DNSimple: Create/Reuse Zone
    DNSimple-->>Backend: Zone Created
    Backend->>DNSimple: Add DNS Records
    Backend->>DNSimple: Get Nameservers
    DNSimple-->>Backend: NS Records
    Backend->>DB: Store ManagedEmailDomain
    Backend-->>Dashboard: Domain ID + NS Records
    Dashboard-->>User: Show NS Records to Delegate
    
    User->>DNS Provider: Delegate Subdomain to NS
    DNS Provider->>DNSimple: NS Resolution
    Resend->>DNSimple: Verify DNS Records
    Resend->>Backend: Webhook: domain.updated (verified)
    Backend->>DB: Update Status to VERIFIED
    
    User->>Dashboard: Click "Use This Domain"
    Dashboard->>Backend: POST /managed-onboarding/apply
    Backend->>Resend: Create Scoped API Key
    Resend-->>Backend: API Key
    Backend->>DB: Update Config + Mark APPLIED
    Backend-->>Dashboard: Success
Loading

Last reviewed commit: afccb8d

Copy link
Copy Markdown
Contributor

@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.

21 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

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

🧹 Nitpick comments (4)
apps/dashboard/src/components/form-dialog.tsx (1)

71-80: Inconsistent equality operators: == vs === for boolean comparison.

Lines 73 and 78 use == for comparing typeof props.okButton with "boolean", while the equivalent code in SmartFormDialog (Lines 22, 27) uses ===. This is functionally equivalent here since typeof always returns a string, but the inconsistency is unnecessary.

Suggested fix for consistency
-    ...(typeof props.okButton == "boolean" ? {} : props.okButton),
+    ...(typeof props.okButton === "boolean" ? {} : props.okButton),
     props: {
       form: formId,
       type: "submit" as const,
       loading: submitting,
-      ...((typeof props.okButton == "boolean") ? {} : props.okButton?.props),
+      ...((typeof props.okButton === "boolean") ? {} : props.okButton?.props),
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/form-dialog.tsx` around lines 71 - 80, The code
in the okButton construction uses loose equality (typeof props.okButton ==
"boolean") in two places; change both to strict equality (===) to match the
style used in SmartFormDialog and maintain consistency. Update the two checks
inside the okButton object that reference typeof props.okButton to use ===;
ensure behavior remains the same for props.okButton, okButton.props merging, and
that variables like formId and submitting are untouched.
apps/backend/src/lib/config.tsx (1)

1087-1096: Duplicated managed SMTP constants across emails.tsx and config.tsx.

The Resend SMTP host (smtp.resend.com), port (465), and username (resend) are hardcoded identically in both apps/backend/src/lib/emails.tsx (Lines 115-117) and here. Consider extracting these into a shared constant to keep them in sync.

Also note: emails.tsx throws a StackAssertionError if managedSubdomain or managedSenderLocalPart is missing, while this code silently falls back to senderEmail (Line 1096). This divergence may be intentional for the read-path, but worth confirming.

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

In `@apps/backend/src/lib/config.tsx` around lines 1087 - 1096, Extract the
duplicated Resend SMTP values (host "smtp.resend.com", port 465, username
"resend") into a single shared constant (e.g., RESEND_SMTP = { host, port,
username }) exported from a common module and replace the hardcoded literals in
both apps/backend/src/lib/config.tsx (where renderedConfig.emails.server is
used) and apps/backend/src/lib/emails.tsx to reference that constant; while
doing this, reconcile the behavioral divergence for missing
managedSubdomain/managedSenderLocalPart by choosing one consistent approach
(either preserve the current fallback to senderEmail in the config path or make
config.tsx mirror emails.tsx and throw a StackAssertionError) and implement that
consistent behavior in both places (update any conditionals around
managedSenderLocalPart/managedSubdomain and error handling to match the chosen
approach).
apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx (1)

78-84: Consider using StatusError(400) for missing webhook payload fields.

When domainId or providerStatusRaw is missing from a domain.updated payload, the code throws StackAssertionError which surfaces as a 500. Since this is externally-triggered input (from Resend), a 400 may be more accurate. That said, if these fields are contractually guaranteed by Resend's API for domain.updated events, treating their absence as an assertion failure is defensible.

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

In `@apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx`
around lines 78 - 84, Replace the 500-level StackAssertionError thrown when
payload.data?.id or payload.data?.status is missing with a 400 StatusError to
reflect a bad external request: in the block that checks domainId and
providerStatusRaw (variables domainId and providerStatusRaw), throw a new
StatusError(400, "Resend webhook payload missing required domain fields") and
include the original payload in the error metadata or message instead of using
StackAssertionError so the response returns 400 for malformed Resend webhooks.
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts (1)

37-121: Add explicit client-access rejection tests for /check, /list, and /apply too.

The suite currently hardens access control only for /setup; adding the same negative checks for the other new endpoints will better protect against authorization regressions.

As per coding guidelines, **/apps/e2e/**/*.{ts,tsx}: ALWAYS add new E2E tests when you change the API or SDK interface. Generally, err on the side of creating too many tests.

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

In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts`
around lines 37 - 121, Add negative E2E assertions in
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts
to ensure client-level access is rejected for the remaining endpoints
(/api/v1/internal/emails/managed-onboarding/check, /list, and /apply). For each
endpoint create a short test (or extend the existing suite) that calls
niceBackendFetch with accessType: "client" and the same minimal body used in the
positive tests (use domain_id/subdomain/sender_local_part where required) and
assert the response indicates authorization failure (e.g., status is 401/403 or
otherwise rejected by your API). Ensure the tests reference the same endpoint
paths and mimic the request shapes used in the passing admin tests so they fail
due to access level rather than request shape.
🤖 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/backend/.env.development`:
- Around line 98-103: Add the missing STACK_RESEND_WEBHOOK_SECRET environment
variable to the development env so getEnvVariable("STACK_RESEND_WEBHOOK_SECRET")
in the Resend webhook route (route.tsx) does not throw at runtime; update the
development .env to include STACK_RESEND_WEBHOOK_SECRET=your_mock_secret (or
load from your local secrets manager) and ensure any local run scripts or
docker-compose env injection include this key so the webhook handler can read
it.

In
`@apps/backend/prisma/migrations/20260224000000_managed_email_domains/migration.sql`:
- Around line 8-9: Add DB-level lowercase enforcement for the "subdomain"
column: add a CHECK constraint ensuring "subdomain" = lower("subdomain") so only
lowercased values can be stored, and create a case-insensitive unique index on
lower("subdomain") (e.g. UNIQUE INDEX ON <table>(lower("subdomain")) ) to
prevent case-variant duplicates; update the migration.sql DDL around the
"subdomain" column to include the CHECK and add the functional unique index.

In `@apps/backend/src/lib/managed-email-domains.tsx`:
- Around line 47-53: The mapping functions (dbStatusToStatus and the opposite
mapping) must not silently coerce unknown enum values to "failed"; replace the
final unconditional return with an explicit fail-fast behavior: throw a
descriptive Error (e.g., throw new Error(`Unknown ManagedEmailDomainRow.status:
${status}`)) or use a TypeScript exhaustive check (assign to never) so unhandled
enum cases cause a compile-time/error-time failure; update both dbStatusToStatus
and statusToDbStatus to perform this explicit error throw/exhaustive check
instead of returning "failed".

In `@apps/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 610-637: Before minting keys, perform an atomic conditional state
transition on the domain to prevent races: update the domain row from status ===
"verified" to an intermediate state (e.g., "applying") using a conditional DB
update (or transaction) and check affected rows; if the update fails (0 rows
affected) re-read the domain and return/throw a 409 or the current status,
otherwise proceed to call createResendScopedKey, saveManagedEmailProviderConfig,
then markManagedEmailDomainApplied (or set final "applied" state) — update or
add helper(s) like markManagedEmailDomainApplying or an
atomicMarkManagedEmailDomainStatus(domain.id, from, to) and use
getManagedEmailDomainByResendDomainId, createResendScopedKey,
saveManagedEmailProviderConfig, and markManagedEmailDomainApplied accordingly.
- Around line 553-575: The current flow calls
createResendDomain/createOrReuseDnsimpleZone/upsertDnsimpleResendRecords before
persisting the managed domain, which can leave external resources created if DB
persistence fails; make the operation idempotent by either (A) persist a
ManagedEmailDomain row first in createManagedEmailDomain with a safe interim
status (e.g., "pending_external") and then perform
createResendDomain/createOrReuseDnsimpleZone/upsertDnsimpleResendRecords and
finally update the row with resendDomainId/nameServerRecords/status before
returning managedDomainToSetupResult, or (B) if you prefer to keep external
calls first, catch "already exists" errors from createResendDomain and look up
the existing resend domain (or reuse createOrReuseDnsimpleZone) so retries can
continue; apply this change around the sequence using the functions
createResendDomain, createOrReuseDnsimpleZone, upsertDnsimpleResendRecords,
createManagedEmailDomain, and managedDomainToSetupResult.
- Around line 181-188: The HTTP fetch calls (e.g., in listDnsimpleZones) lack
timeouts and must be replaced with a timeout-aware helper: implement a
fetchWithTimeout utility that accepts the same args plus a timeoutMs (or an
AbortSignal) and uses an AbortController to cancel the request after the
timeout, then update all DNSimple/Resend callers (listDnsimpleZones,
parseDnsimpleJsonOrThrow usages, and other functions that call fetch with
getDnsimpleHeaders/getResendBaseUrl) to call fetchWithTimeout(...) instead of
fetch(...), passing a sensible timeout constant; ensure the helper forwards
headers and body, rejects/throws consistently on abort, and update callers to
handle the thrown timeout error.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 949-953: The current presence checks use falsy tests on
emailServerConfig properties which will misclassify valid values like 0; update
the checks that build missingFields to use explicit null/undefined comparisons
(e.g., emailServerConfig.host == null, emailServerConfig.port == null,
emailServerConfig.username == null) so only null/undefined are treated as
missing while preserving the existing missingFields logic that operates on the
emailServerConfig variable.
- Around line 310-315: The refreshDomains function can leave loadingDomains true
if listManagedEmailDomains throws; wrap the call in a try/finally (or
try/catch/finally) so setLoadingDomains(false) always runs; specifically update
refreshDomains (which calls stackAdminApp.listManagedEmailDomains, setDomains,
and setLoadingDomains) to call setLoadingDomains(true) then await the API inside
try and call setLoadingDomains(false) in finally, optionally handling/logging
errors in catch and avoiding leaving the dialog stuck.

In `@packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts`:
- Around line 643-647: applyManagedEmailProvider calls
this._interface.applyManagedEmailProvider but doesn't refresh cached
project/config state, causing stale reads; after awaiting
this._interface.applyManagedEmailProvider(...) add an awaited cache
refresh/invalidation step (call the existing cache helpers if present, e.g.
this.invalidateProjectCache() or this.refreshProjectConfig(), or implement a
small helper like refreshProjectAndConfigCaches()) to reload project and config
data before returning the { status: "applied" } result so subsequent reads see
the updated email config.

---

Nitpick comments:
In `@apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx`:
- Around line 78-84: Replace the 500-level StackAssertionError thrown when
payload.data?.id or payload.data?.status is missing with a 400 StatusError to
reflect a bad external request: in the block that checks domainId and
providerStatusRaw (variables domainId and providerStatusRaw), throw a new
StatusError(400, "Resend webhook payload missing required domain fields") and
include the original payload in the error metadata or message instead of using
StackAssertionError so the response returns 400 for malformed Resend webhooks.

In `@apps/backend/src/lib/config.tsx`:
- Around line 1087-1096: Extract the duplicated Resend SMTP values (host
"smtp.resend.com", port 465, username "resend") into a single shared constant
(e.g., RESEND_SMTP = { host, port, username }) exported from a common module and
replace the hardcoded literals in both apps/backend/src/lib/config.tsx (where
renderedConfig.emails.server is used) and apps/backend/src/lib/emails.tsx to
reference that constant; while doing this, reconcile the behavioral divergence
for missing managedSubdomain/managedSenderLocalPart by choosing one consistent
approach (either preserve the current fallback to senderEmail in the config path
or make config.tsx mirror emails.tsx and throw a StackAssertionError) and
implement that consistent behavior in both places (update any conditionals
around managedSenderLocalPart/managedSubdomain and error handling to match the
chosen approach).

In `@apps/dashboard/src/components/form-dialog.tsx`:
- Around line 71-80: The code in the okButton construction uses loose equality
(typeof props.okButton == "boolean") in two places; change both to strict
equality (===) to match the style used in SmartFormDialog and maintain
consistency. Update the two checks inside the okButton object that reference
typeof props.okButton to use ===; ensure behavior remains the same for
props.okButton, okButton.props merging, and that variables like formId and
submitting are untouched.

In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts`:
- Around line 37-121: Add negative E2E assertions in
apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts
to ensure client-level access is rejected for the remaining endpoints
(/api/v1/internal/emails/managed-onboarding/check, /list, and /apply). For each
endpoint create a short test (or extend the existing suite) that calls
niceBackendFetch with accessType: "client" and the same minimal body used in the
positive tests (use domain_id/subdomain/sender_local_part where required) and
assert the response indicates authorization failure (e.g., status is 401/403 or
otherwise rejected by your API). Ensure the tests reference the same endpoint
paths and mimic the request shapes used in the passing admin tests so they fail
due to access level rather than request shape.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09aa757 and afccb8d.

📒 Files selected for processing (21)
  • apps/backend/.env.development
  • apps/backend/prisma/migrations/20260224000000_managed_email_domains/migration.sql
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsx
  • apps/backend/src/app/api/latest/internal/emails/managed-onboarding/apply/route.tsx
  • apps/backend/src/app/api/latest/internal/emails/managed-onboarding/check/route.tsx
  • apps/backend/src/app/api/latest/internal/emails/managed-onboarding/list/route.tsx
  • apps/backend/src/app/api/latest/internal/emails/managed-onboarding/setup/route.tsx
  • apps/backend/src/lib/config.tsx
  • apps/backend/src/lib/emails.tsx
  • apps/backend/src/lib/managed-email-domains.tsx
  • apps/backend/src/lib/managed-email-onboarding.tsx
  • apps/backend/src/lib/projects.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
  • apps/dashboard/src/components/form-dialog.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.ts
  • packages/stack-shared/src/config/schema-fuzzer.test.ts
  • packages/stack-shared/src/config/schema.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 +610 to +637
const domain = await getManagedEmailDomainByResendDomainId(options.domainId);
if (!domain || domain.tenancyId !== options.tenancy.id || !domain.isActive) {
throw new StatusError(404, "Managed domain not found for this project/branch");
}
if (domain.status === "applied") {
return { status: "applied" };
}
if (domain.status !== "verified") {
throw new StatusError(409, "Managed domain is not verified yet");
}

const resendApiKey = shouldUseMockManagedEmailOnboarding()
? `managed_mock_key_${options.tenancy.id}`
: await createResendScopedKey({
subdomain: domain.subdomain,
domainId: domain.resendDomainId,
tenancyId: options.tenancy.id,
});

await saveManagedEmailProviderConfig({
tenancy: options.tenancy,
resendApiKey,
subdomain: domain.subdomain,
senderLocalPart: domain.senderLocalPart,
});

await markManagedEmailDomainApplied(domain.id);
return { status: "applied" };
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

Guard apply with an atomic state transition before creating scoped keys.

Concurrent apply requests can both pass the verified check and mint multiple scoped Resend keys before markManagedEmailDomainApplied runs.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 610 - 637,
Before minting keys, perform an atomic conditional state transition on the
domain to prevent races: update the domain row from status === "verified" to an
intermediate state (e.g., "applying") using a conditional DB update (or
transaction) and check affected rows; if the update fails (0 rows affected)
re-read the domain and return/throw a 409 or the current status, otherwise
proceed to call createResendScopedKey, saveManagedEmailProviderConfig, then
markManagedEmailDomainApplied (or set final "applied" state) — update or add
helper(s) like markManagedEmailDomainApplying or an
atomicMarkManagedEmailDomainStatus(domain.id, from, to) and use
getManagedEmailDomainByResendDomainId, createResendScopedKey,
saveManagedEmailProviderConfig, and markManagedEmailDomainApplied accordingly.

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

♻️ Duplicate comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (1)

952-956: ⚠️ Potential issue | 🟡 Minor

Use == null checks instead of falsy checks for SMTP field validation.

!emailServerConfig.port treats 0 as missing. As per coding guidelines, prefer explicit null/undefined checks.

🔧 Proposed fix
-        if (!emailServerConfig.host) missingFields.push("host");
-        if (!emailServerConfig.port) missingFields.push("port");
-        if (!emailServerConfig.username) missingFields.push("username");
+        if (emailServerConfig.host == null) missingFields.push("host");
+        if (emailServerConfig.port == null) missingFields.push("port");
+        if (emailServerConfig.username == null) missingFields.push("username");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx
around lines 952 - 956, In the provider !== "managed" block, the current
validation uses falsy checks (e.g., if (!emailServerConfig.port)) which
incorrectly treats valid values like 0 as missing; update the checks to explicit
null/undefined comparisons (e.g., if (emailServerConfig.port == null)) for host,
port, and username so only null or undefined are considered missing and still
push the same keys into missingFields; locate these checks on the
emailServerConfig object and replace !emailServerConfig.host /
!emailServerConfig.port / !emailServerConfig.username with == null variants.
🤖 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/backend/.env.development`:
- Line 104: Update the STACK_DNSIMPLE_API_BASE_URL environment variable in the
development config to point at DNSimple's sandbox host rather than the
production API; change the value of STACK_DNSIMPLE_API_BASE_URL to use
api.sandbox.dnsimple.com so local development and mock tokens cannot
accidentally hit the live DNSimple API.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 240-256: The UI can show the literal "undefined" because
emailConfig.managedSubdomain and emailConfig.managedSenderLocalPart are rendered
directly when provider === "managed"; update the rendering logic in
page-client.tsx to avoid printing undefined by either conditionally rendering
those <span> blocks only when the value is non-null/defined or by replacing the
output with a safe fallback (e.g., empty string or a placeholder like "—") using
a nullish coalescing check for emailConfig.managedSubdomain and
emailConfig.managedSenderLocalPart so the UI never displays the string
"undefined".
- Around line 393-449: Wrap each async onClick handler so thrown errors are
surfaced via runAsynchronouslyWithAlert instead of leaving promises unhandled:
replace the direct async arrow functions that call
stackAdminApp.checkManagedEmailStatus (in the "Refresh Status" button),
stackAdminApp.applyManagedEmailProvider (in the "Use This Domain" button inside
the setupState block), and the applyManagedEmailProvider call inside the
domains.map handler with calls to runAsynchronouslyWithAlert(fn) where fn
performs the same await calls and subsequent setSetupState and refreshDomains
actions; ensure you pass the existing logic (setSetupState and await
refreshDomains) into the wrapped function so state updates and domain refresh
still occur after success.

---

Duplicate comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 952-956: In the provider !== "managed" block, the current
validation uses falsy checks (e.g., if (!emailServerConfig.port)) which
incorrectly treats valid values like 0 as missing; update the checks to explicit
null/undefined comparisons (e.g., if (emailServerConfig.port == null)) for host,
port, and username so only null or undefined are considered missing and still
push the same keys into missingFields; locate these checks on the
emailServerConfig object and replace !emailServerConfig.host /
!emailServerConfig.port / !emailServerConfig.username with == null variants.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afccb8d and 6d4e37c.

📒 Files selected for processing (2)
  • apps/backend/.env.development
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx

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

♻️ Duplicate comments (3)
apps/backend/src/lib/managed-email-onboarding.tsx (3)

179-185: ⚠️ Potential issue | 🟠 Major

Add explicit timeouts/abort signals to outbound DNSimple/Resend requests.

These fetch calls run on request paths without a timeout (Line 182 and similar), so upstream hangs can pin workers and degrade API reliability. Please route all provider calls through a timeout-aware helper and pass signal.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 179 - 185,
The outgoing fetch in listDnsimpleZones (and other provider call sites such as
Resend requests) lacks an abort/timeout; wrap these calls with a timeout-aware
helper (e.g., fetchWithTimeout or a shared requestWithAbort) and pass an
AbortSignal into fetch via the options.signal property so upstream hangs can't
pin workers; update listDnsimpleZones to accept or create an AbortController (or
accept a signal parameter) and call fetch(..., { method: "GET", headers:
getDnsimpleHeaders(), signal }) or delegate to the shared helper that enforces a
timeout and exposes the signal to callers.

551-573: ⚠️ Potential issue | 🟠 Major

Make setup idempotent before external side effects.

createResendDomain/DNS operations happen before createManagedEmailDomain. If the DB write fails, retries can hit “already exists” and become unrecoverable for the same subdomain. Persist a pending row first (or add robust external-resource reuse on retry).

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 551 - 573,
The flow performs external side effects (createResendDomain,
createOrReuseDnsimpleZone, upsertDnsimpleResendRecords,
getDnsimpleZoneNameServers) before recording the managed domain in the DB, which
breaks idempotency on retries; change createManagedEmailDomain to persist a
pending row for normalizedSubdomain first (status "pending_verification" and
without resendDomainId), then perform
createResendDomain/createOrReuseDnsimpleZone/upsertDnsimpleResendRecords/getDnsimpleZoneNameServers,
and finally update the persisted row with resendDomainId, nameServerRecords and
final status (or reuse existing row if present). Ensure
createManagedEmailDomain/managedDomainToSetupResult logic supports
creating/updating the pending row and handles existing rows to avoid duplicate
external resource errors on retry.

608-635: ⚠️ Potential issue | 🟠 Major

Guard apply with an atomic status transition before key creation.

Concurrent apply requests can both pass the verified check and mint multiple scoped keys before Line 634 updates status. Use an atomic conditional transition (e.g., verified -> applying) and proceed only when exactly one caller acquires it.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 608 - 635,
Before creating keys, perform an atomic conditional status transition on the
managed domain from "verified" to "applying" so only one caller proceeds: use
getManagedEmailDomainByResendDomainId to locate the domain, then run an atomic
update (e.g., SQL/ORM conditional update or compare-and-swap) that sets status =
"applying" WHERE id = domain.id AND status = "verified" and check the
affected-rows; if the update did not affect a row, re-fetch the domain status
and return/throw appropriately (e.g., return current status or 409), otherwise
proceed to call createResendScopedKey, saveManagedEmailProviderConfig, then
markManagedEmailDomainApplied (or set final status) to complete the flow; ensure
you reference domain.id, domain.resendDomainId, domain.subdomain and tenancy.id
when performing the atomic update and subsequent operations.
🤖 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/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 179-190: listDnsimpleZones currently only fetches
page=1/per_page=100 and can miss results; change it to iterate DNSimple pages
until exhausted by either following the HTTP Link header or incrementing a page
query param (use per_page=100) and accumulating results across requests using
parseDnsimpleJsonOrThrow on each response, then apply the existing
normalizeDomainName filter to the combined list before returning. Apply the same
pagination loop to the other DNSimple listing call in this file (the second
DNSimple list function near the other list call) so both listing functions fully
iterate all pages.
- Around line 119-128: After parsing the Resend domain response into the local
variable body (the { id: string, name: string, records?: ResendDomainRecord[],
status?: ResendDomain["status"] } object), validate that body.id and body.name
are present and non-empty before using them (e.g., before constructing the fetch
to `https://api.resend.com/domains/${body.id}/verify`); if either is missing
throw a StackAssertionError (or the same explicit error used in the file) with a
clear errorContext string. Follow the existing validation pattern used around
lines 485–496: perform explicit checks on body.id/body.name immediately after
await response.json(), and only proceed to the verifyResponse fetch when those
checks pass. Ensure the error includes the response content so debugging is
possible.

---

Duplicate comments:
In `@apps/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 179-185: The outgoing fetch in listDnsimpleZones (and other
provider call sites such as Resend requests) lacks an abort/timeout; wrap these
calls with a timeout-aware helper (e.g., fetchWithTimeout or a shared
requestWithAbort) and pass an AbortSignal into fetch via the options.signal
property so upstream hangs can't pin workers; update listDnsimpleZones to accept
or create an AbortController (or accept a signal parameter) and call fetch(...,
{ method: "GET", headers: getDnsimpleHeaders(), signal }) or delegate to the
shared helper that enforces a timeout and exposes the signal to callers.
- Around line 551-573: The flow performs external side effects
(createResendDomain, createOrReuseDnsimpleZone, upsertDnsimpleResendRecords,
getDnsimpleZoneNameServers) before recording the managed domain in the DB, which
breaks idempotency on retries; change createManagedEmailDomain to persist a
pending row for normalizedSubdomain first (status "pending_verification" and
without resendDomainId), then perform
createResendDomain/createOrReuseDnsimpleZone/upsertDnsimpleResendRecords/getDnsimpleZoneNameServers,
and finally update the persisted row with resendDomainId, nameServerRecords and
final status (or reuse existing row if present). Ensure
createManagedEmailDomain/managedDomainToSetupResult logic supports
creating/updating the pending row and handles existing rows to avoid duplicate
external resource errors on retry.
- Around line 608-635: Before creating keys, perform an atomic conditional
status transition on the managed domain from "verified" to "applying" so only
one caller proceeds: use getManagedEmailDomainByResendDomainId to locate the
domain, then run an atomic update (e.g., SQL/ORM conditional update or
compare-and-swap) that sets status = "applying" WHERE id = domain.id AND status
= "verified" and check the affected-rows; if the update did not affect a row,
re-fetch the domain status and return/throw appropriately (e.g., return current
status or 409), otherwise proceed to call createResendScopedKey,
saveManagedEmailProviderConfig, then markManagedEmailDomainApplied (or set final
status) to complete the flow; ensure you reference domain.id,
domain.resendDomainId, domain.subdomain and tenancy.id when performing the
atomic update and subsequent operations.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d4e37c and f95f93d.

📒 Files selected for processing (2)
  • apps/backend/prisma/migrations/20260224000000_managed_email_domains/migration.sql
  • apps/backend/src/lib/managed-email-onboarding.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/prisma/migrations/20260224000000_managed_email_domains/migration.sql

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

♻️ Duplicate comments (6)
apps/backend/src/lib/managed-email-domains.tsx (1)

47-61: Silent fallback in status mappers — previously flagged.

Both dbStatusToStatus and statusToDbStatus still fall through to a default return value ("failed" / "FAILED") rather than throwing on unknown enum values.

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

In `@apps/backend/src/lib/managed-email-domains.tsx` around lines 47 - 61, The two
mapper functions dbStatusToStatus and statusToDbStatus currently return a silent
default ("failed"/"FAILED") for unknown enum values; change them to be
exhaustive by throwing a clear error when an unexpected status is encountered
(e.g., throw new Error including the incoming status value) instead of falling
through, so update dbStatusToStatus and statusToDbStatus to throw on any
unrecognized input to surface programmer errors.
apps/backend/src/lib/managed-email-onboarding.tsx (5)

271-282: DNSimple DNS record listing is also page-limited — previously flagged.

listDnsimpleDnsRecords hard-codes page=1&per_page=100, mirroring the same pagination gap flagged for listDnsimpleZones.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 271 - 282,
The function listDnsimpleDnsRecords currently hard-codes page=1&per_page=100
causing missed records; change it to page through all results and return the
aggregated array. In listDnsimpleDnsRecords, repeatedly call fetch for the zone
records (using getDnsimpleBaseUrl, getDnsimpleAccountId and getDnsimpleHeaders)
advancing the page parameter (or using Link/next from response.headers if
available) and parse each response with
parseDnsimpleJsonOrThrow<DnsimpleDnsRecord[]>; accumulate results into a single
array and stop when a page returns an empty array (or no next link). Ensure the
final promise resolves to the full combined DnsimpleDnsRecord[].

445-468: Missing validation of body.id / body.name before use — previously flagged.

The cast at line 445 does not validate that body.id and body.name are non-empty strings before they are used at line 447 (URL interpolation) and line 459 (error context).

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 445 - 468,
Validate that body.id and body.name are present and non-empty strings before
using them: check typeof body.id === "string" && body.id.trim() !== "" (and same
for body.name) immediately after parsing the JSON, and if validation fails,
throw a StackAssertionError (or return a clear error) instead of proceeding to
call fetch for verifyResponse or interpolating the URL; ensure any thrown error
includes the original response text/context to aid debugging and only use
body.id/body.name when the checks pass.

551-573: Non-idempotent setup: external side-effects before DB persistence — previously flagged.

Resend domain creation and DNSimple zone setup happen before the DB row is committed, making retries after a partial failure unrecoverable.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 551 - 573,
The current flow calls createResendDomain, createOrReuseDnsimpleZone,
upsertDnsimpleResendRecords and getDnsimpleZoneNameServers before persisting
with createManagedEmailDomain, which makes retries non-idempotent; change the
flow to persist a DB row first (call createManagedEmailDomain with a provisional
status like "creating" or "pending") using the same fields except external IDs,
then perform external side-effects (createResendDomain,
createOrReuseDnsimpleZone, upsertDnsimpleResendRecords,
getDnsimpleZoneNameServers), and finally update the persisted row with
resendDomain.id, nameServerRecords and the final status (verified or
pending_verification); ensure managedDomainToSetupResult reads the updated row
or return the updated object after the DB update so partial failures can be
retried safely.

179-190: Missing HTTP timeouts on all fetch calls — previously flagged.

All DNSimple and Resend fetch calls (here and at lines 196, 209, 241, 273, 307, 423, 473) lack an AbortSignal / timeout, allowing upstream hangs to pin worker threads.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 179 - 190,
The fetch in listDnsimpleZones (and the other DNSimple/Resend fetches flagged)
lacks a timeout/AbortSignal; update listDnsimpleZones to create an
AbortController with a short configurable timeout, pass controller.signal into
fetch, clear the timeout on success, and reject/throw a clear timeout error when
aborted; follow the same pattern for the other functions that call fetch (the
other DNSimple/Resend calls referenced), ensuring you still call
parseDnsimpleJsonOrThrow/getDnsimpleHeaders as before and that the controller is
cleaned up to avoid leaks.

608-636: Race condition between verified check and createResendScopedKey — previously flagged.

Concurrent applyManagedEmailProvider calls can both pass the domain.status !== "verified" guard and mint duplicate scoped Resend keys before markManagedEmailDomainApplied serialises them.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 608 - 636,
Current code can mint duplicate Resend keys because multiple callers can pass
the domain.status === "verified" guard before one marks it applied; fix by
making the transition from "verified" to a reserved state atomic and only
creating the scoped key after you win that atomic update. Update the flow around
getManagedEmailDomainByResendDomainId/createResendScopedKey/markManagedEmailDomainApplied/saveManagedEmailProviderConfig
so that you: attempt a conditional DB update (e.g. markManagedEmailDomainApplied
or a new method) that sets status FROM "verified" TO "applying" (or directly to
"applied") only if the current status === "verified" and returns whether the
update succeeded; if the conditional update fails, re-fetch the domain and
return its current status; if it succeeds, createResendScopedKey and then
persist the provider config and finalize the status to "applied" (or update
"applying"→"applied") — or alternatively implement SELECT ... FOR UPDATE
wrapping the read/create/save sequence in a single transaction so only one
caller can create the key. Use the existing function names
getManagedEmailDomainByResendDomainId, createResendScopedKey and
markManagedEmailDomainApplied (or extend markManagedEmailDomainApplied to
perform the conditional/CAS update) so callers cannot race.
🧹 Nitpick comments (3)
apps/backend/src/lib/managed-email-onboarding.tsx (2)

257-268: match[1] is string | undefined — add a defensive guard per coding guidelines.

TypeScript types regex capture groups as string | undefined. Even though the pattern guarantees a capture when match is truthy, the guidelines require explicit null/undefined checks or ?? throwErr(...) rather than relying on implicit regex semantics.

♻️ Proposed fix
-    const nameServer = normalizeRecordContent(match[1]);
+    const nameServer = normalizeRecordContent(match[1] ?? throwErr("NS regex matched but capture group 1 was undefined"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 257 - 268,
The regex capture match[1] in the loop inside the function reading rawZoneFile
can be undefined even when match is truthy; update the loop to defensively
handle that by extracting the capture into a local variable (e.g., const
captured = match[1]) and then either continue when it's null/undefined or throw
via a helper (per guidelines) before calling normalizeRecordContent and adding
to nameServerSet; ensure you reference match, normalizeRecordContent, and
nameServerSet when making the check so no implicit assumption about match[1]
remains.

103-110: Use explicit != null instead of boolean truthiness check for zoneSubdomainLabel.

zoneLabels[0] is string | undefined. The coding guidelines prefer explicit null/undefined checks (zoneSubdomainLabel != null) over boolean coercions (if (zoneSubdomainLabel && ...)) to avoid treating empty-string edge cases silently.

♻️ Proposed fix
-  if (zoneSubdomainLabel && normalizedName.endsWith(`.${zoneSubdomainLabel}`)) {
+  if (zoneSubdomainLabel != null && normalizedName.endsWith(`.${zoneSubdomainLabel}`)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 103 - 110,
The boolean truthiness check for zoneSubdomainLabel should be replaced with an
explicit null/undefined check to avoid treating an empty string as falsy; in the
block that computes zoneLabels/zoneSubdomainLabel (references: zoneLabels,
zoneSubdomainLabel, normalizedZoneName, normalizedName,
recordWithoutZoneSubdomainLabel), change the condition from "if
(zoneSubdomainLabel && normalizedName.endsWith(...))" to use "zoneSubdomainLabel
!= null && normalizedName.endsWith(...)" so empty-string values are handled
correctly while still guarding against null/undefined.
apps/backend/src/lib/managed-email-domains.tsx (1)

108-108: Prefer ?? throwErr(...) over non-null assertion !.

The length guard above makes rows[0] safe at runtime, but the project guidelines ask for ?? throwErr(...) over ! so the assumption is explicit. The same applies to lines 121, 173, and 191.

♻️ Proposed refactor (all four sites)
-  return mapRow(rows[0]!);    // line 108
+  return mapRow(rows[0] ?? throwErr("getManagedEmailDomainByTenancyAndSubdomain: expected row after length check"));

-  return mapRow(rows[0]!);    // line 121
+  return mapRow(rows[0] ?? throwErr("getManagedEmailDomainByResendDomainId: expected row after length check"));

-  return mapRow(rows[0]!);    // line 173
+  return mapRow(rows[0] ?? throwErr("updateManagedEmailDomainWebhookStatus: expected RETURNING row"));

-  return mapRow(rows[0]!);    // line 191
+  return mapRow(rows[0] ?? throwErr("markManagedEmailDomainApplied: expected RETURNING row"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/managed-email-domains.tsx` at line 108, Replace the
non-null assertion used when passing DB rows into mapRow with an explicit
null-coalescing throw so the assumption is explicit: change occurrences like
return mapRow(rows[0]!) to return mapRow(rows[0] ?? throwErr("expected row not
found")) (use the project throwErr helper and a concise message), and apply the
same pattern at the other sites that pass rows[0] into mapRow (the occurrences
flagged at lines 121, 173, and 191).
🤖 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/backend/src/lib/managed-email-domains.tsx`:
- Around line 94-109: getManagedEmailDomainByTenancyAndSubdomain currently
returns rows regardless of active state causing setupManagedEmailProvider and
checkManagedEmailProviderStatus to short‑circuit on deactivated records; update
the SQL in getManagedEmailDomainByTenancyAndSubdomain to include AND "isActive"
= true in the WHERE clause so only active ManagedEmailDomain rows are returned
(keep returning null when no rows) and leave mapRow usage unchanged.

---

Duplicate comments:
In `@apps/backend/src/lib/managed-email-domains.tsx`:
- Around line 47-61: The two mapper functions dbStatusToStatus and
statusToDbStatus currently return a silent default ("failed"/"FAILED") for
unknown enum values; change them to be exhaustive by throwing a clear error when
an unexpected status is encountered (e.g., throw new Error including the
incoming status value) instead of falling through, so update dbStatusToStatus
and statusToDbStatus to throw on any unrecognized input to surface programmer
errors.

In `@apps/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 271-282: The function listDnsimpleDnsRecords currently hard-codes
page=1&per_page=100 causing missed records; change it to page through all
results and return the aggregated array. In listDnsimpleDnsRecords, repeatedly
call fetch for the zone records (using getDnsimpleBaseUrl, getDnsimpleAccountId
and getDnsimpleHeaders) advancing the page parameter (or using Link/next from
response.headers if available) and parse each response with
parseDnsimpleJsonOrThrow<DnsimpleDnsRecord[]>; accumulate results into a single
array and stop when a page returns an empty array (or no next link). Ensure the
final promise resolves to the full combined DnsimpleDnsRecord[].
- Around line 445-468: Validate that body.id and body.name are present and
non-empty strings before using them: check typeof body.id === "string" &&
body.id.trim() !== "" (and same for body.name) immediately after parsing the
JSON, and if validation fails, throw a StackAssertionError (or return a clear
error) instead of proceeding to call fetch for verifyResponse or interpolating
the URL; ensure any thrown error includes the original response text/context to
aid debugging and only use body.id/body.name when the checks pass.
- Around line 551-573: The current flow calls createResendDomain,
createOrReuseDnsimpleZone, upsertDnsimpleResendRecords and
getDnsimpleZoneNameServers before persisting with createManagedEmailDomain,
which makes retries non-idempotent; change the flow to persist a DB row first
(call createManagedEmailDomain with a provisional status like "creating" or
"pending") using the same fields except external IDs, then perform external
side-effects (createResendDomain, createOrReuseDnsimpleZone,
upsertDnsimpleResendRecords, getDnsimpleZoneNameServers), and finally update the
persisted row with resendDomain.id, nameServerRecords and the final status
(verified or pending_verification); ensure managedDomainToSetupResult reads the
updated row or return the updated object after the DB update so partial failures
can be retried safely.
- Around line 179-190: The fetch in listDnsimpleZones (and the other
DNSimple/Resend fetches flagged) lacks a timeout/AbortSignal; update
listDnsimpleZones to create an AbortController with a short configurable
timeout, pass controller.signal into fetch, clear the timeout on success, and
reject/throw a clear timeout error when aborted; follow the same pattern for the
other functions that call fetch (the other DNSimple/Resend calls referenced),
ensuring you still call parseDnsimpleJsonOrThrow/getDnsimpleHeaders as before
and that the controller is cleaned up to avoid leaks.
- Around line 608-636: Current code can mint duplicate Resend keys because
multiple callers can pass the domain.status === "verified" guard before one
marks it applied; fix by making the transition from "verified" to a reserved
state atomic and only creating the scoped key after you win that atomic update.
Update the flow around
getManagedEmailDomainByResendDomainId/createResendScopedKey/markManagedEmailDomainApplied/saveManagedEmailProviderConfig
so that you: attempt a conditional DB update (e.g. markManagedEmailDomainApplied
or a new method) that sets status FROM "verified" TO "applying" (or directly to
"applied") only if the current status === "verified" and returns whether the
update succeeded; if the conditional update fails, re-fetch the domain and
return its current status; if it succeeds, createResendScopedKey and then
persist the provider config and finalize the status to "applied" (or update
"applying"→"applied") — or alternatively implement SELECT ... FOR UPDATE
wrapping the read/create/save sequence in a single transaction so only one
caller can create the key. Use the existing function names
getManagedEmailDomainByResendDomainId, createResendScopedKey and
markManagedEmailDomainApplied (or extend markManagedEmailDomainApplied to
perform the conditional/CAS update) so callers cannot race.

---

Nitpick comments:
In `@apps/backend/src/lib/managed-email-domains.tsx`:
- Line 108: Replace the non-null assertion used when passing DB rows into mapRow
with an explicit null-coalescing throw so the assumption is explicit: change
occurrences like return mapRow(rows[0]!) to return mapRow(rows[0] ??
throwErr("expected row not found")) (use the project throwErr helper and a
concise message), and apply the same pattern at the other sites that pass
rows[0] into mapRow (the occurrences flagged at lines 121, 173, and 191).

In `@apps/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 257-268: The regex capture match[1] in the loop inside the
function reading rawZoneFile can be undefined even when match is truthy; update
the loop to defensively handle that by extracting the capture into a local
variable (e.g., const captured = match[1]) and then either continue when it's
null/undefined or throw via a helper (per guidelines) before calling
normalizeRecordContent and adding to nameServerSet; ensure you reference match,
normalizeRecordContent, and nameServerSet when making the check so no implicit
assumption about match[1] remains.
- Around line 103-110: The boolean truthiness check for zoneSubdomainLabel
should be replaced with an explicit null/undefined check to avoid treating an
empty string as falsy; in the block that computes zoneLabels/zoneSubdomainLabel
(references: zoneLabels, zoneSubdomainLabel, normalizedZoneName, normalizedName,
recordWithoutZoneSubdomainLabel), change the condition from "if
(zoneSubdomainLabel && normalizedName.endsWith(...))" to use "zoneSubdomainLabel
!= null && normalizedName.endsWith(...)" so empty-string values are handled
correctly while still guarding against null/undefined.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f95f93d and b1a19a3.

📒 Files selected for processing (3)
  • apps/backend/src/lib/managed-email-domains.tsx
  • apps/backend/src/lib/managed-email-onboarding.tsx
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts

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

♻️ Duplicate comments (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (2)

977-981: Falsy presence checks for SMTP fields — still unaddressed.

!emailServerConfig.host, !emailServerConfig.port, and !emailServerConfig.username treat 0/"" as missing. Use explicit == null checks. This was previously flagged.

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

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx
around lines 977 - 981, Replace the falsy checks that treat valid values like 0
or "" as missing by using explicit null/undefined checks: in the block that
inspects emailServerConfig when emailServerConfig.provider !== "managed", change
the conditions for host, port, and username from
"!emailServerConfig.host"/"!emailServerConfig.port"/"!emailServerConfig.username"
to checks that test for null or undefined (e.g., emailServerConfig.host ==
null), and continue pushing the same field names onto missingFields when those
explicit checks indicate absence; update the logic in the surrounding
conditional that references emailServerConfig/provider to ensure behavior is
unchanged for non-managed providers.

240-256: managedSubdomain and managedSenderLocalPart still render as "undefined" when not set.

The past fix was applied to earlier commits but the current code at Lines 245 and 254 still renders these fields directly without a null guard. When provider === "managed" but the fields are not yet populated, React will render the literal string "undefined".

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

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx
around lines 240 - 256, The two spans displaying emailConfig.managedSubdomain
and emailConfig.managedSenderLocalPart can render the literal "undefined" when
provider === "managed" but those fields are not yet set; update the rendering in
page-client.tsx so the values are null-guarded (e.g., render an empty string or
a placeholder like "—" when emailConfig.managedSubdomain or
emailConfig.managedSenderLocalPart is undefined or falsy). Keep the existing
provider === "managed" checks and change only the span contents that reference
emailConfig.managedSubdomain and emailConfig.managedSenderLocalPart to safely
coalesce to a fallback value.
apps/backend/src/lib/managed-email-onboarding.tsx (3)

445-447: body.id used without null-check before the verify fetch.

This was previously flagged and remains unaddressed. Validate body.id and body.name exist before constructing the verification URL.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 445 - 447,
Validate that the parsed response object (the variable named body from the const
body = await response.json(...) assignment) contains non-empty body.id and
body.name before calling
fetch(`https://api.resend.com/domains/${body.id}/verify`); add a guard that
checks both fields, log or throw a clear error (or return early) if either is
missing, and only construct/perform the verify fetch when body.id and body.name
are present so you never interpolate an undefined id into the verification URL.

179-191: HTTP timeout and pagination concerns still unaddressed.

The fetch-without-timeout and page=1 hard-coded pagination issues were previously flagged and remain in the current code.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 179 - 191, In
listDnsimpleZones: add an HTTP timeout using AbortController (or a shared
fetch-with-timeout helper) so the fetch call respects a configurable timeout
instead of hanging, and replace the hard-coded single-page fetch with paginated
fetching that loops (incrementing page) to accumulate results (use per_page as a
constant or config and stop when returned page length < per_page or when no more
pages); continue to call getDnsimpleBaseUrl, getDnsimpleAccountId,
getDnsimpleHeaders, and parseDnsimpleJsonOrThrow for each page and filter the
final accumulated list with normalizeDomainName(zone.name) ===
normalizeDomainName(subdomain).

551-573: External resources created before DB row persisted — still unaddressed.

The idempotency concern (Resend domain + DNSimple zone created before createManagedEmailDomain) was previously raised and remains in the current code.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 551 - 573,
External resources (createResendDomain, createOrReuseDnsimpleZone,
upsertDnsimpleResendRecords, getDnsimpleZoneNameServers) are created before
persisting the DB row (createManagedEmailDomain), risking orphaned resources if
DB insert fails; change flow to first persist a provisional managed-email row
(e.g., status "provisioning" or similar) via createManagedEmailDomain, then call
createResendDomain/createOrReuseDnsimpleZone/upsertDnsimpleResendRecords/getDnsimpleZoneNameServers,
and finally update that row with resendDomainId, nameServerRecords and final
status (or implement compensating cleanup to delete resendDomain and DNSimple
zone on DB failure if you prefer rollback instead of provisional row); update
managedDomainToSetupResult usage to read from the persisted/updated row.
🧹 Nitpick comments (4)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (3)

991-1008: Redundant inner provider === "managed" checks after the outer discriminant — consider restructuring.

The outer condition at Line 992 already narrows to provider === 'resend' || provider === 'managed'. The five inner eslint-disable-next-line @typescript-eslint/no-unnecessary-condition`` comments on repeated provider === "managed" checks indicate TypeScript already knows these are redundant. Extracting the managed branch into its own `if/else` block would eliminate the suppressed warnings and make the intent explicit.

♻️ Proposed refactor
-      // eslint-disable-next-line `@typescript-eslint/no-unnecessary-condition`
-      const emailConfig: AdminEmailConfig = emailServerConfig.provider === 'resend' || emailServerConfig.provider === 'managed' ? {
-        type: 'resend',
-        // eslint-disable-next-line `@typescript-eslint/no-unnecessary-condition`
-        host: emailServerConfig.provider === "managed" ? "smtp.resend.com" : (emailServerConfig.host ?? throwErr("Email host is missing")),
-        // eslint-disable-next-line `@typescript-eslint/no-unnecessary-condition`
-        port: emailServerConfig.provider === "managed" ? 465 : (emailServerConfig.port ?? throwErr("Email port is missing")),
-        // eslint-disable-next-line `@typescript-eslint/no-unnecessary-condition`
-        username: emailServerConfig.provider === "managed" ? "resend" : (emailServerConfig.username ?? throwErr("Email username is missing")),
-        password: emailServerConfig.password ?? throwErr("Email password is missing"),
-        // eslint-disable-next-line `@typescript-eslint/no-unnecessary-condition`
-        senderName: emailServerConfig.provider === "managed" ? project.displayName : (emailServerConfig.senderName ?? throwErr("Email sender name is missing")),
-        // eslint-disable-next-line `@typescript-eslint/no-unnecessary-condition`
-        senderEmail: emailServerConfig.provider === "managed"
-          ? (emailServerConfig.managedSubdomain && emailServerConfig.managedSenderLocalPart
-            ? `${emailServerConfig.managedSenderLocalPart}@${emailServerConfig.managedSubdomain}`
-            : throwErr("Managed sender config is missing"))
-          : (emailServerConfig.senderEmail ?? throwErr("Email sender email is missing")),
-      } : {
+      let emailConfig: AdminEmailConfig;
+      if (emailServerConfig.provider === 'managed') {
+        emailConfig = {
+          type: 'resend',
+          host: 'smtp.resend.com',
+          port: 465,
+          username: 'resend',
+          password: emailServerConfig.password ?? throwErr("Email password is missing"),
+          senderName: project.displayName,
+          senderEmail: (emailServerConfig.managedSubdomain && emailServerConfig.managedSenderLocalPart)
+            ? `${emailServerConfig.managedSenderLocalPart}@${emailServerConfig.managedSubdomain}`
+            : throwErr("Managed sender config is missing"),
+        };
+      } else if (emailServerConfig.provider === 'resend') {
+        emailConfig = {
+          type: 'resend',
+          host: emailServerConfig.host ?? throwErr("Email host is missing"),
+          port: emailServerConfig.port ?? throwErr("Email port is missing"),
+          username: emailServerConfig.username ?? throwErr("Email username is missing"),
+          password: emailServerConfig.password ?? throwErr("Email password is missing"),
+          senderName: emailServerConfig.senderName ?? throwErr("Email sender name is missing"),
+          senderEmail: emailServerConfig.senderEmail ?? throwErr("Email sender email is missing"),
+        };
+      } else {
+        emailConfig = {
           type: 'standard',
           host: emailServerConfig.host ?? throwErr("Email host is missing"),
           port: emailServerConfig.port ?? throwErr("Email port is missing"),
           username: emailServerConfig.username ?? throwErr("Email username is missing"),
           password: emailServerConfig.password ?? throwErr("Email password is missing"),
           senderName: emailServerConfig.senderName ?? throwErr("Email sender name is missing"),
           senderEmail: emailServerConfig.senderEmail ?? throwErr("Email sender email is missing"),
-      };
+        };
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx
around lines 991 - 1008, The emailConfig construction repeats provider ===
"managed" checks inside a branch already guarded by emailServerConfig.provider
=== 'resend' || 'managed', causing redundant conditions and suppressed ESLint
warnings; refactor by splitting the logic into two explicit branches (e.g., if
(emailServerConfig.provider === 'managed') { ... } else { ... }) when building
emailConfig so the managed-specific values (host, port, username, senderName,
senderEmail managed-subdomain logic) are set in the managed branch and the
resend/default values use the other branch, referencing the same identifiers
(emailConfig, emailServerConfig, project, throwErr) to remove the unnecessary
inner provider checks and eslint-disable comments.

787-793: Using toast for the "managed unchanged" path hides feedback and is UX-confusing.

When a user selects "managed" and clicks Save, the form closes and a toast briefly appears. The user gets no clear indication that nothing was saved. Per the AGENTS.md guidance, blocking feedback should use alerts (not toasts). A better pattern is to prevent form submission with an inline <Alert> inside the render block (already shown at Lines 873–887) and not allow the form to "submit" the managed type at all.

💡 Suggested direction

Return "prevent-close" from the managed branch and display the informational content inline (already done in the render block at Lines 873–887) rather than using a toast + silent close.

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

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx
around lines 787 - 793, In the branch that handles values.type === 'managed'
(the save/submit handler where the toast is currently shown), remove the toast
call and instead return the sentinel string "prevent-close" so the parent/form
does not close; rely on the existing inline Alert in the component's render
block to show the informational message for the managed path. Specifically, edit
the handler (the function that checks values.type === 'managed') to stop
performing a close or success toast, return "prevent-close" from that branch,
and ensure no other code treats that branch as a successful submit.

198-205: "Managed Setup" button is always visible regardless of the current email provider.

Unlike the "Test" button (which is hidden for shared providers), the "Managed Setup" button appears even when the project is using shared or custom SMTP email. Consider conditionally showing it (e.g., hide when emailConfig.isShared or when emailConfig.provider === "managed" and already applied), to reduce UI noise.

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

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx
around lines 198 - 205, The "Managed Setup" button should be conditionally
rendered like the "Test" button: update the render around
ManagedEmailSetupDialog so it only shows when the project is not using a shared
provider and is not already using the managed provider (e.g., wrap
ManagedEmailSetupDialog in a check such as !emailConfig.isShared &&
emailConfig.provider !== 'managed'); reference the ManagedEmailSetupDialog
component and the emailConfig object (emailConfig.isShared and
emailConfig.provider) to locate and implement the conditional rendering.
apps/backend/src/lib/managed-email-onboarding.tsx (1)

638-657: Consider logging when no domain row is found for a webhook event.

updateManagedEmailDomainWebhookStatus may silently affect 0 rows if the resendDomainId is unknown (e.g., stale webhook for a deleted or externally-created domain). A structured log at this point would aid debugging without changing the webhook's success response contract.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 638 - 657, In
processResendDomainWebhookEvent, capture the result from
updateManagedEmailWebhookStatus (or modify updateManagedEmailDomainWebhookStatus
to return the number of affected rows) and if it indicates 0 rows updated, emit
a structured warning log (e.g., using processLogger.warn or your request logger)
that includes resendDomainId (options.domainId), providerStatusRaw, computed
mappedStatus, and errorMessage; reference the symbols
processResendDomainWebhookEvent and updateManagedEmailDomainWebhookStatus when
making the change so the caller checks the returned affected-row count and logs
the stale/unknown domain webhook event.
🤖 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/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 619-635: The Resend scoped key created by createResendScopedKey
can be orphaned if saveManagedEmailProviderConfig or
markManagedEmailDomainApplied throws; modify the flow so the external key is
either persisted locally before activating the domain or is cleaned up on
failure: after calling createResendScopedKey (or when
shouldUseMockManagedEmailOnboarding is false) immediately persist the key into
the ManagedEmailDomain row (or call saveManagedEmailProviderConfig first to
store it) and only then call markManagedEmailDomainApplied, OR wrap the
save/mark calls in a try/catch and on any error call a new
deleteResendScopedKey(key) helper to revoke the minted key before rethrowing;
update code paths around createResendScopedKey, saveManagedEmailProviderConfig,
and markManagedEmailDomainApplied accordingly to ensure no orphaned keys.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 277-285: The senderLocalPart Yup schema is only checking
non-emptiness; update the schema for senderLocalPart to mirror backend
validation by adding a matches(...) check for the allowed characters
/^[a-zA-Z0-9._%+-]+$/ (use the same error message as the backend) instead of—or
in addition to—the current .test; locate the senderLocalPart schema in
page-client.tsx and make the regex validation consistent with the rule used in
managed-email-onboarding.tsx so users get inline validation for invalid
characters.

---

Duplicate comments:
In `@apps/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 445-447: Validate that the parsed response object (the variable
named body from the const body = await response.json(...) assignment) contains
non-empty body.id and body.name before calling
fetch(`https://api.resend.com/domains/${body.id}/verify`); add a guard that
checks both fields, log or throw a clear error (or return early) if either is
missing, and only construct/perform the verify fetch when body.id and body.name
are present so you never interpolate an undefined id into the verification URL.
- Around line 179-191: In listDnsimpleZones: add an HTTP timeout using
AbortController (or a shared fetch-with-timeout helper) so the fetch call
respects a configurable timeout instead of hanging, and replace the hard-coded
single-page fetch with paginated fetching that loops (incrementing page) to
accumulate results (use per_page as a constant or config and stop when returned
page length < per_page or when no more pages); continue to call
getDnsimpleBaseUrl, getDnsimpleAccountId, getDnsimpleHeaders, and
parseDnsimpleJsonOrThrow for each page and filter the final accumulated list
with normalizeDomainName(zone.name) === normalizeDomainName(subdomain).
- Around line 551-573: External resources (createResendDomain,
createOrReuseDnsimpleZone, upsertDnsimpleResendRecords,
getDnsimpleZoneNameServers) are created before persisting the DB row
(createManagedEmailDomain), risking orphaned resources if DB insert fails;
change flow to first persist a provisional managed-email row (e.g., status
"provisioning" or similar) via createManagedEmailDomain, then call
createResendDomain/createOrReuseDnsimpleZone/upsertDnsimpleResendRecords/getDnsimpleZoneNameServers,
and finally update that row with resendDomainId, nameServerRecords and final
status (or implement compensating cleanup to delete resendDomain and DNSimple
zone on DB failure if you prefer rollback instead of provisional row); update
managedDomainToSetupResult usage to read from the persisted/updated row.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 977-981: Replace the falsy checks that treat valid values like 0
or "" as missing by using explicit null/undefined checks: in the block that
inspects emailServerConfig when emailServerConfig.provider !== "managed", change
the conditions for host, port, and username from
"!emailServerConfig.host"/"!emailServerConfig.port"/"!emailServerConfig.username"
to checks that test for null or undefined (e.g., emailServerConfig.host ==
null), and continue pushing the same field names onto missingFields when those
explicit checks indicate absence; update the logic in the surrounding
conditional that references emailServerConfig/provider to ensure behavior is
unchanged for non-managed providers.
- Around line 240-256: The two spans displaying emailConfig.managedSubdomain and
emailConfig.managedSenderLocalPart can render the literal "undefined" when
provider === "managed" but those fields are not yet set; update the rendering in
page-client.tsx so the values are null-guarded (e.g., render an empty string or
a placeholder like "—" when emailConfig.managedSubdomain or
emailConfig.managedSenderLocalPart is undefined or falsy). Keep the existing
provider === "managed" checks and change only the span contents that reference
emailConfig.managedSubdomain and emailConfig.managedSenderLocalPart to safely
coalesce to a fallback value.

---

Nitpick comments:
In `@apps/backend/src/lib/managed-email-onboarding.tsx`:
- Around line 638-657: In processResendDomainWebhookEvent, capture the result
from updateManagedEmailWebhookStatus (or modify
updateManagedEmailDomainWebhookStatus to return the number of affected rows) and
if it indicates 0 rows updated, emit a structured warning log (e.g., using
processLogger.warn or your request logger) that includes resendDomainId
(options.domainId), providerStatusRaw, computed mappedStatus, and errorMessage;
reference the symbols processResendDomainWebhookEvent and
updateManagedEmailDomainWebhookStatus when making the change so the caller
checks the returned affected-row count and logs the stale/unknown domain webhook
event.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 991-1008: The emailConfig construction repeats provider ===
"managed" checks inside a branch already guarded by emailServerConfig.provider
=== 'resend' || 'managed', causing redundant conditions and suppressed ESLint
warnings; refactor by splitting the logic into two explicit branches (e.g., if
(emailServerConfig.provider === 'managed') { ... } else { ... }) when building
emailConfig so the managed-specific values (host, port, username, senderName,
senderEmail managed-subdomain logic) are set in the managed branch and the
resend/default values use the other branch, referencing the same identifiers
(emailConfig, emailServerConfig, project, throwErr) to remove the unnecessary
inner provider checks and eslint-disable comments.
- Around line 787-793: In the branch that handles values.type === 'managed' (the
save/submit handler where the toast is currently shown), remove the toast call
and instead return the sentinel string "prevent-close" so the parent/form does
not close; rely on the existing inline Alert in the component's render block to
show the informational message for the managed path. Specifically, edit the
handler (the function that checks values.type === 'managed') to stop performing
a close or success toast, return "prevent-close" from that branch, and ensure no
other code treats that branch as a successful submit.
- Around line 198-205: The "Managed Setup" button should be conditionally
rendered like the "Test" button: update the render around
ManagedEmailSetupDialog so it only shows when the project is not using a shared
provider and is not already using the managed provider (e.g., wrap
ManagedEmailSetupDialog in a check such as !emailConfig.isShared &&
emailConfig.provider !== 'managed'); reference the ManagedEmailSetupDialog
component and the emailConfig object (emailConfig.isShared and
emailConfig.provider) to locate and implement the conditional rendering.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1a19a3 and a7480c6.

📒 Files selected for processing (2)
  • apps/backend/src/lib/managed-email-onboarding.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx

Comment on lines +619 to +635
const resendApiKey = shouldUseMockManagedEmailOnboarding()
? `managed_mock_key_${options.tenancy.id}`
: await createResendScopedKey({
subdomain: domain.subdomain,
domainId: domain.resendDomainId,
tenancyId: options.tenancy.id,
});

await saveManagedEmailProviderConfig({
tenancy: options.tenancy,
resendApiKey,
subdomain: domain.subdomain,
senderLocalPart: domain.senderLocalPart,
});

await markManagedEmailDomainApplied(domain.id);
return { status: "applied" };
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

Resend scoped key is leaked if saveManagedEmailProviderConfig or markManagedEmailDomainApplied fails.

createResendScopedKey mints an external API key (Lines 619–625). If either subsequent step throws, the key is created in Resend but never persisted or tracked. Each retry then mints another orphaned key. Additionally, if markManagedEmailDomainApplied fails after saveManagedEmailProviderConfig succeeds, the domain stays in "verified" and the next retry issues yet another key.

Consider saving the scoped key into the ManagedEmailDomain row before (or atomically with) activating it — or delete the key on failure via Resend's delete-key API in a rollback step.

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

In `@apps/backend/src/lib/managed-email-onboarding.tsx` around lines 619 - 635,
The Resend scoped key created by createResendScopedKey can be orphaned if
saveManagedEmailProviderConfig or markManagedEmailDomainApplied throws; modify
the flow so the external key is either persisted locally before activating the
domain or is cleaned up on failure: after calling createResendScopedKey (or when
shouldUseMockManagedEmailOnboarding is false) immediately persist the key into
the ManagedEmailDomain row (or call saveManagedEmailProviderConfig first to
store it) and only then call markManagedEmailDomainApplied, OR wrap the
save/mark calls in a try/catch and on any error call a new
deleteResendScopedKey(key) helper to revoke the minted key before rethrowing;
update code paths around createResendScopedKey, saveManagedEmailProviderConfig,
and markManagedEmailDomainApplied accordingly to ensure no orphaned keys.

Comment on lines +277 to +285
senderLocalPart: yup
.string()
.trim()
.defined("Sender local part is required")
.test(
"non-empty-sender-local-part",
"Sender local part is required",
(value) => value.trim().length > 0,
),
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 | 🟡 Minor

senderLocalPart frontend schema is weaker than backend validation.

The backend enforces /^[a-zA-Z0-9._%+-]+$/ (line 77 of managed-email-onboarding.tsx), but the frontend schema only validates non-emptiness. Users with invalid characters (e.g., spaces) will hit a backend error instead of an inline form validation message.

🔧 Proposed fix
  senderLocalPart: yup
    .string()
    .trim()
    .defined("Sender local part is required")
    .test(
      "non-empty-sender-local-part",
      "Sender local part is required",
      (value) => value.trim().length > 0,
    )
+   .matches(
+     /^[a-zA-Z0-9._%+-]+$/,
+     "Sender local part may only contain letters, digits, and . _ % + -",
+   ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx
around lines 277 - 285, The senderLocalPart Yup schema is only checking
non-emptiness; update the schema for senderLocalPart to mirror backend
validation by adding a matches(...) check for the allowed characters
/^[a-zA-Z0-9._%+-]+$/ (use the same error message as the backend) instead of—or
in addition to—the current .test; locate the senderLocalPart schema in
page-client.tsx and make the regex validation consistent with the rule used in
managed-email-onboarding.tsx so users get inline validation for invalid
characters.

@BilalG1 BilalG1 requested a review from N2D4 February 25, 2026 23:21
@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Feb 25, 2026
Copy link
Copy Markdown
Contributor

@N2D4 N2D4 left a comment

Choose a reason for hiding this comment

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

can you create prod accounts & envvars when you merge this?

@github-actions github-actions bot assigned BilalG1 and unassigned N2D4 Mar 10, 2026
@BilalG1 BilalG1 merged commit b701fdf into dev Mar 10, 2026
28 checks passed
@BilalG1 BilalG1 deleted the managed-emails-provider branch March 10, 2026 03:23
mantrakp04 pushed a commit that referenced this pull request Mar 10, 2026
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Managed email domain onboarding: setup, DNS provisioning,
verification, status checks, and apply flow (Resend-backed).
* **UI**
* Project email settings: managed-provider setup dialog, managed sender
fields, status display, and test-send mapping.
* **Integrations**
* DNS provider automation and Resend webhook handling for domain status
updates; scoped keys for sending.
* **API**
* Admin endpoints / client APIs to setup, check, list, and apply managed
email domains.
* **Tests**
  * End-to-end tests covering the full onboarding flow.
* **Chores**
* Added environment variables and config schema support for Resend and
DNS integrations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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