Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@N2D4
Copy link
Contributor

@N2D4 N2D4 commented Nov 26, 2025

I'm sorry for whoever reviews this. I tried to split it up as much as possible so this is only the existing functionality migrated onto the new email outbox table; the new endpoints or UI changes are not here yet.

Tests not passing yet but already ready for reviews.

How to review:

  • The most important file is schema.prisma. It contains the explanation of the new EmailOutbox table, and all its constraints.
  • After that, check the updated emails.tsx. It writes to the EmailOutbox table.
  • Then, check email-queue-step.tsx. It contains the logic that renders, queues, and sends an email (alongside emails-low-level.tsx). This is called once every ~second (in a Vercel cron job in prod or a script in dev) and whenever a new email is sent (see the callers of runEmailQueueStep for more info).
  • Then, look at the updated endpoints. Would love to have some eyes on the ones that @BilalG1 wrote in particular.
  • Finally, everything else. Everything else should be relatively straight-forward, I think, mostly migrations.

The prompt that generated the first version (but note that the final version is somewhat different from what was generated, but it might help get some overview):

i want you to implement a new email sending backend infrastructure. apps/backend is the right folder

let's start with the database. For this purpose, I've already created an EmailOutbox model in the prisma schema. Note that, for now, canHaveDeliveryInfo is always false (we never fetch delivery info rn). Note that the EmailOutbox is on the global database, not the source-of-truth database (which means it's not in the same database as the project users, for example, and can't have foreign key relations to those). You must create a new migration for this new schema; you will have to manually write the migration SQL for it (can't use the Prisma command). After that, create another subsequent migration that migrates from the old SentEmail table to the new one.

Create a file in `lib` that has functions for the email outbox for listing all emails (returning the database objects), editing an email, resetting an email, deleting an email (which should only work under the same conditions as the retry one). This file should be SUPER clean, make sure to have many explained/documented functions with concise docs.

You also want to update emails.tsx to use the new email outbox. The usual current email-sending functions should instead of sending the email directly, create an EmailOutbox row in the database instead. It should also queue the email-queue-step below onto QStash so that it can start queuing itself repeatedly (this should probably be exported as a function so. Keep the existing function implementations somewhere for your own reference until you've implemented the email queue step endpoint down below, as you'll need to implement the email queue step endpoint down below with them. Note that if you send from a templtae to many users, each user is its own row in the email outbox system. Feel free to update the signature of the send email functions to match the information that is required in EmailOutbox, just update all the callers.

Also, update the `email-rendering.tsx` file to export a new function that allows you to render many email templates/drafts/themes at once, as long as they are for the same tenancy ID. They should all be in a single call to Freestyle.

Note that difference between InternalError and ExternalError in the EmailOutbox schema. Internal errors contain all the information, but maybe have sensitive implementaiton-speciifc data that should not exposed to the end user who sent the email. External errors are guaranteed to be safe to expose to the end user who sent the email. There is already a similar handling that's implicit in the emails.tsx file, so take some of its logic and make it more explicit (for both rendering & sending server errors, make functions that take internal full error objects and returns an external error object, with just a string like "Internal server error" if it's an unknown error). 

Also, create the following endpoints (please look at other endpoints in the backend to see how they're implemented, and make sure the security is good):

- GET /api/latest/emails/delivery-info: This endpoint returns some stats on the emails. It should return the number of emails sent in the last hour, last day, last week, and last month, and how many of those emails (per hour, day, week, and month) were marked as spam or bounced. It should also return the "capacity rate": the number of emails that can be sent per second. It should be calculated like this: `max(max(<the number of emails sent in the last week> * 4, 200) * penaltyFactor, 5) / 7 / 60 / 60`, where penaltyFactor is a number between 0 and 1 depending on what % of emails were bounced or marked as spam (I will build a precise metric for this later, for now you can think about it and make your own).
- GET /api/latest/emails/outbox: List all emails in the outbox, paginated like the users endpoint. Should return a nice type for the individual rows which should at least contain the status and simple status, and probably some other useful fields that you think should be displayed to the user in the UI, but not all those from the database (if you're not sure whether it's needed, don't include it, we can include it later). The function which converts DB EmailOutbox objects to the API type should be separate from the other stuff as we will use it elsewhere too.
- PATCH /api/latest/emails/outbox/{id}: Edit an email in the outbox, if the conditions for it are met. Only some fields can be edited, see the Prisma schema for an explanation
- POST /api/latest/emails/outbox/{id}/cancel: Cancel an email in the outbox, if the conditions for it are met
- POST /api/latest/emails/outbox/{id}/retry: Retry an email in the outbox, if the conditions for it are met. See the Prisma schema for an explanation
- POST /api/latest/internal/email-queue-step: This is a QStash receiver that is constantly called by QStash in an infinite loop. Make sure that it is super clean and easy to understand. Feel free to split it up into multiple functions and files as needed.
- - First thing it should do is add a call to itself back into the QStash queue, with Flow Control with a flow control key equal to the URL, a rate limit of 1 per second, and a parallelism limit of 1.
- - Then, it should acquire a Postgres exclusive lock, to make sure that only one email-queue-step can be running at a time (make sure this is easy to understand and free of data races). It should be released automatically at the end of this endpoint (think about how we can make it that there's no deadlock if the endpoint fails for whatever reason, eg. the server it's running on dies).
- - Now, it should get the timestamp of the last execution of this endpoint (which is stored in some metadata table in the global database), and calculate the precise time delta in seconds. The new time should be updated in the metadata table. (Make sure that it's not negative, which can happen due to clock skew, so make sure to handle that by just not doing anything if the new time is before the last execution time).
- - Then, it should fetch all the email outbox rows that do not have a renderedByWorkerId, and set renderedByWorkerId to a random UUID and startedRenderingAt to now (it should do this in an atomic statement so that an email is never rendered by multiple workers at the same time). It should then use email-rendering.tsx to render the email, using the previously created batch-by-tenancy function (you need to group the email outbox rows by tenancy ID for this). Once done, it should (again, in a single atomic statement) check whether the renderedByWorkerId is still set to the same UUID (this is to make sure there are no race conditions), and if so, set the appropriate fields in the EmailOutbox row.
- - Next, it should run a SQL query that updates all rows with scheduledAt < now() and isQueued = false to set isQueued = true.
- - Now, it should fetch a deduplicated list of all tenancy IDs of all email outbox rows that have isQueued = true and startedSendingAt being null, and set startedSendingAt to now.
- - For each tenancy ID in the list:
- - - Calculate the capacity rate. Multiply it by the time delta since the last endpoint execution. Use stochastic rounding to round to the nearest integer.
- - - For that tenancy, fetch at most that many email outbox rows that have isQueued = true and startedSendingAt being null, and set startedSendingAt to now. Fetch those rows that have the highest priority (see the Prisma schema for an explanation).
- - - Then, asynchronously in parallel, process each email that needs to be sent. Use the email-sending functions to send the emails, and update finishedSendingAt and canHaveDeliveryInfo (recall it's always set to false for now) for each row when the email is sent. Before you send the email, check whether the user still exists in the DB, and use the same notification category logic from the send-email endpoint (extract it into a helper function) to find out which users have already unsubscribed and should not receive the email, and set skippedReason accordingly if necessary. If the email encountered an error while sending, set the sendServerError-ish fields in the same atomic statement. `waitUntil` each promise (to make sure that they continue running in the background). Ensure that a bug when handling one of the emails does not mess with the other emails. Don't use `.catch`, ever, use try-catch blocks, and use `captureError("<some-machine-readable-location-id>", ...)` to capture all unknown errors aggressively.


While you're at it, we now have some new features like scheduling, add those to the send-email endpoint.

Make sure to make VERY comprehensive E2E tests for ALL the edge cases.

The code should be ULTRA clean and NEVER repetitive, feel free to create helper functions and files as needed. Definitely make sure everything is clean and easy to understand. When there's some logic that is used in multiple places, create a helper function for it. In the end, write a human-readable README.md file in the email endpoint folder that explains how the email infrastructure works, meant to be read by a technical human (it should be VERY concise and understandable!)


Note

Migrates email to an async outbox pipeline with queueing/rendering, delivery stats, updated APIs, and extensive tests.

  • Database/Migrations:
    • Add EmailOutbox schema with generated columns, constraints, and indices; migrate data from SentEmail and drop it.
  • Backend/Logic:
    • Implement outbox-driven flow: enqueue via sendEmailToMany, batch render (Freestyle), queue, and send (low-level SMTP/provider) with robust error handling and skip reasons.
    • Add periodic processor: runEmailQueueStep and internal endpoint /api/latest/internal/email-queue-step with locking, delta-based capacity, and stochastic quotas.
    • Extract low-level email sender, batched renderer, and delivery stats calculator.
  • APIs:
    • Update POST /api/latest/emails/send-email (per-user enqueue, variables JSON, priority/scheduling, notification category validation).
    • Add GET /api/latest/emails/delivery-info (hour/day/week/month stats and capacity).
    • Switch internal email listing to outbox-backed data shape.
  • SDK/Template:
    • Expose getEmailDeliveryStats and useEmailDeliveryStats; server interface method added.
  • Tests:
    • Comprehensive E2E for queue step, delivery stats, unsubscribe, drafts/templates, variables, all-users sends, and edge cases.
    • Test helpers improved (mailbox wait utilities, auth fast signup).
  • Infra/Tooling:
    • Add Vercel cron for email queue step; dev script to run queue locally.
    • Docker deps: add Drizzle Gateway; tweak Postgres and compose; CI workflow renamed to build/run.
    • Misc refactors (ESM config, Next/Sentry adjustments), README for email pipeline.

Written by Cursor Bugbot for commit 313c229. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Asynchronous Email Outbox pipeline (queuing, rendering, scheduling, priority, drafts) and internal queue runner
    • Delivery statistics API with capacity metrics
    • Send-email API: draft/template/html support, high-priority flag, and results now return user_id
    • Unsubscribe handling and two‑pass template rendering; unsubscribe links
  • Bug Fixes

    • Improved deliverability checks, error reporting, retries and skip conditions
  • Documentation

    • Email infrastructure README added
  • Tests

    • Extensive e2e and unit tests for email flows and delivery-info endpoint

✏️ Tip: You can customize this high-level summary in your review settings.

@N2D4 N2D4 requested a review from BilalG1 November 26, 2025 07:30
@cmux-agent
Copy link

cmux-agent bot commented Dec 12, 2025

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Copy link
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: 0

♻️ Duplicate comments (8)
docker/server/entrypoint.sh (1)

77-98: Unresolved: path quoting and word-splitting risks remain.

The past review flagged two outstanding issues in this section that still need attention:

  1. Line 97: find $WORK_DIR/apps is unquoted. Should be find "$WORK_DIR/apps" to safely handle paths with spaces.
  2. Line 77: for sentinel in $unhandled_sentinels; do risks word-splitting if $unhandled_sentinels contains newlines or whitespace. Although sentinels match [A-Z_], the pattern is fragile. Consider a newline-safe loop:
-for sentinel in $unhandled_sentinels; do
+while IFS= read -r sentinel; do
+  [ -z "$sentinel" ] && continue

And update the done:

-done
+done <<EOF
+$unhandled_sentinels
+EOF
.github/workflows/docker-server-build-run.yaml (1)

35-76: Health-check hardening looks good; this addresses prior feedback.
You’re now (1) checking backend via /health, and (2) guarding against curl failures + empty status codes + quoting vars—matching the earlier review concerns.

apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts (2)

101-101: Fixed sleep assertions can yield false positives.

These tests use fixed wait() delays followed by "no messages" assertions. This pattern cannot distinguish between "email correctly skipped" and "processor never ran," making tests both flaky and prone to false positives.

Consider driving the processor explicitly or asserting against outbox status via the delivery APIs to confirm skip logic.

Also applies to: 166-166, 228-228, 301-301

Also applies to: 166-166, 228-228, 301-301


331-333: Placeholder snapshot must be replaced.

The snapshot placeholder "todo" needs to be replaced with the actual expected response body after running the test.

apps/backend/src/lib/email-queue-step.tsx (2)

224-231: Verify themeProps is accepted by rendering function.

The buildRenderRequest includes themeProps.projectLogos in the input object. Past review flagged that renderEmailsForTenancyBatched may not accept this field in its type signature.

Verify that the rendering function accepts themeProps in the input:

#!/bin/bash
# Check the RenderEmailRequestForTenancy type definition
rg -nP --type=ts -A10 'type\s+RenderEmailRequestForTenancy|interface\s+RenderEmailRequestForTenancy' apps/backend/src/lib/email-rendering.tsx

79-94: Missing automatic recovery for emails stuck in sending.

logEmailsStuckInSending only logs emails stuck in the sending state, while retryEmailsStuckInRendering (lines 59-77) actively resets stuck rendering jobs. This asymmetry means emails that crash during sending remain stuck indefinitely with startedSendingAt set but finishedSendingAt null, requiring manual intervention.

Consider adding automatic recovery for stuck sending jobs similar to rendering.

apps/backend/prisma/schema.prisma (1)

726-729: Verify bidirectional constraints are enforced in migration.

The comments document bidirectional constraints for emailDraftId and emailProgrammaticCallTemplateId:

  • emailDraftId: "must be set if and only if createdWith is DRAFT"
  • emailProgrammaticCallTemplateId: "Must be NOT set if createdWith is NOT PROGRAMMATIC_CALL"

Past review noted the migration may only enforce one direction of these constraints.

Verify the migration includes both directions of the constraint:

#!/bin/bash
# Check if both directions of emailDraftId constraint exist
rg -nP 'email_draft.*CHECK|EmailOutbox_email_draft' apps/backend/prisma/migrations/20251020180000_email_outbox/migration.sql
apps/backend/src/app/api/latest/internal/failed-emails-digest/crud.tsx (1)

67-68: Arrays accumulate duplicates from SQL join.

The SQL query produces a row per failed email per team member. Both emails (line 67) and tenantOwnerEmails (line 68) accumulate duplicate entries. The TODO comment acknowledges this but the issue remains unresolved.

Consider deduplicating before the loop or using a Map/Set to track unique entries.

🧹 Nitpick comments (2)
.github/workflows/docker-server-build-run.yaml (1)

22-24: Avoid postgres:latest in CI; pin and clean up containers.
Using postgres:latest can break unpredictably. Pin to a major/minor (or at least major) version aligned with prod/dev (e.g. postgres:16). Also consider cleaning up the db container (and stackframe-server) at the end (or via trap) to reduce flakiness on runners that reuse Docker state.

apps/backend/src/lib/emails.tsx (1)

62-62: Remove unnecessary non-null assertion.

serializeRecipient(recipient)! uses a non-null assertion, but the function always returns Json (never null). The assertion is unnecessary.

Apply this diff:

-      to: serializeRecipient(recipient)!,
+      to: serializeRecipient(recipient),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 400d23f and e3decb1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • .github/workflows/docker-server-build-run.yaml (1 hunks)
  • apps/backend/prisma/migrations/20251020180000_email_outbox/migration.sql (1 hunks)
  • apps/backend/prisma/schema.prisma (4 hunks)
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx (5 hunks)
  • apps/backend/src/app/api/latest/internal/failed-emails-digest/crud.tsx (4 hunks)
  • apps/backend/src/lib/email-queue-step.tsx (1 hunks)
  • apps/backend/src/lib/emails.tsx (3 hunks)
  • apps/e2e/package.json (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts (1 hunks)
  • docker/server/entrypoint.sh (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Always add new E2E tests when changing the API or SDK interface
For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Use ES6 maps instead of records wherever you can

Files:

  • apps/backend/src/lib/email-queue-step.tsx
  • apps/backend/src/app/api/latest/internal/failed-emails-digest/crud.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
  • apps/backend/src/lib/emails.tsx
**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,css}: Keep hover/click transitions snappy and fast; avoid fade-in delays on hover. Apply transitions after action completion instead, like smooth fade-out when hover ends
Use hover-exit transitions instead of hover-enter transitions; for example, use 'transition-colors hover:transition-none' instead of fade-in on hover

Files:

  • apps/backend/src/lib/email-queue-step.tsx
  • apps/backend/src/app/api/latest/internal/failed-emails-digest/crud.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
  • apps/backend/src/lib/emails.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static

Files:

  • apps/backend/src/lib/email-queue-step.tsx
  • apps/backend/src/app/api/latest/internal/failed-emails-digest/crud.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
  • apps/backend/src/lib/emails.tsx
{.env*,**/*.{ts,tsx,js}}

📄 CodeRabbit inference engine (AGENTS.md)

Prefix environment variables with STACK_ (or NEXT_PUBLIC_STACK_ if public) so changes are picked up by Turborepo and improves readability

Files:

  • apps/backend/src/lib/email-queue-step.tsx
  • apps/backend/src/app/api/latest/internal/failed-emails-digest/crud.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
  • apps/backend/src/lib/emails.tsx
apps/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps

Files:

  • apps/backend/src/lib/email-queue-step.tsx
  • apps/backend/src/app/api/latest/internal/failed-emails-digest/crud.tsx
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
  • apps/backend/src/lib/emails.tsx
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer .toMatchInlineSnapshot over other selectors in tests when possible; check snapshot-serializer.ts for formatting details

Files:

  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
apps/e2e/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always add new E2E tests when changing API or SDK interface; err on the side of creating too many tests due to the critical nature of the industry

Files:

  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
🧠 Learnings (5)
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.{ts,tsx} : Always add new E2E tests when changing the API or SDK interface

Applied to files:

  • apps/e2e/package.json
  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to apps/e2e/**/*.{ts,tsx} : Always add new E2E tests when changing API or SDK interface; err on the side of creating too many tests due to the critical nature of the industry

Applied to files:

  • apps/e2e/package.json
  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Run pnpm install to install dependencies, pnpm test run for testing with Vitest, pnpm lint for linting (use --fix flag to auto-fix), pnpm typecheck for type checking

Applied to files:

  • apps/e2e/package.json
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to **/*.test.{ts,tsx} : Prefer .toMatchInlineSnapshot over other selectors in tests when possible; check snapshot-serializer.ts for formatting details

Applied to files:

  • apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to packages/stack-shared/src/config/schema.ts : Whenever making backwards-incompatible changes to the config schema, update the migration functions in packages/stack-shared/src/config/schema.ts

Applied to files:

  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
🧬 Code graph analysis (2)
apps/backend/src/app/api/latest/internal/failed-emails-digest/crud.tsx (1)
apps/backend/src/lib/emails.tsx (1)
  • EmailOutboxRecipient (21-24)
apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts (6)
apps/e2e/tests/helpers/ports.ts (1)
  • withPortPrefix (6-6)
packages/stack-shared/src/utils/strings.tsx (1)
  • deindent (235-238)
apps/e2e/tests/helpers.ts (1)
  • it (12-12)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • backendContext (35-57)
  • niceBackendFetch (109-173)
  • bumpEmailAddress (179-187)
packages/stack-shared/src/utils/promises.tsx (1)
  • wait (260-268)
packages/stack-shared/src/utils/html.tsx (1)
  • html (30-32)
🪛 actionlint (1.7.9)
.github/workflows/docker-server-build-run.yaml

16-16: label "ubicloud-standard-8" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xl", "macos-latest-xlarge", "macos-latest-large", "macos-26-xlarge", "macos-26", "macos-15-intel", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xl", "macos-14-xlarge", "macos-14-large", "macos-14", "macos-13-xl", "macos-13-xlarge", "macos-13-large", "macos-13", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file

(runner-label)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: Cursor Bugbot
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: restart-dev-and-test
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: all-good
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: docker
🔇 Additional comments (8)
docker/server/entrypoint.sh (1)

3-3: Good catch removing -x from set options.

Removing the -x flag from set -exo pipefailset -eo pipefail correctly prevents bash xtrace from leaking the generated secrets (lines 14–16) and environment variable values into Docker logs. This is a solid security fix.

apps/e2e/package.json (2)

18-20: No action needed. The E2E test suite has a clean, single-point env-loading pattern via global-setup.ts, which explicitly calls dotenv.config() with specific file paths. Vitest doesn't auto-load .env files without explicit configuration (not present), so there is no double-loading or conflict with Vitest/Vite env behavior. Both dotenv and js-beautify are actively used: dotenv in global-setup.ts and js-beautify in email-queue.test.ts for HTML snapshot formatting.


19-23: js-beautify + @types/js-beautify usage is appropriate and already stable.

The dependency is used in email-queue.test.ts to format HTML for snapshot comparisons via .toMatchInlineSnapshot(). Since beautify.html() is called with default options at a single location, output is already normalized and stable—no separate helper needed.

Verify this PR introduces no breaking changes to email API or SDK interfaces that would require additional E2E test coverage per the guidelines.

apps/backend/prisma/migrations/20251020180000_email_outbox/migration.sql (1)

59-60: LGTM! Status logic correctly keys off error fields.

The RENDER_ERROR condition now checks renderErrorExternalMessage IS NOT NULL instead of renderedHtml IS NULL, which properly handles text-only renders and aligns with the constraint logic.

apps/backend/src/lib/email-queue-step.tsx (1)

592-602: LGTM! Empty email array now properly skipped.

The code correctly handles the case where user-custom-emails has an empty array by checking emails.length === 0 and returning NO_EMAIL_PROVIDED skip reason (line 595). This addresses the previous concern about empty arrays being marked as successfully sent.

apps/backend/src/app/api/latest/emails/send-email/route.tsx (1)

143-160: LGTM! Duplicate user IDs properly handled.

The code now deduplicates user_ids via [...new Set(body.user_ids)] (line 144) before validation, preventing false UserIdDoesNotExist errors when duplicates are present in the request.

apps/backend/src/lib/emails.tsx (1)

92-92: LGTM! Priority correctly set to true for default templates.

The isHighPriority flag is now unconditionally true for emails sent via sendEmailFromDefaultTemplate, ensuring these critical emails (verification, password reset, etc.) jump the queue as intended.

apps/backend/prisma/schema.prisma (1)

798-800: LGTM! sentAt comment now matches generated expression.

The comment correctly describes the generated column logic: when canHaveDeliveryInfo is true, sentAt equals deliveredAt; when false, it equals finishedSendingAt. This aligns with the dbgenerated CASE expression.

@N2D4 N2D4 disabled auto-merge December 12, 2025 02:55
@cmux-agent
Copy link

cmux-agent bot commented Dec 12, 2025

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

@N2D4 N2D4 enabled auto-merge (squash) December 12, 2025 09:42
Copy link
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: 0

🧹 Nitpick comments (1)
apps/backend/src/auto-migrations/index.tsx (1)

104-196: Avoid sleeping while holding the advisory xact lock (and a DB connection)

await wait(500) runs inside the Prisma transaction, so it holds pg_advisory_xact_lock(...) and the transaction connection for the extra 500ms, which can amplify contention/timeouts when multiple workers race migrations. Prefer committing first, then sleeping before the next retry.

       await options.prismaClient.$transaction(async (tx) => {
+        let requestedRepeatDelayMs: number | null = null;
         log(`  |> Preparing...`);
         await tx.$executeRaw`
           SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_ID});
         `;
@@
                 if (res[0].should_repeat_migration) {
                   log(`  |> Migration ${migration.migrationName} requested to be repeated. This is normal and *not* indicative of a problem.`);
-                  await wait(500);  // give the database a chance to catch up with everything else that's happening
+                  requestedRepeatDelayMs = 500; // delay after commit to avoid holding locks/connections
                   // Commit the transaction and continue re-running the migration
                   return;
                 }
@@
         newlyAppliedMigrationNames.push(migration.migrationName);
         shouldRepeat = false;
-      }, {
+        if (requestedRepeatDelayMs != null) {
+          // keep shouldRepeat=true, but sleep outside of the transaction
+        }
+      }, {
         timeout: 80_000,
         maxWait: 30_000,
       });
+      // If we returned early to repeat, wait here after commit/connection release.
+      if (shouldRepeat) {
+        await wait(500);
+      }
     }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3decb1 and c7fc0a0.

📒 Files selected for processing (2)
  • apps/backend/prisma/migrations/20251020183000_migrate_sent_email/migration.sql (1 hunks)
  • apps/backend/src/auto-migrations/index.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/prisma/migrations/20251020183000_migrate_sent_email/migration.sql
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Always add new E2E tests when changing the API or SDK interface
For blocking alerts and errors, never use toast; use alerts instead as they are less easily missed by the user
NEVER try-catch-all, NEVER void a promise, and NEVER .catch(console.error); use loading indicators and async callbacks instead, or use runAsynchronously/runAsynchronouslyWithAlert for error handling
Use ES6 maps instead of records wherever you can

Files:

  • apps/backend/src/auto-migrations/index.tsx
**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,css}: Keep hover/click transitions snappy and fast; avoid fade-in delays on hover. Apply transitions after action completion instead, like smooth fade-out when hover ends
Use hover-exit transitions instead of hover-enter transitions; for example, use 'transition-colors hover:transition-none' instead of fade-in on hover

Files:

  • apps/backend/src/auto-migrations/index.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

NEVER use Next.js dynamic functions if you can avoid them; prefer using client components and hooks like usePathname instead of await params to keep pages static

Files:

  • apps/backend/src/auto-migrations/index.tsx
{.env*,**/*.{ts,tsx,js}}

📄 CodeRabbit inference engine (AGENTS.md)

Prefix environment variables with STACK_ (or NEXT_PUBLIC_STACK_ if public) so changes are picked up by Turborepo and improves readability

Files:

  • apps/backend/src/auto-migrations/index.tsx
apps/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Check existing apps for inspiration when implementing new apps or pages; update apps-frontend.tsx and apps-config.ts to add new apps

Files:

  • apps/backend/src/auto-migrations/index.tsx
🧠 Learnings (1)
📚 Learning: 2025-12-04T18:03:49.984Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T18:03:49.984Z
Learning: Applies to packages/stack-shared/src/config/schema.ts : Whenever making backwards-incompatible changes to the config schema, update the migration functions in packages/stack-shared/src/config/schema.ts

Applied to files:

  • apps/backend/src/auto-migrations/index.tsx
🧬 Code graph analysis (1)
apps/backend/src/auto-migrations/index.tsx (1)
packages/stack-shared/src/utils/promises.tsx (1)
  • wait (260-268)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: Cursor Bugbot
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: setup-tests
  • GitHub Check: build (22.x)
  • GitHub Check: all-good
🔇 Additional comments (1)
apps/backend/src/auto-migrations/index.tsx (1)

4-4: wait import path/version stability

Importing from @stackframe/stack-shared/dist/... is fine if that path is treated as public API; otherwise it’s easy to break on package/build changes. Consider importing from a stable/public entrypoint if one exists.

@cmux-agent
Copy link

cmux-agent bot commented Dec 12, 2025

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

@cmux-agent
Copy link

cmux-agent bot commented Dec 12, 2025

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Copy link
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

🧹 Nitpick comments (3)
apps/backend/prisma/migrations/20251212183000_migrate_sent_email/migration.sql (1)

4-15: Optional: make batching deterministic to reduce long-tail/dup work
Consider adding an ORDER BY se."id" (or createdAt/id) in to_migrate to avoid non-deterministic LIMIT selection across runs.

apps/backend/prisma/migrations/20251212180000_email_outbox/migration.sql (2)

92-95: Priority ordering vs indexes: priority ASC likely fights “high priority first”
priority assigns 100 for high-priority (Line 93), but EmailOutbox_sending_idx is (tenancyId, priority, scheduledAt) (Line 248) and EmailOutbox_ordering_idx stores priority ASC (Line 256). If the processor selects “highest priority first”, these indexes won’t support that ordering well (and may encourage lowest-priority-first scans).

Also applies to: 248-258


251-257: Double-check scheduledAtIfNotYetQueued DESC aligns with “send oldest first”
scheduledAtIfNotYetQueued is scheduledAt when not queued (Line 109-111), but ordering_idx uses scheduledAtIfNotYetQueued DESC (Line 255). If you intend FIFO by scheduled time, this likely wants ASC (or you need matching query ORDER BY).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3f2303 and 1a4f46e.

📒 Files selected for processing (3)
  • apps/backend/prisma/migrations/20251212180000_email_outbox/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251212183000_migrate_sent_email/migration.sql (1 hunks)
  • apps/backend/prisma/migrations/20251212185000_add_no_email_provided_skip_reason/migration.sql (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/backend/prisma/migrations/20251212185000_add_no_email_provided_skip_reason/migration.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: restart-dev-and-test
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: docker
  • GitHub Check: setup-tests
  • GitHub Check: all-good
  • GitHub Check: Vercel Agent Review
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (2)
apps/backend/prisma/migrations/20251212183000_migrate_sent_email/migration.sql (2)

1-3: The migration runner properly gates statements 2/3 (INSERT metadata and DROP TABLE) on statement 1 completion. When statement 1 returns should_repeat_migration = true, line 155 of index.tsx returns from the transaction callback, preventing execution of remaining statements in that iteration. Statements 2/3 only execute after should_repeat_migration = false. The SPLIT_STATEMENT_SENTINEL and CONDITIONALLY_REPEAT_MIGRATION_SENTINEL are control-flow markers (not just SQL comments) that the runner uses to gate conditional logic—the outer loop (for (let repeat = 0; shouldRepeat; repeat++) at line 105) re-runs the migration until completion. This design eliminates the risk of premature DROP TABLE execution.

Likely an incorrect or invalid review comment.


67-76: The legacy recipient mapping is correct and safe. SentEmail.to is defined as TEXT[] (PostgreSQL text array of email address strings), and to_jsonb(se."to") correctly converts it to a JSONB array of strings, which matches the downstream EmailOutbox.to contract expecting { type: "custom-emails", emails: string[] }. The concern about potential {email,name} object mismatches is invalid since the source data structure is always a string array, not objects with multiple properties.

Likely an incorrect or invalid review comment.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Bug: Unused fetch could throw if email env vars missing

The variables internalTenancy and emailConfig are fetched unconditionally but never used because the email sending functionality is disabled (line 72 throws immediately). The getSharedEmailConfig call invokes getEnvVariable for email configuration variables (STACK_EMAIL_HOST, etc.) without defaults, which will throw an error if those environment variables aren't set. This causes the endpoint to fail with a confusing error about missing email config even though email sending is disabled. These fetches should be removed or moved inside the conditional block.

apps/backend/src/app/api/latest/internal/failed-emails-digest/route.ts#L46-L48

const failedEmailsByTenancy = await getFailedEmailsByTenancy(new Date(Date.now() - 1000 * 60 * 60 * 24));
const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID);
const emailConfig = await getSharedEmailConfig("Stack Auth");

Fix in Cursor Fix in Web


@cmux-agent
Copy link

cmux-agent bot commented Dec 12, 2025

Older cmux preview screenshots (latest comment is below)

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

Copy link
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: 0

🧹 Nitpick comments (2)
apps/backend/prisma/migrations/20251212183000_migrate_sent_email/migration.sql (2)

7-9: Dropping SentEmail will discard “orphaned tenancy” rows (if any) without migrating them.
The WHERE EXISTS (Tenancy…) filter intentionally skips any SentEmail whose tenancyId no longer exists, but DROP TABLE … "SentEmail" will still remove them. If that’s intended cleanup, consider making it explicit (e.g., pre-delete with a comment / sanity-count), or gate the DROP on “no remaining rows”.

Also applies to: 140-140


14-15: Add a deterministic ORDER BY to the 10k batching selection.
LIMIT 10000 without ORDER BY makes each batch nondeterministic, which can complicate debugging/rollbacks. Deterministic ordering is cheap here and keeps re-runs predictable.

 WITH to_migrate AS (
     SELECT se."tenancyId", se."id"
     FROM "SentEmail" se
@@
     AND NOT EXISTS (
         SELECT 1 FROM "EmailOutbox" eo 
         WHERE eo."tenancyId" = se."tenancyId" AND eo."id" = se."id"
     )
+    ORDER BY se."createdAt" ASC, se."tenancyId" ASC, se."id" ASC
     LIMIT 10000
 ),

Also applies to: 128-133

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a4f46e and 1a83322.

📒 Files selected for processing (1)
  • apps/backend/prisma/migrations/20251212183000_migrate_sent_email/migration.sql (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: Cursor Bugbot
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: all-good
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: docker
  • GitHub Check: setup-tests
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: build (22.x)
🔇 Additional comments (2)
apps/backend/prisma/migrations/20251212183000_migrate_sent_email/migration.sql (2)

1-3: Sentinel placement is correct — the runner splits by SPLIT_STATEMENT_SENTINEL, so the first statement (lines 1–133) with CONDITIONALLY_REPEAT_MIGRATION_SENTINEL repeats in full, and the second statement (lines 136–140) without the sentinel executes once. The DROP at line 140 is safely outside the repeat block and will not execute after the first 10k batch.


88-88: No action needed. PostgreSQL 16.1 includes gen_random_uuid() as a built-in function (available since PostgreSQL 13), so pgcrypto extension is not required. This usage is consistent with the project's Prisma schema, which already uses the same function without extension setup.

Likely an incorrect or invalid review comment.

@cmux-agent
Copy link

cmux-agent bot commented Dec 12, 2025

Preview Screenshots

Open Diff Heatmap

Preview screenshots are being captured...

Workspace and dev browser links will appear here once the preview environment is ready.


Generated by cmux preview system

@N2D4 N2D4 merged commit e7e792d into dev Dec 12, 2025
23 checks passed
@N2D4 N2D4 deleted the email-outbox-backend branch December 12, 2025 18:26
@coderabbitai coderabbitai bot mentioned this pull request Dec 16, 2025
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.

3 participants