feat(emails): send one-time welcome email on account creation - #774
Conversation
New signups have never received a single email (email_send_log has zero non-task rows), so anyone who closes the tab after signing up is unreachable. This sends a short branded welcome email, with one next step (see your valuation on chat), whenever an account is first created with an email address. - buildWelcomeEmail: subject + inline-HTML body matching the existing plain email template style, CTA links to the chat app via getFrontendBaseUrl, standard reply footer appended - sendWelcomeEmail: sends from RECOUP_FROM_EMAIL via Resend, logs sent/send_failed to email_send_log with a "type":"welcome_email" raw_body marker; best-effort (never throws into account creation) - selectWelcomeEmailLog: idempotency guard - skips the send when a sent welcome row already exists for the account; failed attempts do not match, so they can retry - Hooked into both first-creation paths: createAccountHandler (POST /api/accounts new-account branch, email signups only) and getOrCreateAccountIdByAuthToken (fresh Privy bearer-token users) Part of recoupable/chat#1867 (welcome email on signup). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 24 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (6)
📝 WalkthroughWalkthroughNew-account flows now send a one-time welcome email when an email address is available. The email includes curated onboarding content, a frontend CTA, and a shared footer, then is sent through Resend and tracked in Supabase. ChangesWelcome Email Delivery
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant AccountCreation
participant sendWelcomeEmail
participant Supabase
participant Resend
participant email_send_log
AccountCreation->>sendWelcomeEmail: accountId and email
sendWelcomeEmail->>Supabase: check sent welcome log
Supabase-->>sendWelcomeEmail: existing log or no match
sendWelcomeEmail->>Resend: send rendered welcome email
Resend-->>sendWelcomeEmail: response or Resend id
sendWelcomeEmail->>email_send_log: record sent or send_failed attempt
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ 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 |
There was a problem hiding this comment.
5 issues found across 11 files
Confidence score: 2/5
- In
lib/accounts/createAccountHandler.ts, welcome sends can proceed even when the email-link insert fails, so a message may go to an address not actually attached to the new account; this creates orphan-account communication risk and inconsistent state — gate sending on a verified link insert and abort/rollback the create path when linking fails. lib/emails/sendWelcomeEmail.tshas a race where concurrent requests can both pass pre-send checks and each send, leading to duplicate welcome emails and duplicate audit rows; this is user-facing and likely under retry/load conditions — add an atomic per-account idempotency claim before sending.- Across
lib/accounts/createAccountHandler.tsandlib/privy/getOrCreateAccountIdByAuthToken.ts, failed downstream provisioning/send paths can leave accounts permanently marked as existing but never welcomed, because retries skip the creation branch; the consequence is silent, long-lived under-delivery — introduce retryable provisioning state/job handling so failed welcomes are recoverable. - In
lib/accounts/createAccountHandler.ts, signup now blocks on the external Resend call plus email-log write, so third-party/API latency can slow or time out account creation even though email is best-effort; this increases registration failure risk during transient outages — move welcome dispatch/logging to async background work or defer it from the request path.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/emails/sendWelcomeEmail.ts">
<violation number="1" location="lib/emails/sendWelcomeEmail.ts:35">
P2: Concurrent calls can both pass the lookup before either call records its `sent` row, causing duplicate welcome emails. An atomic per-account claim/idempotency operation before sending would prevent both calls from reaching Resend.</violation>
</file>
<file name="lib/accounts/createAccountHandler.ts">
<violation number="1" location="lib/accounts/createAccountHandler.ts:100">
P2: Accounts whose later provisioning step fails can permanently miss the welcome email: the handler returns an error, but the next attempt sees the persisted email link and skips the creation branch. Trigger the welcome workflow after the email link is confirmed, using an outbox or retryable job so it is independent of later provisioning failures.</violation>
<violation number="2" location="lib/accounts/createAccountHandler.ts:100">
P2: Signup now waits for the Resend request and email-log write before returning, so an external email or database slowdown can make account creation slow or time out even though sending is best-effort. Dispatching through a background job/outbox would keep account creation independent of email-provider latency.</violation>
<violation number="3" location="lib/accounts/createAccountHandler.ts:100">
P1: Welcome emails can be sent to an email that was never linked to the new account. Checking the email-link insert result and resolving or aborting the failed create before sending would prevent orphan-account sends and duplicate welcomes from concurrent or retried requests.</violation>
</file>
<file name="lib/privy/getOrCreateAccountIdByAuthToken.ts">
<violation number="1" location="lib/privy/getOrCreateAccountIdByAuthToken.ts:31">
P2: A transient welcome-email failure becomes permanent for this account because later authentication requests return from the existing-account branch before reaching this call. A retryable provisioning job/state, rather than invoking the sender only on the no-account branch, would allow `send_failed` attempts to be retried without resending already-sent messages.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Browser/App
participant API as API Route
participant Auth as Auth Service
participant Account as Account Service
participant Welcome as sendWelcomeEmail
participant Log as selectWelcomeEmailLog
participant DB as Supabase
participant Resend as Resend API
Note over Client,Resend: Account Creation Flows (First-Time Only)
alt POST /api/accounts (email signup)
Client->>API: POST /api/accounts { email }
API->>Account: createAccountHandler()
Account->>DB: Check existing account
DB-->>Account: Not found
Account->>DB: Create account
Account->>Account: Initialize credits, assign org
alt Has email (not wallet-only)
Account->>Welcome: sendWelcomeEmail(accountId, email)
end
Account-->>API: Account created
API-->>Client: 200 + account data
else GET /api/accounts/id (Privy bearer token)
Client->>Auth: GET /api/accounts/id (Privy token)
Auth->>Account: getOrCreateAccountIdByAuthToken()
Account->>DB: Check existing email
DB-->>Account: Not found
Account->>DB: Create account via createAccountWithEmail
Account->>Welcome: sendWelcomeEmail(accountId, email)
Account-->>Auth: Account ID
Auth-->>Client: Account ID
end
Note over Welcome,Resend: Welcome Email Sending (Best-Effort)
Welcome->>Log: selectWelcomeEmailLog(accountId)
Log->>DB: SELECT id FROM email_send_log WHERE account_id=? AND status='sent' AND raw_body LIKE '%"type":"welcome_email"%'
DB-->>Log: Row or null
Log-->>Welcome: null or log row
alt No prior sent welcome
Welcome->>Welcome: buildWelcomeEmail()
Welcome->>Resend: sendEmailWithResend(from=RECOUP_FROM_EMAIL, to=[email], subject, html)
alt Resend success
Resend-->>Welcome: { id: "re_xxx" }
Welcome->>DB: logEmailAttempt(status="sent", resendId, rawBody={type:welcome_email})
else Resend failure
Resend-->>Welcome: NextResponse error
Welcome->>DB: logEmailAttempt(status="send_failed", rawBody={type:welcome_email})
end
else Already sent
Note over Welcome: Skip (idempotent)
end
Note over Welcome: Never throws - errors are caught and logged
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if (email) { | ||
| // First creation of this account: send the one-time welcome email. | ||
| // Best-effort (never throws) and guarded by email_send_log inside. | ||
| await sendWelcomeEmail({ accountId: newAccount.id, email }); |
There was a problem hiding this comment.
P1: Welcome emails can be sent to an email that was never linked to the new account. Checking the email-link insert result and resolving or aborting the failed create before sending would prevent orphan-account sends and duplicate welcomes from concurrent or retried requests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/accounts/createAccountHandler.ts, line 100:
<comment>Welcome emails can be sent to an email that was never linked to the new account. Checking the email-link insert result and resolving or aborting the failed create before sending would prevent orphan-account sends and duplicate welcomes from concurrent or retried requests.</comment>
<file context>
@@ -93,6 +94,12 @@ export async function createAccountHandler(body: CreateAccountBody): Promise<Nex
+ if (email) {
+ // First creation of this account: send the one-time welcome email.
+ // Best-effort (never throws) and guarded by email_send_log inside.
+ await sendWelcomeEmail({ accountId: newAccount.id, email });
+ }
+
</file context>
| const { subject, html } = buildWelcomeEmail(); | ||
| const rawBody = JSON.stringify({ type: WELCOME_EMAIL_LOG_TYPE, to: email, subject }); | ||
|
|
||
| const result = await sendEmailWithResend({ |
There was a problem hiding this comment.
P2: Concurrent calls can both pass the lookup before either call records its sent row, causing duplicate welcome emails. An atomic per-account claim/idempotency operation before sending would prevent both calls from reaching Resend.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/sendWelcomeEmail.ts, line 35:
<comment>Concurrent calls can both pass the lookup before either call records its `sent` row, causing duplicate welcome emails. An atomic per-account claim/idempotency operation before sending would prevent both calls from reaching Resend.</comment>
<file context>
@@ -0,0 +1,51 @@
+ const { subject, html } = buildWelcomeEmail();
+ const rawBody = JSON.stringify({ type: WELCOME_EMAIL_LOG_TYPE, to: email, subject });
+
+ const result = await sendEmailWithResend({
+ from: RECOUP_FROM_EMAIL,
+ to: [email],
</file context>
| if (email) { | ||
| // First creation of this account: send the one-time welcome email. | ||
| // Best-effort (never throws) and guarded by email_send_log inside. | ||
| await sendWelcomeEmail({ accountId: newAccount.id, email }); |
There was a problem hiding this comment.
P2: Signup now waits for the Resend request and email-log write before returning, so an external email or database slowdown can make account creation slow or time out even though sending is best-effort. Dispatching through a background job/outbox would keep account creation independent of email-provider latency.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/accounts/createAccountHandler.ts, line 100:
<comment>Signup now waits for the Resend request and email-log write before returning, so an external email or database slowdown can make account creation slow or time out even though sending is best-effort. Dispatching through a background job/outbox would keep account creation independent of email-provider latency.</comment>
<file context>
@@ -93,6 +94,12 @@ export async function createAccountHandler(body: CreateAccountBody): Promise<Nex
+ if (email) {
+ // First creation of this account: send the one-time welcome email.
+ // Best-effort (never throws) and guarded by email_send_log inside.
+ await sendWelcomeEmail({ accountId: newAccount.id, email });
+ }
+
</file context>
| if (email) { | ||
| // First creation of this account: send the one-time welcome email. | ||
| // Best-effort (never throws) and guarded by email_send_log inside. | ||
| await sendWelcomeEmail({ accountId: newAccount.id, email }); |
There was a problem hiding this comment.
P2: Accounts whose later provisioning step fails can permanently miss the welcome email: the handler returns an error, but the next attempt sees the persisted email link and skips the creation branch. Trigger the welcome workflow after the email link is confirmed, using an outbox or retryable job so it is independent of later provisioning failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/accounts/createAccountHandler.ts, line 100:
<comment>Accounts whose later provisioning step fails can permanently miss the welcome email: the handler returns an error, but the next attempt sees the persisted email link and skips the creation branch. Trigger the welcome workflow after the email link is confirmed, using an outbox or retryable job so it is independent of later provisioning failures.</comment>
<file context>
@@ -93,6 +94,12 @@ export async function createAccountHandler(body: CreateAccountBody): Promise<Nex
+ if (email) {
+ // First creation of this account: send the one-time welcome email.
+ // Best-effort (never throws) and guarded by email_send_log inside.
+ await sendWelcomeEmail({ accountId: newAccount.id, email });
+ }
+
</file context>
|
|
||
| // First creation of this account: send the one-time welcome email. | ||
| // Best-effort (never throws) and guarded by email_send_log inside. | ||
| await sendWelcomeEmail({ accountId, email }); |
There was a problem hiding this comment.
P2: A transient welcome-email failure becomes permanent for this account because later authentication requests return from the existing-account branch before reaching this call. A retryable provisioning job/state, rather than invoking the sender only on the no-account branch, would allow send_failed attempts to be retried without resending already-sent messages.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/privy/getOrCreateAccountIdByAuthToken.ts, line 31:
<comment>A transient welcome-email failure becomes permanent for this account because later authentication requests return from the existing-account branch before reaching this call. A retryable provisioning job/state, rather than invoking the sender only on the no-account branch, would allow `send_failed` attempts to be retried without resending already-sent messages.</comment>
<file context>
@@ -23,5 +24,11 @@ export async function getOrCreateAccountIdByAuthToken(authToken: string): Promis
+
+ // First creation of this account: send the one-time welcome email.
+ // Best-effort (never throws) and guarded by email_send_log inside.
+ await sendWelcomeEmail({ accountId, email });
+
+ return accountId;
</file context>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
lib/emails/sendWelcomeEmail.ts (2)
43-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCentralize email-log status values.
"sent"is duplicated between the selector and sender, and"send_failed"is a cross-file domain status. Define shared typed constants inlib/const.tsso the query and writer cannot drift.As per coding guidelines: use constants for repeated values and store shared constants in
lib/const.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/emails/sendWelcomeEmail.ts` around lines 43 - 47, Centralize the email-log statuses used by sendWelcomeEmail and its selector by defining shared typed constants for "sent" and "send_failed" in lib/const.ts. Update the sender’s logEmailAttempt calls and the corresponding query/status comparisons to reference those constants, preserving the existing status values and behavior.Source: Coding guidelines
19-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep delivery orchestration within the small-function guideline.
sendWelcomeEmailcombines lookup, rendering, provider delivery, outcome classification, and persistence across 33 lines. Extracting the state-transition/logging boundary would keep the public function focused and make failure paths easier to test.As per coding guidelines: files matching
**/*.{js,ts,tsx,jsx,py,java,cs,go,rb,php}must flag functions longer than 20 lines and keep functions small and focused.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/emails/sendWelcomeEmail.ts` around lines 19 - 50, Refactor sendWelcomeEmail so it stays under the 20-line function guideline by extracting the delivery outcome classification and logEmailAttempt state-transition logic into a focused helper. Keep account lookup, email construction, provider invocation, and swallowed-error behavior unchanged, and have the helper handle both send_failed and sent outcomes using the existing rawBody, accountId, and result values.Source: Coding guidelines
lib/accounts/createAccountHandler.ts (1)
97-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftKeep
createAccountHandlerfocused.This change adds another side effect to an 85-line handler that already performs account lookup, persistence, credit initialization, and response shaping. Extract the new-account workflow or post-create side-effect orchestration before more responsibilities accumulate.
As per coding guidelines: files matching
**/*.{js,ts,tsx,jsx,py,java,cs,go,rb,php}must flag functions longer than 20 lines and keep functions small and focused.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/accounts/createAccountHandler.ts` around lines 97 - 101, Refactor createAccountHandler to keep the account creation flow focused by extracting the post-create side-effect orchestration, including the conditional sendWelcomeEmail call, into a dedicated helper or workflow function. Have createAccountHandler invoke that extracted unit while preserving the existing account lookup, persistence, credit initialization, response shaping, and best-effort email behavior.Source: Coding guidelines
lib/emails/buildWelcomeEmail.ts (1)
11-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep the email renderer under the small-function guideline.
buildWelcomeEmailspans 25 lines, mostly due to static markup. Move the template into a dedicated constant or renderer so the exported function only composes the URL/footer and returns the payload.As per coding guidelines: files matching
**/*.{js,ts,tsx,jsx,py,java,cs,go,rb,php}must flag functions longer than 20 lines and keep functions small and focused.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/emails/buildWelcomeEmail.ts` around lines 11 - 35, Reduce the length of buildWelcomeEmail by moving the static HTML template into a dedicated module-level constant or renderer. Keep buildWelcomeEmail focused on obtaining chatUrl and footer, composing the template with those values, and returning the subject/html payload while preserving the existing email content.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/accounts/createAccountHandler.ts`:
- Around line 97-101: Remove synchronous email delivery waits from both
account-creation paths: in lib/accounts/createAccountHandler.ts lines 97-101 and
lib/privy/getOrCreateAccountIdByAuthToken.ts lines 27-33, dispatch
sendWelcomeEmail through the same non-blocking mechanism or an outbox before
returning. Preserve the existing email guards and best-effort, never-throw
behavior while ensuring account creation does not await Resend/Supabase
operations.
In `@lib/emails/sendWelcomeEmail.ts`:
- Around line 27-50: The welcome-email flow is non-atomic and can suppress
lookup failures or lose provider outcomes. In lib/emails/sendWelcomeEmail.ts
lines 27-50, replace the read-then-send-then-log sequence around
sendWelcomeEmail, selectWelcomeEmailLog, and logEmailAttempt with a durable
claim/outbox or provider idempotency key, recording every provider outcome; in
lib/supabase/email_send_log/selectWelcomeEmailLog.ts lines 24-29, propagate
lookup errors as errors or discriminated results and return null only for a
successful no-row query.
In `@lib/supabase/email_send_log/selectWelcomeEmailLog.ts`:
- Line 14: Update the return type of selectWelcomeEmailLog to use the generated
Supabase Tables<"email_send_log"> type, narrowing it with Pick to the id field
and preserving the nullable result.
---
Nitpick comments:
In `@lib/accounts/createAccountHandler.ts`:
- Around line 97-101: Refactor createAccountHandler to keep the account creation
flow focused by extracting the post-create side-effect orchestration, including
the conditional sendWelcomeEmail call, into a dedicated helper or workflow
function. Have createAccountHandler invoke that extracted unit while preserving
the existing account lookup, persistence, credit initialization, response
shaping, and best-effort email behavior.
In `@lib/emails/buildWelcomeEmail.ts`:
- Around line 11-35: Reduce the length of buildWelcomeEmail by moving the static
HTML template into a dedicated module-level constant or renderer. Keep
buildWelcomeEmail focused on obtaining chatUrl and footer, composing the
template with those values, and returning the subject/html payload while
preserving the existing email content.
In `@lib/emails/sendWelcomeEmail.ts`:
- Around line 43-47: Centralize the email-log statuses used by sendWelcomeEmail
and its selector by defining shared typed constants for "sent" and "send_failed"
in lib/const.ts. Update the sender’s logEmailAttempt calls and the corresponding
query/status comparisons to reference those constants, preserving the existing
status values and behavior.
- Around line 19-50: Refactor sendWelcomeEmail so it stays under the 20-line
function guideline by extracting the delivery outcome classification and
logEmailAttempt state-transition logic into a focused helper. Keep account
lookup, email construction, provider invocation, and swallowed-error behavior
unchanged, and have the helper handle both send_failed and sent outcomes using
the existing rawBody, accountId, and result values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 88f0a355-dc35-42d3-b37b-d3c426a3091d
⛔ Files ignored due to path filters (5)
lib/accounts/__tests__/createAccountHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/emails/__tests__/buildWelcomeEmail.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/emails/__tests__/sendWelcomeEmail.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/privy/__tests__/getOrCreateAccountIdByAuthToken.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/supabase/email_send_log/__tests__/selectWelcomeEmailLog.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (6)
lib/accounts/createAccountHandler.tslib/const.tslib/emails/buildWelcomeEmail.tslib/emails/sendWelcomeEmail.tslib/privy/getOrCreateAccountIdByAuthToken.tslib/supabase/email_send_log/selectWelcomeEmailLog.ts
| if (email) { | ||
| // First creation of this account: send the one-time welcome email. | ||
| // Best-effort (never throws) and guarded by email_send_log inside. | ||
| await sendWelcomeEmail({ accountId: newAccount.id, email }); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Both account-creation paths synchronously await email delivery.
This makes account creation latency and capacity dependent on external Resend/Supabase operations, contrary to the best-effort objective.
lib/accounts/createAccountHandler.ts#L97-L101: dispatch welcome delivery asynchronously or through an outbox before returning the account response.lib/privy/getOrCreateAccountIdByAuthToken.ts#L27-L33: apply the same non-blocking delivery mechanism during Privy provisioning.
📍 Affects 2 files
lib/accounts/createAccountHandler.ts#L97-L101(this comment)lib/privy/getOrCreateAccountIdByAuthToken.ts#L27-L33
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/accounts/createAccountHandler.ts` around lines 97 - 101, Remove
synchronous email delivery waits from both account-creation paths: in
lib/accounts/createAccountHandler.ts lines 97-101 and
lib/privy/getOrCreateAccountIdByAuthToken.ts lines 27-33, dispatch
sendWelcomeEmail through the same non-blocking mechanism or an outbox before
returning. Preserve the existing email guards and best-effort, never-throw
behavior while ensuring account creation does not await Resend/Supabase
operations.
| * @param accountId - The account to check. | ||
| * @returns The matching log row id, or null when none exists (or on error). | ||
| */ | ||
| export async function selectWelcomeEmailLog(accountId: string): Promise<{ id: string } | null> { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use the generated Supabase row type for the result.
Replace the handwritten { id: string } with a Pick<Tables<"email_send_log">, "id">-based type, or the repository’s equivalent generated type, so schema changes remain visible to TypeScript.
As per path instructions: lib/supabase/**/*.ts requires return types using Tables<"table_name">.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/supabase/email_send_log/selectWelcomeEmailLog.ts` at line 14, Update the
return type of selectWelcomeEmailLog to use the generated Supabase
Tables<"email_send_log"> type, narrowing it with Pick to the id field and
preserving the nullable result.
Source: Path instructions
Preview verification —
|
| # | Case | Documented | Actual |
|---|---|---|---|
| A | POST /api/accounts {email: <fresh alias>} |
200 + welcome send | 200, account f38b445e; one email_send_log row status=sent, type=welcome_email, subject "Welcome to Recoup", resend_id fa654f84, to the alias ✅ |
| B | Repeat the same email | 200 existing, no 2nd send | 200 (existing-account branch, same account_id); no second welcome_email row ✅ |
| C | Invalid email (not-an-email) |
400 | 400 {"status":"error","missing_fields":["email"],"error":"email must be a valid email address"} ✅ |
| D | Pre-existing email (sweetmantech@gmail.com) |
200 existing, no send | 200; no welcome_email row created ✅ |
Idempotency proven at the DB level: across A–D there is exactly one welcome_email row (account f38b445e). The repeat (B) and the pre-existing lookup (D) short-circuit on the create-branch guard before sendWelcomeEmail, so no duplicate/extra send. No secret/env value echoed in any response.
Rendered email
Delivered to the alias inbox (subject "Welcome to Recoup"): heading, one-line value prop, single CTA (See your valuation → chat app), reply-to footer.
Note on house style: this is the plain inline-HTML template as scoped in this PR — it is not yet wrapped in the shared
renderEmailLayout(branded header/footer/card) used by the valuation email. That convergence is the consistency pass (api#784), which adopts the shared wrapper for welcome + valuation once merged. Flagging so the plain look here is expected, not a regression.
All of this PR's own Done-when criteria pass. Ready to merge on approval.
…e artists Replace the generic welcome with a 5-step onboarding walkthrough (mirroring chat's onboarding: confirm artists, verify socials, claim catalog, see baseline valuation, automate with tasks), illustrated with art from the house cast (Gatsby Grace, LA EQUIS, Sound of Fractures, Brauxelion, LATASHA): a PFP cast strip + per-step PFPs/album covers. - lib/emails/welcome/welcomeEmailCast.ts: curated cast + stable i.scdn.co art (Spotify CDN never expires, unlike signed IG/TikTok avatar URLs) - lib/emails/welcome/welcomeOnboardingSteps.ts: the 5 steps + thumbnails - lib/emails/welcome/renderWelcomeCastStrip.ts / renderWelcomeSteps.ts (+ tests) - buildWelcomeEmail.ts: house-style chrome (logo header, card, CTA) matching the valuation email; copy avoids em/en dashes 10 tests pass; tsc + lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
lib/emails/welcome/welcomeEmailCast.ts (1)
11-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the exported email configuration readonly.
Both datasets are shared module-level configuration, but their arrays and entries remain mutable. Add readonly properties and readonly arrays (or use
as const) at both declarations.
lib/emails/welcome/welcomeEmailCast.ts#L11-L20: makeWelcomeArtistandWELCOME_EMAIL_CASTreadonly.lib/emails/welcome/welcomeOnboardingSteps.ts#L11-L20: makeWelcomeStepandWELCOME_ONBOARDING_STEPSreadonly.As per coding guidelines, use constants for shared values and keep the configuration self-documenting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/emails/welcome/welcomeEmailCast.ts` around lines 11 - 20, Make the shared welcome email configuration immutable: in lib/emails/welcome/welcomeEmailCast.ts lines 11-20, update WelcomeArtist properties and WELCOME_EMAIL_CAST to readonly; apply the same readonly property and array changes to WelcomeStep and WELCOME_ONBOARDING_STEPS in lib/emails/welcome/welcomeOnboardingSteps.ts lines 11-20. Preserve the existing configuration values and self-documenting type structure.Source: Coding guidelines
lib/emails/welcome/welcomeOnboardingSteps.ts (1)
24-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid positional references to the cast dataset.
WELCOME_EMAIL_CAST[0],[3],[2],[1], and[4]encode meaning in array order. A future reorder can silently attach the wrong artwork, while a shorter array causes property access onundefined. Use a keyed record or stable artist identifiers instead.As per coding guidelines, “Use configuration objects instead of hardcoded values” and “Use descriptive names.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/emails/welcome/welcomeOnboardingSteps.ts` around lines 24 - 56, Replace the positional WELCOME_EMAIL_CAST index lookups in the onboarding step definitions with a keyed record or stable artist identifiers, using descriptive keys for each referenced cast member. Update thumbUrl and thumbAlt to resolve through those stable keys while preserving each step’s existing artwork and shape.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lib/emails/welcome/welcomeEmailCast.ts`:
- Around line 11-20: Make the shared welcome email configuration immutable: in
lib/emails/welcome/welcomeEmailCast.ts lines 11-20, update WelcomeArtist
properties and WELCOME_EMAIL_CAST to readonly; apply the same readonly property
and array changes to WelcomeStep and WELCOME_ONBOARDING_STEPS in
lib/emails/welcome/welcomeOnboardingSteps.ts lines 11-20. Preserve the existing
configuration values and self-documenting type structure.
In `@lib/emails/welcome/welcomeOnboardingSteps.ts`:
- Around line 24-56: Replace the positional WELCOME_EMAIL_CAST index lookups in
the onboarding step definitions with a keyed record or stable artist
identifiers, using descriptive keys for each referenced cast member. Update
thumbUrl and thumbAlt to resolve through those stable keys while preserving each
step’s existing artwork and shape.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: fb6dc5c9-7e6f-4f4f-aac7-fa267afd5f45
⛔ Files ignored due to path filters (3)
lib/emails/__tests__/buildWelcomeEmail.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/emails/welcome/__tests__/renderWelcomeCastStrip.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/emails/welcome/__tests__/renderWelcomeSteps.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (5)
lib/emails/buildWelcomeEmail.tslib/emails/welcome/renderWelcomeCastStrip.tslib/emails/welcome/renderWelcomeSteps.tslib/emails/welcome/welcomeEmailCast.tslib/emails/welcome/welcomeOnboardingSteps.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/emails/buildWelcomeEmail.ts
Update — welcome email rebuilt around the onboarding flow (
|
There was a problem hiding this comment.
2 issues found across 8 files (changes from recent commits).
Confidence score: 5/5
- In
lib/emails/buildWelcomeEmail.ts, duplicating the full card/header shell fromrenderValuationReportHtmlcreates drift risk as templates evolve, which can lead to inconsistent branding/layout across emails over time — extract and reuse a shared email shell/header renderer. - In
lib/emails/buildWelcomeEmail.ts, the phrasefive stepis a user-facing grammar slip that can reduce polish in welcome messaging — update it tofive-stepin the rendered copy.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/emails/buildWelcomeEmail.ts">
<violation number="1" location="lib/emails/buildWelcomeEmail.ts:26">
P3: Future email-template changes can diverge because this renderer duplicates the complete card/header shell already implemented in `renderValuationReportHtml`. A shared email-shell/header renderer would keep the two templates consistent while leaving welcome-specific content here.</violation>
<violation number="2" location="lib/emails/buildWelcomeEmail.ts:36">
P3: The welcome copy renders `five step` as an unhyphenated compound adjective. Using `five-step` keeps this user-facing sentence grammatically correct.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| </td> | ||
| <td valign="top" align="right" width="44"><a href="${WEBSITE_URL}"><img src="${RECOUP_LOGO_URL}" width="36" height="36" alt="Recoup" style="display:block;width:36px;height:36px;border-radius:8px"/></a></td> | ||
| </tr></table> | ||
| <p style="margin:0 0 24px;font-size:14px;line-height:1.6;color:#0a0a0a">Recoup is your AI team for the music business. Here is the five step path from a new account to a catalog you measure, grow, and automate.</p> |
There was a problem hiding this comment.
P3: The welcome copy renders five step as an unhyphenated compound adjective. Using five-step keeps this user-facing sentence grammatically correct.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/buildWelcomeEmail.ts, line 36:
<comment>The welcome copy renders `five step` as an unhyphenated compound adjective. Using `five-step` keeps this user-facing sentence grammatically correct.</comment>
<file context>
@@ -1,35 +1,45 @@
+</td>
+<td valign="top" align="right" width="44"><a href="${WEBSITE_URL}"><img src="${RECOUP_LOGO_URL}" width="36" height="36" alt="Recoup" style="display:block;width:36px;height:36px;border-radius:8px"/></a></td>
+</tr></table>
+<p style="margin:0 0 24px;font-size:14px;line-height:1.6;color:#0a0a0a">Recoup is your AI team for the music business. Here is the five step path from a new account to a catalog you measure, grow, and automate.</p>
+${renderWelcomeCastStrip()}
+${renderWelcomeSteps()}
</file context>
| <p style="margin:0 0 24px;font-size:14px;line-height:1.6;color:#0a0a0a">Recoup is your AI team for the music business. Here is the five step path from a new account to a catalog you measure, grow, and automate.</p> | |
| <p style="margin:0 0 24px;font-size:14px;line-height:1.6;color:#0a0a0a">Recoup is your AI team for the music business. Here is the five-step path from a new account to a catalog you measure, grow, and automate.</p> |
| const chatUrl = getFrontendBaseUrl(); | ||
| const footer = getEmailFooter(); | ||
|
|
||
| const html = `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f7f7f7;padding:24px 0"><tr><td align="center"> |
There was a problem hiding this comment.
P3: Future email-template changes can diverge because this renderer duplicates the complete card/header shell already implemented in renderValuationReportHtml. A shared email-shell/header renderer would keep the two templates consistent while leaving welcome-specific content here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/buildWelcomeEmail.ts, line 26:
<comment>Future email-template changes can diverge because this renderer duplicates the complete card/header shell already implemented in `renderValuationReportHtml`. A shared email-shell/header renderer would keep the two templates consistent while leaving welcome-specific content here.</comment>
<file context>
@@ -1,35 +1,45 @@
- </p>
- ${footer}
-</div>`.trim();
+ const html = `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f7f7f7;padding:24px 0"><tr><td align="center">
+<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border:1px solid #e8e8e8;border-radius:16px">
+<tr><td style="padding:32px 32px 28px;font-family:${FONT}">
</file context>
…email Iterate on the welcome email per feedback: - Remove the 'Managers run artists like these' cast strip. - Step 1 image is now an overlapping stack of the 5 house-artist PFPs (general social proof, no names), pre-composed on Vercel Blob. - Step 2 image is a real Instagram post thumbnail with an IG badge (pre-composed on Blob; IG CDN URLs are signed/expiring so the frame is re-hosted durably). - Steps 3/4 use specific album covers (LA EQUIS, Sound of Fractures); step 5 unchanged. - Each step gets an inline link into its /setup/<step> route; the primary CTA now targets /setup. Links build on getFrontendBaseUrl (env-aware). 10 welcome tests + 188 lib/emails tests pass; tsc + lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update — per-step art +
|
| Step | Image | Inline link → |
|---|---|---|
| 1. Confirm your artists | Overlapping stack of all 5 cast PFPs (social proof, no names) | Confirm your artists. → /setup/artists |
| 2. Verify their socials | The Instagram post thumbnail (Davp6vvP1rU) with an IG badge bottom-right | Verify artist socials. → /setup/socials |
| 3. Claim your catalog | Album art — LA EQUIS El Niño Estrella (album) | Claim your catalog. → /setup/catalog |
| 4. See your baseline valuation | Album art — Sound of Fractures Vibe Coding (album) | See your baseline valuation → /setup/valuation |
| 5. Automate with tasks | Unchanged | Setup your first task. → /setup/tasks |
- Removed the "Managers run artists like these on Recoup" cast strip.
- Primary CTA Confirm your roster →
/setup.
Image durability
The overlap stack and the IG-post-with-badge can't be done reliably in email HTML (clients strip negative margins / absolute positioning), so each is pre-composed into a single PNG and hosted on Vercel Blob (blob.vercel-storage.com). This also solves durability: the Instagram frame is served from a signed, expiring IG CDN URL, so it's re-hosted rather than hot-linked. Album covers are stable Spotify CDN URLs used directly.
A note on the links
Step links + CTA are built on getFrontendBaseUrl() (env-aware), so in production they resolve to https://chat.recoupable.dev/setup/* exactly as specified. In a preview send they point at the preview origin, so don't click them from the test email. Heads up: the /setup/* routes need to exist in the chat app for these to land — flagging in case they're still to be built.
Verification (preview api-7fhwobjlz, c375e62f, CI green)
POST /api/accounts (fresh alias) → 200; email_send_log status=sent, type=welcome_email, resend_id 12fdb1e9, delivered to the alias inbox (all art renders: Blob composites + Spotify covers). 10 welcome unit tests (+188 lib/emails); tsc + lint + format clean.
…nk for consistency
There was a problem hiding this comment.
2 issues found across 8 files (changes from recent commits).
Confidence score: 2/5
- In
lib/emails/welcome/welcomeOnboardingSteps.ts, all five/setup/*onboarding links reportedly resolve to 404 in production and test, so recipients can’t complete key onboarding steps from the welcome email—update these URLs to existing frontend routes (or add matching routes) before merge. - In
lib/emails/welcome/__tests__/renderWelcomeSteps.test.ts, the third test duplicates coverage already provided by the first test, which can add maintenance noise and obscure real regressions over time—remove or repurpose it to validate a distinct behavior.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/emails/welcome/__tests__/renderWelcomeSteps.test.ts">
<violation number="1" location="lib/emails/welcome/__tests__/renderWelcomeSteps.test.ts:27">
P3: Third test (`"points every step at its /setup route"`) is fully redundant with the first test. The first test already asserts `href="${BASE}${step.linkPath}"` for every step from `WELCOME_ONBOARDING_STEPS`, so hardcoding the same paths in a separate test adds no coverage and creates a maintenance burden when step paths change.</violation>
</file>
<file name="lib/emails/welcome/welcomeOnboardingSteps.ts">
<violation number="1" location="lib/emails/welcome/welcomeOnboardingSteps.ts:36">
P1: Every onboarding link in the welcome email currently lands on a 404 page: the production and test frontend return 404 for all five `/setup/*` URLs introduced here. The links would need to target routes that exist in the deployed frontend, or the corresponding frontend routes must be deployed before this email is sent.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| title: "Confirm your artists", | ||
| description: "Add the artists you manage to your roster so Recoup works across all of them.", | ||
| linkText: "Confirm your artists.", | ||
| linkPath: "/setup/artists", |
There was a problem hiding this comment.
P1: Every onboarding link in the welcome email currently lands on a 404 page: the production and test frontend return 404 for all five /setup/* URLs introduced here. The links would need to target routes that exist in the deployed frontend, or the corresponding frontend routes must be deployed before this email is sent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/welcome/welcomeOnboardingSteps.ts, line 36:
<comment>Every onboarding link in the welcome email currently lands on a 404 page: the production and test frontend return 404 for all five `/setup/*` URLs introduced here. The links would need to target routes that exist in the deployed frontend, or the corresponding frontend routes must be deployed before this email is sent.</comment>
<file context>
@@ -1,58 +1,79 @@
- thumbShape: "circle",
- thumbAlt: WELCOME_EMAIL_CAST[0].name,
+ linkText: "Confirm your artists.",
+ linkPath: "/setup/artists",
+ imageUrl: `${BLOB}/step1-artists-overlap-8yfiYIyB0tye7PnZYmvqU7fAP40JDT.png`,
+ imageStyle: "wide",
</file context>
| expect(html).toContain("width:56px;height:56px"); | ||
| }); | ||
|
|
||
| it("points every step at its /setup route", () => { |
There was a problem hiding this comment.
P3: Third test ("points every step at its /setup route") is fully redundant with the first test. The first test already asserts href="${BASE}${step.linkPath}" for every step from WELCOME_ONBOARDING_STEPS, so hardcoding the same paths in a separate test adds no coverage and creates a maintenance burden when step paths change.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/welcome/__tests__/renderWelcomeSteps.test.ts, line 27:
<comment>Third test (`"points every step at its /setup route"`) is fully redundant with the first test. The first test already asserts `href="${BASE}${step.linkPath}"` for every step from `WELCOME_ONBOARDING_STEPS`, so hardcoding the same paths in a separate test adds no coverage and creates a maintenance burden when step paths change.</comment>
<file context>
@@ -2,21 +2,39 @@ import { describe, it, expect } from "vitest";
+ expect(html).toContain("width:56px;height:56px");
+ });
+
+ it("points every step at its /setup route", () => {
+ const html = renderWelcomeSteps(BASE);
</file context>
…ment URL getFrontendBaseUrl falls back to the deployment's own VERCEL_URL on previews, so /setup links pointed at the api preview origin. Use CHAT_APP_URL (like the valuation email) so links always resolve to chat.recoupable.dev.
|
Fix — deep links now use fixed
|
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Requires human review: Auto-approval blocked by 11 unresolved issues from previous reviews.
Re-trigger cubic
| import { WELCOME_ONBOARDING_STEPS } from "@/lib/emails/welcome/welcomeOnboardingSteps"; | ||
|
|
||
| /** Inline-style the step thumbnail by kind (overlap strip vs album cover vs IG). */ | ||
| function imageTag(url: string, style: "wide" | "square" | "rounded", alt: string): string { |
There was a problem hiding this comment.
SRP - new lib file for imageTag
There was a problem hiding this comment.
Done in 58ec6ebf — extracted to lib/emails/welcome/imageTag.ts (one exported function); renderWelcomeSteps.ts imports it.
There was a problem hiding this comment.
KISS
- actual: lib/supabase/email_send_log/selectWelcomeEmailLog.ts
- required: lib/supabase/email_send_log/selectEmailSendLog.ts
There was a problem hiding this comment.
Done in 58ec6ebf — deleted selectWelcomeEmailLog.ts (+ its test) and reused the generic selectEmailSendLog from api#773. Extended it with an optional accountId filter so the per-account welcome dedup is preserved: selectEmailSendLog({ accountId, status: "sent", rawBodyLike: '"type":"welcome_email"', limit: 1 }). Backward-compatible (valuation path doesn't pass accountId).
… selectEmailSendLog - SRP: move the step-thumbnail helper into lib/emails/welcome/imageTag.ts - KISS/DRY: drop the bespoke selectWelcomeEmailLog; reuse the generic selectEmailSendLog (from api#773), extended with an optional accountId filter for the per-account welcome dedup. Deletes selectWelcomeEmailLog.ts + test. 265 lib/emails+supabase+accounts+privy tests pass; tsc + lint + format clean.
There was a problem hiding this comment.
0 issues found across 8 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Requires human review: Auto-approval blocked by 9 unresolved issues from previous reviews.
Re-trigger cubic
Re-verified after the review fixes (
|



Implements the "Welcome email on signup" item of recoupable/chat#1867.
Problem
email_send_loghas zero non-task rows ever: the full 2026-07-13→20 Resend log (158 sends) is 100% scheduled-task reports. A new signup who closes the tab is unreachable. (Prod walkthrough evidence in the issue.)What this does
When an account is first created with an email address, the api sends a short branded welcome email with exactly one next step (see your catalog valuation, linking to the chat app) and records the attempt in
email_send_log.Creation paths hooked (first-creation only, never sign-in)
lib/accounts/createAccountHandler.tsnew-account branch (POST /api/accounts, the Privy web signup)lib/privy/getOrCreateAccountIdByAuthToken.tscreate branch (fresh Privy bearer-token users,GET /api/accounts/idetc.)POST /api/accountslib/agents/handleNormalSignup/handleAgentPrefixSignup(agent signup)lib/accounts/getOrCreateAccountByEmail.ts(org-member invite pre-provisioning)Existing-account lookups return before the send in both hooked paths.
Idempotency (as required)
Two layers, both stated per the issue:
email_send_loglookup guard:sendWelcomeEmailfirst callsselectWelcomeEmailLog(accountId), which matches a priorstatus = "sent"row whoseraw_bodycarries the"type":"welcome_email"marker. Already sent → skip.send_failedrows don't match, so a failed welcome can retry on a later provisioning race.Logging
Same
logEmailAttempt→email_send_logpath asPOST /api/emails:account_id,status(sent/send_failed),resend_id, andraw_body = {"type":"welcome_email","to":…,"subject":…}.From address (note, per issue)
The repo has exactly one outbound from-address convention:
RECOUP_FROM_EMAIL=Agent by Recoup <agent@recoupable.dev>(lib/const.ts), reused here.agent@is not a dead drop — replies route through the existing inbound-email handling (lib/emails/inbound/), and the standard footer ("you can reply directly to this email") is appended. If we want a dedicated monitored address (e.g.welcome@/hello@), that's a Resend-domain + const change to make on top of this PR; the issue's "not bare agent@" ask is flagged rather than silently resolved.Safety
sendWelcomeEmailis best-effort and never throws: a Resend outage or DB error can never fail account creation or a first authenticated request. Recipient is always the account's own just-linked email, consistent with the/api/emailsself-send policy.Files
lib/emails/buildWelcomeEmail.ts(+ test): subject/HTML builder, mirrors the existing plain inline-HTML template style,getFrontendBaseUrl()CTA, standard footerlib/emails/sendWelcomeEmail.ts(+ test): guard → build → Resend send → loglib/supabase/email_send_log/selectWelcomeEmailLog.ts(+ test): idempotency lookup (Supabase access stays inlib/supabase/)lib/const.ts:WELCOME_EMAIL_LOG_TYPElib/accounts/createAccountHandler.ts,lib/privy/getOrCreateAccountIdByAuthToken.ts: hooks (+ updated tests)Verification
Local (worktree at
origin/main+ this change):pnpm exec tsc --noEmit: 0 errors in any touched file (the ~200 pre-existing errors onmainare all in untouched test files).pnpm lintandpnpm format:check: clean.Preview verification: pending. Note previews share the prod DB (different key salt) and this send is real. Suggested preview test (clone-to-own-account pattern): on the preview URL,
POST /api/accountswith a fresh own-alias email (e.g.sweetmantech+welcome-test@gmail.com), then (1) confirm the email arrives within a minute, (2) confirm oneemail_send_logrow with the newaccount_id,status='sent', and thewelcome_emailmarker inraw_body, (3) repeat the same POST and confirm no second row/send, (4)POST /api/accountswith an existing email and confirm no send.How to test
Part of recoupable/chat#1867.
🤖 Generated with Claude Code
Summary by cubic
Send a one-time welcome email when an account is first created with an email address. The email walks through five onboarding steps with
/setupdeep links built onCHAT_APP_URL, guiding users to their valuation. Part ofrecoupable/chat#1867.New Features
POST /api/accountsandgetOrCreateAccountIdByAuthToken; skips wallet-only signups, agent signups, and invite pre-provisioning.email_send_logmarker{"type":"welcome_email"}; failed sends can retry.ResendfromRECOUP_FROM_EMAIL; logssentorsend_failed; never blocks account creation./setup/*links onCHAT_APP_URL; durable images on Vercel Blob andi.scdn.co; primary CTA targets/setup.Refactors
selectEmailSendLogwith a newaccountIdfilter for per-account dedup; removed the bespokeselectWelcomeEmailLog.imageTaghelper for step thumbnails.Written for commit 58ec6eb. Summary will update on new commits.
Summary by CodeRabbit