Skip to content

feat(emails): send one-time welcome email on account creation - #774

Merged
sweetmantech merged 7 commits into
mainfrom
feat/welcome-email
Jul 23, 2026
Merged

feat(emails): send one-time welcome email on account creation#774
sweetmantech merged 7 commits into
mainfrom
feat/welcome-email

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Implements the "Welcome email on signup" item of recoupable/chat#1867.

Problem

email_send_log has 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)

Path Hooked Why
lib/accounts/createAccountHandler.ts new-account branch (POST /api/accounts, the Privy web signup) primary signup path; email signups only (wallet-only signups have no address)
lib/privy/getOrCreateAccountIdByAuthToken.ts create branch (fresh Privy bearer-token users, GET /api/accounts/id etc.) a brand-new Privy user's first request can provision here without ever hitting POST /api/accounts
lib/agents/handleNormalSignup / handleAgentPrefixSignup (agent signup) ❌ deliberate that flow already sends its own verification-code email
lib/accounts/getOrCreateAccountByEmail.ts (org-member invite pre-provisioning) ❌ deliberate invited members haven't signed up yet; welcoming them at invite time is the invite email's job

Existing-account lookups return before the send in both hooked paths.

Idempotency (as required)

Two layers, both stated per the issue:

  1. Creation-path semantics: the send is only reachable from the create-new branches; a sign-in of an existing account short-circuits earlier.
  2. email_send_log lookup guard: sendWelcomeEmail first calls selectWelcomeEmailLog(accountId), which matches a prior status = "sent" row whose raw_body carries the "type":"welcome_email" marker. Already sent → skip. send_failed rows don't match, so a failed welcome can retry on a later provisioning race.

Logging

Same logEmailAttemptemail_send_log path as POST /api/emails: account_id, status (sent / send_failed), resend_id, and raw_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

sendWelcomeEmail is 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/emails self-send policy.

Files

  • lib/emails/buildWelcomeEmail.ts (+ test): subject/HTML builder, mirrors the existing plain inline-HTML template style, getFrontendBaseUrl() CTA, standard footer
  • lib/emails/sendWelcomeEmail.ts (+ test): guard → build → Resend send → log
  • lib/supabase/email_send_log/selectWelcomeEmailLog.ts (+ test): idempotency lookup (Supabase access stays in lib/supabase/)
  • lib/const.ts: WELCOME_EMAIL_LOG_TYPE
  • lib/accounts/createAccountHandler.ts, lib/privy/getOrCreateAccountIdByAuthToken.ts: hooks (+ updated tests)

Verification

Local (worktree at origin/main + this change):

  • TDD red-first: all three new units' tests written and confirmed failing before implementation; hook assertions added to the two existing suites and confirmed failing.
  • Full vitest suite: 756 files / 4134 tests passed.
  • pnpm exec tsc --noEmit: 0 errors in any touched file (the ~200 pre-existing errors on main are all in untouched test files).
  • pnpm lint and pnpm 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/accounts with a fresh own-alias email (e.g. sweetmantech+welcome-test@gmail.com), then (1) confirm the email arrives within a minute, (2) confirm one email_send_log row with the new account_id, status='sent', and the welcome_email marker in raw_body, (3) repeat the same POST and confirm no second row/send, (4) POST /api/accounts with an existing email and confirm no send.

How to test

pnpm exec vitest run lib/emails lib/supabase/email_send_log lib/accounts lib/privy
pnpm exec tsc --noEmit

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 /setup deep links built on CHAT_APP_URL, guiding users to their valuation. Part of recoupable/chat#1867.

  • New Features

    • Hooks into first creation only: POST /api/accounts and getOrCreateAccountIdByAuthToken; skips wallet-only signups, agent signups, and invite pre-provisioning.
    • Idempotent via email_send_log marker {"type":"welcome_email"}; failed sends can retry.
    • Sends via Resend from RECOUP_FROM_EMAIL; logs sent or send_failed; never blocks account creation.
    • Updated content: five-step onboarding with /setup/* links on CHAT_APP_URL; durable images on Vercel Blob and i.scdn.co; primary CTA targets /setup.
  • Refactors

    • Reuse generic selectEmailSendLog with a new accountId filter for per-account dedup; removed the bespoke selectWelcomeEmailLog.
    • Extracted imageTag helper for step thumbnails.

Written for commit 58ec6eb. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • New accounts now receive a one-time welcome email when an email address is available.
    • The welcome email includes a link to view the account valuation.
    • Welcome emails are also sent when accounts are created during authentication.
  • Bug Fixes
    • Prevented duplicate welcome emails from being sent.
    • Email delivery failures no longer interrupt account creation.

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

cursor Bot commented Jul 20, 2026

Copy link
Copy Markdown

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.

@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 23, 2026 9:05pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b6f2030-dd0c-4ad6-8d7e-286db530d5bc

📥 Commits

Reviewing files that changed from the base of the PR and between 03b0014 and 58ec6eb.

⛔ Files ignored due to path filters (4)
  • lib/emails/__tests__/buildWelcomeEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/sendWelcomeEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/welcome/__tests__/renderWelcomeSteps.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/email_send_log/__tests__/selectEmailSendLog.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (6)
  • lib/emails/buildWelcomeEmail.ts
  • lib/emails/sendWelcomeEmail.ts
  • lib/emails/welcome/imageTag.ts
  • lib/emails/welcome/renderWelcomeSteps.ts
  • lib/emails/welcome/welcomeOnboardingSteps.ts
  • lib/supabase/email_send_log/selectEmailSendLog.ts
📝 Walkthrough

Walkthrough

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

Changes

Welcome Email Delivery

Layer / File(s) Summary
Welcome email content and templates
lib/emails/welcome/*, lib/emails/buildWelcomeEmail.ts
Adds typed artist and onboarding data, sanitized HTML renderers, and a complete welcome email template.
Idempotent email delivery and logging
lib/const.ts, lib/supabase/email_send_log/selectWelcomeEmailLog.ts, lib/emails/sendWelcomeEmail.ts
Identifies welcome-email logs, skips previously sent messages, sends through Resend, records outcomes, and swallows delivery errors.
Account creation integration
lib/accounts/createAccountHandler.ts, lib/privy/getOrCreateAccountIdByAuthToken.ts
Triggers best-effort welcome-email delivery after creating accounts with email addresses.

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
Loading

Poem

A new account wakes to a greeting bright,
With onboarding steps in warm delight.
Resend carries the welcome through,
Logs keep duplicates from coming anew.
A friendly first step, neatly spun—
The account is made, the welcome begun.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning FAIL: createAccountHandler, buildWelcomeEmail, and sendWelcomeEmail are long multi-responsibility functions (85/24/33 lines), violating the SRP/20-line guidance. Split the welcome-email flow into smaller helpers: keep handlers as orchestration only, move HTML assembly into template fragments, and reduce each function to a single focused responsibility.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/welcome-email

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.ts has 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.ts and lib/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
Loading

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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread lib/supabase/email_send_log/selectWelcomeEmailLog.ts Outdated
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread lib/supabase/email_send_log/__tests__/selectWelcomeEmailLog.test.ts Outdated

// 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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
lib/emails/sendWelcomeEmail.ts (2)

43-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize 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 in lib/const.ts so 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 win

Keep delivery orchestration within the small-function guideline.

sendWelcomeEmail combines 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 lift

Keep createAccountHandler focused.

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 win

Keep the email renderer under the small-function guideline.

buildWelcomeEmail spans 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

📥 Commits

Reviewing files that changed from the base of the PR and between e7d4f7d and 263654e.

⛔ Files ignored due to path filters (5)
  • lib/accounts/__tests__/createAccountHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/buildWelcomeEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/sendWelcomeEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/privy/__tests__/getOrCreateAccountIdByAuthToken.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/email_send_log/__tests__/selectWelcomeEmailLog.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (6)
  • lib/accounts/createAccountHandler.ts
  • lib/const.ts
  • lib/emails/buildWelcomeEmail.ts
  • lib/emails/sendWelcomeEmail.ts
  • lib/privy/getOrCreateAccountIdByAuthToken.ts
  • lib/supabase/email_send_log/selectWelcomeEmailLog.ts

Comment on lines +97 to +101
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 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread lib/emails/sendWelcomeEmail.ts Outdated
* @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> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — 263654e8

Synced the branch with main (was 7 behind; clean auto-merge, lib/const.ts merged cleanly — both RECOUP_FROM_EMAIL and WELCOME_EMAIL_LOG_TYPE intact). Affected suites + tsc clean locally (263 tests across lib/emails · lib/supabase/email_send_log · lib/accounts · lib/privy), pushed, CI green (test · format · lint). PR is now CLEAN/MERGEABLE.

Tested against the preview built from this commit (api-p82nzbfhx, Vercel success). All sends are real (previews share prod DB).

Behavior matrix

# 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

Welcome 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
lib/emails/welcome/welcomeEmailCast.ts (1)

11-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make 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: make WelcomeArtist and WELCOME_EMAIL_CAST readonly.
  • lib/emails/welcome/welcomeOnboardingSteps.ts#L11-L20: make WelcomeStep and WELCOME_ONBOARDING_STEPS readonly.

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 win

Avoid 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 on undefined. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 263654e and 03b0014.

⛔ Files ignored due to path filters (3)
  • lib/emails/__tests__/buildWelcomeEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/welcome/__tests__/renderWelcomeCastStrip.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/welcome/__tests__/renderWelcomeSteps.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • lib/emails/buildWelcomeEmail.ts
  • lib/emails/welcome/renderWelcomeCastStrip.ts
  • lib/emails/welcome/renderWelcomeSteps.ts
  • lib/emails/welcome/welcomeEmailCast.ts
  • lib/emails/welcome/welcomeOnboardingSteps.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/emails/buildWelcomeEmail.ts

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Update — welcome email rebuilt around the onboarding flow (03b00141)

Per feedback, the generic one-CTA welcome is replaced with an onboarding walkthrough illustrated with our house artists. The email now mirrors chat's real onboarding sequence and features the cast (Gatsby Grace, LA EQUIS, Sound of Fractures, Brauxelion, LATASHA).

Redesigned welcome email

What's in it

  • House-style chrome (logo header + card) matching the valuation email.
  • Cast strip — 5 circular artist PFPs ("Managers run artists like these on Recoup").
  • 5 numbered steps, each with artwork (PFPs for the artist/social steps, real album covers for the catalog/value/task steps, so every artist appears):
    1. Confirm your artists · 2. Verify their socials · 3. Claim your catalog · 4. See your baseline valuation · 5. Automate with tasks
  • CTA Confirm your roster → chat app.

Step language mirrors the product (chat lib/onboarding/getOnboardingStepContent.ts)

The product's canonical onboarding is 4 steps (Confirm artists → Verify socials → Claim catalog → Schedule weekly report), with the baseline valuation treated as upstream (already done at signup via the funnel). The email keeps it as an explicit 5th-in-sequence step because a direct signup (not from the valuation funnel) hasn't valued yet, so surfacing it is correct for the general welcome. Verbs match the app: Confirm / Verify / Claim / Schedule.

Artwork sourcing (durability)

All images are stable i.scdn.co (Spotify CDN) URLs, resolved from each artist's linked Spotify profile (authoritative artist id, not name search — which had mismatched "LATASHA" → "Latasha Lee" and a homonym "Brauxelion"). Deliberately not Instagram/TikTok avatar URLs — those are signed and expire, which would break the email over time. The curated set is hard-coded (lib/emails/welcome/welcomeEmailCast.ts) since the welcome email is identical for every signup — no per-send DB/Spotify lookup.

Verification (preview api-6g3emxc4d, built from 03b00141, CI green)

  • POST /api/accounts with a fresh alias → 200; email_send_log row status=sent, type=welcome_email, subject "Welcome to Recoup", resend_id 9d2e784a, delivered to the alias inbox (images render from Spotify CDN).
  • Idempotency + validation paths from the prior verification still hold (unchanged send/guard path).
  • 10 unit tests (cast strip, steps, builder — order + full-cast + no-em-dash + stable-CDN assertions); tsc + lint + format clean.

New files (SRP, one export each): lib/emails/welcome/welcomeEmailCast.ts, welcomeOnboardingSteps.ts, renderWelcomeCastStrip.ts, renderWelcomeSteps.ts (+ tests). buildWelcomeEmail.ts assembles them.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 from renderValuationReportHtml creates 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 phrase five step is a user-facing grammar slip that can reduce polish in welcome messaging — update it to five-step in 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

Comment thread lib/emails/welcome/__tests__/renderWelcomeCastStrip.test.ts Outdated
Comment thread lib/emails/welcome/welcomeOnboardingSteps.ts Outdated
Comment thread lib/emails/welcome/__tests__/renderWelcomeSteps.test.ts Outdated
</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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
<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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Update — per-step art + /setup deep links (c375e62f)

Applied the requested changes to each step.

Welcome email with 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Fix — deep links now use fixed CHAT_APP_URL (75f7038c). They were built on getFrontendBaseUrl(), which falls back to the deployment's own VERCEL_URL on previews, so a preview test send linked to the api preview origin (api-*.vercel.app/setup/...). Switched to CHAT_APP_URL (same as the valuation email), so /setup/* always resolves to https://chat.recoupable.dev in every environment. Unit tests updated + green.

⚠️ Blocker for prod: the /setup + /setup/{artists,socials,catalog,valuation,tasks} routes do not exist in the chat app yet (app/setup is absent). These links 404 until built. Tracking the page work in chat#1885; the welcome email should not reach prod signups before those pages ship.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

SRP - new lib file for imageTag

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 58ec6ebf — extracted to lib/emails/welcome/imageTag.ts (one exported function); renderWelcomeSteps.ts imports it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

KISS

  • actual: lib/supabase/email_send_log/selectWelcomeEmailLog.ts
  • required: lib/supabase/email_send_log/selectEmailSendLog.ts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Re-verified after the review fixes (58ec6ebf)

Preview api-xlc62s2jr, CI green (test · format · lint). The refactor (extracted imageTag, DRY selectEmailSendLog dedup) is behavior-identical:

  • POST /api/accounts (fresh alias) → 200, account ff4383df; welcome email sent (resend_id bfd54aa6), delivered to the alias inbox.
  • Idempotency: repeat POST with the same email → 200 same account, and email_send_log has exactly one welcome_email row — the generic selectEmailSendLog({ accountId, status:'sent', rawBodyLike:'"type":"welcome_email"' }) guard holds.

Ready to merge.

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.

1 participant