Skip to content

fix: resolve email broadcast Save Draft silent failure#567

Merged
adambarito merged 2 commits intomainfrom
fix/email-broadcast-bugs
Mar 18, 2026
Merged

fix: resolve email broadcast Save Draft silent failure#567
adambarito merged 2 commits intomainfrom
fix/email-broadcast-bugs

Conversation

@adambarito
Copy link
Contributor

Summary

  • Remove replyTo from createEmailBroadcastWithTemplateSchema — empty string default was failing z.email() validation silently, causing "Save Draft" to do nothing
  • The backend already pulls replyTo from the EmailAddress record when sending (email-broadcast.trigger.ts:268), so the field was redundant
  • Fix layout children type (ReactElementReactNode) to resolve typecheck errors
  • Scope fan unique index to workspaceId to allow same email across workspaces

Test plan

  • Navigate to email broadcasts → Create Email Broadcast
  • Fill in name, from, subject, body → click "Save Draft"
  • Verify broadcast is created and appears in the list
  • Verify sending a broadcast still uses correct replyTo from EmailAddress

🤖 Generated with Claude Code

Remove replyTo from createEmailBroadcastWithTemplateSchema — empty string
default was failing z.email() validation silently. The backend already
pulls replyTo from the EmailAddress record when sending.

Also fix layout children type (ReactElement → ReactNode) and scope fan
unique index to workspaceId.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 17, 2026

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

14 Skipped Deployments
Project Deployment Actions Updated (UTC)
app-bio Ignored Ignored Mar 18, 2026 2:08am
app-chat Ignored Ignored Mar 18, 2026 2:08am
app-email Ignored Ignored Mar 18, 2026 2:08am
app-fm Ignored Ignored Mar 18, 2026 2:08am
app-link Ignored Ignored Mar 18, 2026 2:08am
app-pub Ignored Ignored Mar 18, 2026 2:08am
app-vip Ignored Ignored Mar 18, 2026 2:08am
app-work Ignored Ignored Mar 18, 2026 2:08am
art Ignored Ignored Mar 18, 2026 2:08am
baresky Ignored Ignored Mar 18, 2026 2:08am
bio Ignored Ignored Mar 18, 2026 2:08am
chat Ignored Ignored Mar 18, 2026 2:08am
pub Ignored Ignored Mar 18, 2026 2:08am
work Ignored Ignored Mar 18, 2026 2:08am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR fixes a silent "Save Draft" failure in the email broadcast flow by removing the replyTo field from createEmailBroadcastWithTemplateSchema — an empty-string default was failing z.email() validation without surfacing an error, causing the mutation to never fire. It also scopes the Fans unique index from a global (email) constraint to a composite (email, workspaceId) constraint to support the same fan email across multiple workspaces, and corrects both onConflictDoUpdate call sites to match the new target. Two layout files receive a ReactElementReactNode type correction to resolve typecheck errors.

Key changes:

  • Email broadcast schema (email-broadcast.schema.ts, email-broadcast.route.ts, modal): replyTo removed from the "create with template" flow; replyTo continues to be sourced from the EmailAddress record at send time, so no behavioral regression.
  • Fan unique index (fan.sql.ts): changed from ON (email) to ON (email, workspaceId). This directly reverses the direction of the existing 2025-08-06-global-fan-uniqueness migration, which had consolidated all fans globally. Both upsert call sites in bio-render.route.ts and vip-swap-render.route.ts are updated to use the composite target — but this code will break fan signups at runtime until pnpm db:push (or equivalent DDL) is run, since the database still holds the old single-column unique index.
  • Layout type fixes: ReactElementReactNode in two Next.js layout components.

Confidence Score: 2/5

  • The email broadcast fix is safe, but the fan index change introduces a deployment-order hazard that will break all bio and VIP fan signups until the DB schema is pushed.
  • The root cause fix for "Save Draft" is clean and correct. However, the composite unique index on Fans is only in the Drizzle schema file — the database still holds the old single-column unique_email index. Both onConflictDoUpdate call sites target the new composite key, which means every returning fan signup via bio or VIP swap will throw a Postgres runtime error ("there is no unique constraint matching the given keys") the moment this code is live, until db:push is executed. This is a hard runtime failure for a core user acquisition path.
  • packages/db/src/sql/fan.sql.ts — composite index change requires a db:push before the bio-render and vip-swap-render routes will function correctly

Important Files Changed

Filename Overview
packages/db/src/sql/fan.sql.ts Unique index changed from single-column (email) to composite (email, workspaceId); no accompanying migration/db:push is included, so the onConflictDoUpdate calls in bio-render and vip-swap-render will fail at runtime until the DB schema is updated.
packages/validators/src/schemas/email-broadcast.schema.ts replyTo field correctly removed from createEmailBroadcastWithTemplateSchema; the empty-string default was causing silent z.email() validation failure that blocked "Save Draft".
packages/lib/src/trpc/routes/email-broadcast.route.ts replyTo removed from createWithTemplate mutation consistently with the schema change; backend still reads replyTo from EmailAddress when sending, so no data is lost.
packages/lib/src/trpc/routes/bio-render.route.ts onConflictDoUpdate target correctly updated to [Fans.email, Fans.workspaceId] to match new composite index; will break at runtime until the DB index is created.
packages/lib/src/trpc/routes/vip-swap-render.route.ts onConflictDoUpdate target and comment updated to reflect new composite uniqueness; same deployment dependency on DB schema push as bio-render route.
apps/app/src/app/[handle]/email/broadcasts/_components/create-or-update-email-broadcast-modal.tsx replyTo: '' default value removed from createNewForm; consistent with schema change and no UI regression since SendTestEmail in update/use-template modes still receives replyTo from the template record.
apps/app/src/app/[handle]/bios/(edit)/layout.tsx ReactElement → ReactNode type fix; correct — Next.js layouts can receive any renderable node, not just a single React element.
apps/app/src/app/[handle]/layout.tsx Same ReactElement → ReactNode fix applied to the handle layout; correct change.

Sequence Diagram

sequenceDiagram
    participant UI as Browser (Modal)
    participant tRPC as emailBroadcast.createWithTemplate
    participant DB as PostgreSQL

    Note over UI,DB: Before fix — replyTo: '' fails z.email() silently
    UI->>tRPC: createWithTemplate({ ..., replyTo: '' })
    tRPC-->>UI: Zod validation error (swallowed)

    Note over UI,DB: After fix — replyTo removed from schema
    UI->>tRPC: createWithTemplate({ name, fromId, subject, body, ... })
    tRPC->>DB: INSERT INTO EmailTemplates (no replyTo)
    DB-->>tRPC: emailTemplate
    tRPC->>DB: INSERT INTO EmailBroadcasts
    DB-->>tRPC: emailBroadcast
    tRPC-->>UI: { emailBroadcast, emailTemplate }

    Note over UI,DB: Fan upsert — requires composite index in DB
    UI->>tRPC: bioRender / vipSwapRender (fan sign-up)
    tRPC->>DB: INSERT INTO Fans ON CONFLICT (email, workspaceId) DO UPDATE
    alt composite index exists (after db:push)
        DB-->>tRPC: fan record (upserted)
    else old single-column index still in DB
        DB-->>tRPC: ERROR: no unique constraint matching (email, workspaceId)
    end
Loading

Last reviewed commit: "fix: align fan upser..."

},
f => ({
unique: uniqueIndex('unique_email').on(f.email),
unique: uniqueIndex('unique_email').on(f.email, f.workspaceId),
Copy link
Contributor

Choose a reason for hiding this comment

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

P0 onConflictDoUpdate calls will break at runtime

The unique index is changed from (email) to (email, workspaceId), but two fan-creation routes still use target: [Fans.email] in their onConflictDoUpdate calls, which must exactly match an existing unique constraint or index. Since the unique_email index no longer covers just email, PostgreSQL will throw an error at runtime.

Affected locations:

  • packages/lib/src/trpc/routes/bio-render.route.ts:238-247.onConflictDoUpdate({ target: [Fans.email], set: { ... } })
  • packages/lib/src/trpc/routes/vip-swap-render.route.ts:170-176.onConflictDoUpdate({ target: [Fans.email], ... }) (even has a now-incorrect comment: // Only email is unique, not email+workspace)

These two paths handle every new fan sign-up via bio pages and VIP swaps. Both will fail whenever a repeat visitor returns to sign up, causing fan creation to error out entirely.

Both call sites need to be updated to match the new composite constraint:

.onConflictDoUpdate({
  target: [Fans.email, Fans.workspaceId],
  set: { ... },
})

@github-actions
Copy link

apps

📱 app 🚀 preview 🔍 inspect
📱 app preview inspect
📻 app-fm preview inspect
🧾 app-invoice preview inspect
🌱 bio preview inspect
🛒 cart preview inspect
📻 fm preview inspect
🧾 invoice preview inspect
🔗 link preview inspect
📧 manage-email preview inspect
🌆 nyc preview inspect
📄 page preview inspect
📰 press preview inspect
⭐ vip preview inspect
🌐 www preview inspect

db

💾 db 🔍 inspect
🐘 neon inspect

The Fans table unique index changed from (email) to (email, workspaceId),
but two routes still used the old target, causing runtime errors on
repeat fan sign-ups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@adambarito
Copy link
Contributor Author

@greptileai pls update the code review based on new changes.

@github-actions
Copy link

apps

📱 app 🚀 preview 🔍 inspect
📱 app preview inspect
📻 app-fm preview inspect
🧾 app-invoice preview inspect
🌱 bio preview inspect
🛒 cart preview inspect
📻 fm preview inspect
🧾 invoice preview inspect
🔗 link preview inspect
📧 manage-email preview inspect
🌆 nyc preview inspect
📄 page preview inspect
📰 press preview inspect
⭐ vip preview inspect
🌐 www preview inspect

db

💾 db 🔍 inspect
🐘 neon inspect

@adambarito adambarito added this pull request to the merge queue Mar 18, 2026
Merged via the queue into main with commit f982660 Mar 18, 2026
101 checks passed
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