Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
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 }
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 }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryImplemented 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
Important Files Changed
Sequence DiagramsequenceDiagram
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
Last reviewed commit: afccb8d |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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 comparingtypeof props.okButtonwith"boolean", while the equivalent code inSmartFormDialog(Lines 22, 27) uses===. This is functionally equivalent here sincetypeofalways 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 acrossemails.tsxandconfig.tsx.The Resend SMTP host (
smtp.resend.com), port (465), and username (resend) are hardcoded identically in bothapps/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.tsxthrows aStackAssertionErrorifmanagedSubdomainormanagedSenderLocalPartis missing, while this code silently falls back tosenderEmail(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 usingStatusError(400)for missing webhook payload fields.When
domainIdorproviderStatusRawis missing from adomain.updatedpayload, the code throwsStackAssertionErrorwhich 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 fordomain.updatedevents, 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/applytoo.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
📒 Files selected for processing (21)
apps/backend/.env.developmentapps/backend/prisma/migrations/20260224000000_managed_email_domains/migration.sqlapps/backend/prisma/schema.prismaapps/backend/src/app/api/latest/integrations/resend/webhooks/route.tsxapps/backend/src/app/api/latest/internal/emails/managed-onboarding/apply/route.tsxapps/backend/src/app/api/latest/internal/emails/managed-onboarding/check/route.tsxapps/backend/src/app/api/latest/internal/emails/managed-onboarding/list/route.tsxapps/backend/src/app/api/latest/internal/emails/managed-onboarding/setup/route.tsxapps/backend/src/lib/config.tsxapps/backend/src/lib/emails.tsxapps/backend/src/lib/managed-email-domains.tsxapps/backend/src/lib/managed-email-onboarding.tsxapps/backend/src/lib/projects.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsxapps/dashboard/src/components/form-dialog.tsxapps/e2e/tests/backend/endpoints/api/v1/internal/managed-email-onboarding.test.tspackages/stack-shared/src/config/schema-fuzzer.test.tspackages/stack-shared/src/config/schema.tspackages/stack-shared/src/interface/admin-interface.tspackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.tspackages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
apps/backend/prisma/migrations/20260224000000_managed_email_domains/migration.sql
Show resolved
Hide resolved
| 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" }; |
There was a problem hiding this comment.
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.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Show resolved
Hide resolved
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
Show resolved
Hide resolved
…th/stack-auth into managed-emails-provider
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (1)
952-956:⚠️ Potential issue | 🟡 MinorUse
== nullchecks instead of falsy checks for SMTP field validation.
!emailServerConfig.porttreats0as 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
📒 Files selected for processing (2)
apps/backend/.env.developmentapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
apps/backend/src/lib/managed-email-onboarding.tsx (3)
179-185:⚠️ Potential issue | 🟠 MajorAdd explicit timeouts/abort signals to outbound DNSimple/Resend requests.
These
fetchcalls 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 passsignal.🤖 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 | 🟠 MajorMake setup idempotent before external side effects.
createResendDomain/DNS operations happen beforecreateManagedEmailDomain. 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 | 🟠 MajorGuard 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
📒 Files selected for processing (2)
apps/backend/prisma/migrations/20260224000000_managed_email_domains/migration.sqlapps/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
There was a problem hiding this comment.
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
dbStatusToStatusandstatusToDbStatusstill 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.
listDnsimpleDnsRecordshard-codespage=1&per_page=100, mirroring the same pagination gap flagged forlistDnsimpleZones.🤖 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 ofbody.id/body.namebefore use — previously flagged.The cast at line 445 does not validate that
body.idandbody.nameare 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
fetchcalls (here and at lines 196, 209, 241, 273, 307, 423, 473) lack anAbortSignal/ 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 betweenverifiedcheck andcreateResendScopedKey— previously flagged.Concurrent
applyManagedEmailProvidercalls can both pass thedomain.status !== "verified"guard and mint duplicate scoped Resend keys beforemarkManagedEmailDomainAppliedserialises 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]isstring | undefined— add a defensive guard per coding guidelines.TypeScript types regex capture groups as
string | undefined. Even though the pattern guarantees a capture whenmatchis 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!= nullinstead of boolean truthiness check forzoneSubdomainLabel.
zoneLabels[0]isstring | 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
📒 Files selected for processing (3)
apps/backend/src/lib/managed-email-domains.tsxapps/backend/src/lib/managed-email-onboarding.tsxpackages/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
There was a problem hiding this comment.
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.usernametreat0/""as missing. Use explicit== nullchecks. 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:managedSubdomainandmanagedSenderLocalPartstill 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.idused without null-check before the verify fetch.This was previously flagged and remains unaddressed. Validate
body.idandbody.nameexist 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 innerprovider === "managed"checks after the outer discriminant — consider restructuring.The outer condition at Line 992 already narrows to
provider === 'resend' || provider === 'managed'. The five innereslint-disable-next-line@typescript-eslint/no-unnecessary-condition`` comments on repeatedprovider === "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: Usingtoastfor 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 therenderblock (already shown at Lines 873–887) and not allow the form to "submit" the managed type at all.💡 Suggested direction
Return
"prevent-close"from themanagedbranch and display the informational content inline (already done in therenderblock 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.isSharedor whenemailConfig.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.
updateManagedEmailDomainWebhookStatusmay silently affect 0 rows if theresendDomainIdis 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
📒 Files selected for processing (2)
apps/backend/src/lib/managed-email-onboarding.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
| 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" }; |
There was a problem hiding this comment.
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.
| 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, | ||
| ), |
There was a problem hiding this comment.
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.
N2D4
left a comment
There was a problem hiding this comment.
can you create prod accounts & envvars when you merge this?
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Outdated
Show resolved
Hide resolved
<!-- 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 -->
Summary by CodeRabbit