# Code Review: FRE-580 — Email Marketing Sequences **Date**: 2026-05-13 **Reviewer**: Code Reviewer (f274248f-c47e-4f79-98ad-45919d951aa0) **Author**: Senior Engineer (c99c4ede-feab-4aaa-a9a5-17d81cd80644) **Run ID**: $PAPERCLIP_RUN_ID --- ## Files Reviewed | File | Lines | Description | |------|-------|-------------| | `server/services/email-service.ts` | 111 | Resend email sender with template rendering | | `server/services/email-templates.ts` | 418 | HTML/text templates for all sequences | | `server/services/email-sequence-service.ts` | 527 | Sequence orchestration | | `server/trpc/routers/email-marketing.ts` | 156 | tRPC API endpoints | | `server/trpc/appRouter.ts` | 33 | Router registration | | `src/db/schema/email_marketing.ts` | 132 | Database schema (reviewed for completeness) | **Total**: 1,377 lines --- ## P1 Issues ### 1. Missing Scheduler Integration **Severity**: Critical **Location**: `server/services/email-sequence-service.ts:165` The `processDueSteps` method is the core scheduling mechanism but is never actually called by any scheduler. The tRPC endpoint `processSequence` exists but requires manual admin invocation. **Required Fix**: Add a cron-based scheduler (e.g., `node-cron` or `@upstash/cron`) that calls `processDueSteps` for each sequence type on an appropriate interval (every 5-15 minutes). --- ### 2. Welcome Sequence Enrollment Not Wired **Severity**: Critical **Location**: `server/services/email-sequence-service.ts:124` The welcome sequence has `triggerEvent: 'user_signed_up'` but there is no registration of a signup event handler that calls `enrollUser(userId, 'welcome', email)`. **Required Fix**: Register a signup event listener (or add a hook in the auth registration flow) that calls `emailSequenceService.enrollUser(userId, 'welcome', email)` after user creation. --- ### 3. Email Send Status Tracking Incomplete **Severity**: Critical **Location**: `server/services/email-sequence-service.ts:267-275` ```typescript status: result.status === 'sent' || result.status === 'id' ? 'sent' : 'pending', ``` The Resend API returns a message ID (`id`) on success, not a `status` field. No webhook handlers are implemented to process delivery events. **Required Fix**: Implement Resend webhook handlers (`/api/webhooks/resend`) to process delivery events (delivered, opened, clicked, bounced, unsubscribed) and update `emailSendLog` status accordingly. --- ## P2 Issues ### 4. No Deduplication for Concurrent Scheduler Runs **Severity**: High **Location**: `server/services/email-sequence-service.ts:165-216` If the scheduler runs twice concurrently, the same enrollments could be processed twice. **Required Fix**: Add a mutex/lock mechanism or use database-level locking (`SELECT FOR UPDATE`). --- ### 5. tRPC `processSequence` Allows Any Authenticated User **Severity**: High **Location**: `server/trpc/routers/email-marketing.ts:135-145` Any logged-in user can trigger sequence processing. Should be restricted to admin users. **Required Fix**: Add an admin-only middleware check. --- ### 6. `enrollSequence` tRPC Endpoint Accepts Empty Email **Severity**: High **Location**: `server/trpc/routers/email-marketing.ts:102-113` The email parameter is hardcoded to empty string. **Required Fix**: Fetch the current user's email from the users table before enrolling. --- ### 7. Template Initialization stepNumber Mapping is Fragile **Severity**: High **Location**: `server/services/email-sequence-service.ts:98-110` The uniqueness check uses `stepNumber === delayHours`, but stepNumber is mapped differently (0→1, 24→2, 72→3). This means the lookup will never find existing templates. **Required Fix**: Use the correct stepNumber mapping for the lookup, or add a unique constraint on `sequence + stepNumber` where stepNumber is the actual ordinal (1, 2, 3). --- ## P3 Issues ### 8. No Unsubscribe Link Tracking No tRPC endpoint or API route to handle unsubscribe actions. ### 9. No Rate Limiting on Email Sending Could hit Resend API rate limits or trigger spam filters. ### 10. Analytics Query Uses String Concatenation for SQL Bypasses drizzle-orm's parameter binding. ### 11. No Error Handling for Email Service Failures Failed emails are silently lost. ### 12. No A/B Testing Implementation Beyond Schema No logic for traffic splitting, variant selection, or statistical significance. --- ## Architecture Assessment **Positive**: - Template registry pattern is clean and extensible - Drizzle-ORM schema is well-structured with proper constraints - tRPC router follows project conventions - HTML templates use inline styles (email-client compatible) - Both HTML and text versions provided for all templates - UTM tracking for analytics is implemented **Areas for Improvement**: - Missing production infrastructure (scheduler, webhooks) - No error recovery for email delivery failures - Analytics would be incomplete without webhook integration --- ## Final Disposition **Status**: `in_progress` — Assigned back to Senior Engineer **Priority Fixes Needed**: 1. Add scheduler cron job for `processDueSteps` 2. Wire welcome sequence enrollment to signup event 3. Implement Resend webhook handlers for delivery tracking **Secondary Fixes (P2)**: - Add admin-only access to `processSequence` - Fix template initialization stepNumber mapping - Add concurrent execution protection --- *Review Document: `/home/mike/code/FrenoCorp/agents/code-reviewer/reviews/FRE-580-review.md`*