Back to .md Directory

DRAMAC Marketing Module — Complete Implementation Master Guide

**Document Type**: Master Implementation Guide (AI Agent Reference)

May 2, 2026
0 downloads
1 views
ai agent claude workflow automation
View source

DRAMAC Marketing Module — Complete Implementation Master Guide

Document Type: Master Implementation Guide (AI Agent Reference)
Created: April 11, 2026
Platform: DRAMAC CMS — Enterprise Module Marketplace
Production URL: https://app.dramacagency.com
Module Prefix: mod_mktmod01_
Module Slug: marketing


How to Use This Guide

This document is the single source of truth for building the DRAMAC Marketing Module. It is designed to be consumed by AI agents across multiple sessions.

Workflow Per Session

  1. Read this entire guide to understand the full vision
  2. Read ALL memory bank files (/memory-bank/*.md) to understand current platform state
  3. Pick the next unimplemented phase from the phase list below
  4. Create a detailed phase document (e.g., PHASE-MKT-01-DATABASE-FOUNDATION.md) with step-by-step implementation plan
  5. Implement that phase completely — all code, migrations, types, tests
  6. Verify: Run npx tsc --noEmit --skipLibCheck with NODE_OPTIONS="--max-old-space-size=8192" — zero errors required
  7. Build: Run npx next build — must succeed
  8. Commit: git add -A && git commit -m "feat: Phase MKT-XX — [description]" && git push
  9. Update memory bank: Update activeContext.md and progress.md with what was actually built
  10. Do NOT modify this master guide — it stays as the north star vision

Critical Platform Rules (MUST Follow)

  • All prices in CENTS (integers) — K250.00 = 25000
  • Supabase returns snake_case — always use mapRecord()/mapRecords() from @/lib/map-db-record
  • Every server page needs auth guard: if (!user) redirect('/login')
  • AI Zod schemas: No .int(), .min(), .max() — Claude rejects these
  • Vercel function timeout: maxDuration = 60 on all AI API routes
  • Locale: Zambia-first — ZMW currency, K symbol, Africa/Lusaka timezone, 16% VAT
  • Email sender: Dramac <noreply@app.dramacagency.com> via Resend
  • Module DB prefix: All tables MUST be mod_mktmod01_*
  • No dark: Tailwind variants in storefront/public components
  • Use semantic Tailwind: bg-card, text-foreground, bg-primary — never hardcoded colors
  • 'use client' components must NOT contain inline 'use server' annotations
  • Import server actions as functions from separate files
  • Toast notifications: Use sonner (not useToast)
  • Font inline styles: Always use fontFamily: value || undefined (never empty string)

Table of Contents

Foundation

  • Phase MKT-01: Database Foundation, Module Registration & Type System
  • Phase MKT-02: Email Campaign Engine (Builder, Sending, Templates)
  • Phase MKT-03: Email Analytics & Tracking (Opens, Clicks, Bounces)

Core Marketing

  • Phase MKT-04: Drip Sequences & Marketing Automation Templates
  • Phase MKT-05: Marketing Hub Dashboard (Unified Analytics)
  • Phase MKT-06: Landing Pages & Conversion Funnels

Content & Channels

  • Phase MKT-07: Blog Marketing Enhancement (CTAs, Lead Capture, RSS, Content-to-Email)
  • Phase MKT-08: SMS & WhatsApp Marketing (Multi-Channel Campaigns)

Intelligence & Administration

  • Phase MKT-09: AI Marketing Intelligence (Subject Lines, Send Times, Personalization)
  • Phase MKT-10: Super Admin Platform Controls (Quotas, Deliverability, Abuse)
  • Phase MKT-11: Client Portal Marketing Views (Permission-Gated)
  • Phase MKT-12: Cross-Module Deep Integration (Chat AI, E-Commerce, Booking, CRM, Social)

Platform Architecture Context

Multi-Tenant Hierarchy

Super Admin (Platform operator — DRAMAC)
    └── Agency (Organization that manages client businesses)
            ├── Team Members (roles: owner, admin, member)
            │   └── 21+ granular permissions across 5 categories
            ├── Clients (businesses the agency manages)
            │   └── Client Portal Access (12 permission columns)
            ├── Sites (websites/storefronts per client)
            │   ├── Installed Modules (from marketplace)
            │   ├── Pages (DRAMAC Studio visual builder)
            │   ├── Blog, SEO, Forms, Media
            │   └── Storefront (customer auth, cart, orders)
            └── Module Subscriptions (agency pays via Paddle)

Where Marketing Lives in This Hierarchy

  • Marketing data is SITE-SCOPED — campaigns belong to a site, not an agency
  • Agency manages marketing for their clients' sites (same as e-commerce, CRM, etc.)
  • Client portal shows marketing data for their site(s) based on permissions
  • Super admin sees platform-wide email health and sending metrics
  • Storefront visitors see: opt-in forms, landing pages, email preference center

Existing Module Dependencies

The Marketing Module DEPENDS ON and INTEGRATES WITH:

ModuleHow Marketing Uses It
CRM (mod_crmmod01_*)Contacts are the audience. Segments become targeting criteria. Tags used for personalization. Lead scores updated by engagement.
Automation (automation_*)Workflow engine processes drip sequences. Marketing events trigger workflows. Marketing actions available in workflow builder.
E-Commerce (mod_ecommod01_*)Abandoned cart data, purchase history for segmentation. Flash sales auto-create campaigns. Product recommendations in emails.
Live Chat (mod_chat_*)AI responder reads email engagement data. Chat events trigger marketing sequences.
Social Media (social media module)Campaign calendar syncs. Attribution tracking. Social posts can promote email campaigns.
Blog (blog tables)Content-to-email pipeline. Blog CTAs capture subscribers. RSS auto-newsletters.
Email System (src/lib/email/)Reuses Resend client, branding system, template rendering. Marketing adds bulk sending + tracking on top.

Tech Stack Reference

  • Framework: Next.js 16.1.1, React 19, TypeScript strict
  • Database: Supabase PostgreSQL with RLS
  • UI: Radix UI + Tailwind CSS 4 + Framer Motion
  • State: Zustand + TanStack Query + React Hook Form + Zod
  • Email Provider: Resend (REST API)
  • AI: Claude Sonnet 4.6 (primary), Haiku 4.5 (fast tasks)
  • Charts: Recharts
  • Workflow Canvas: ReactFlow (used by Automation module)
  • Rich Text: TipTap
  • Billing: Paddle (Merchant of Record)
  • Auth: Supabase Auth (dashboard/portal) + bcrypt custom (storefront)

Relationship: Marketing Module vs Automation Module

They Are DIFFERENT Layers

┌─────────────────────────────────────────────────────────┐
│  MARKETING MODULE (Campaign Management Layer)            │
│                                                          │
│  "WHAT to send, to WHOM, WHEN, and measure RESULTS"     │
│                                                          │
│  • Campaign builder & scheduler                          │
│  • Visual email template designer                        │
│  • Audience builder (wraps CRM segments)                 │
│  • A/B testing framework                                 │
│  • Email analytics (opens, clicks, bounces, revenue)     │
│  • Marketing hub dashboard                               │
│  • Landing page funnels                                  │
│  • Blog-to-email pipeline                                │
│  • Multi-channel (email + SMS + WhatsApp)                │
│  • Attribution & ROI tracking                            │
│                                                          │
│  PRODUCES: marketing.* events                            │
│  CONSUMES: automation workflows for drip execution       │
└────────────────────┬────────────────────────────────────┘
                     │ USES (not replaces)
                     ▼
┌─────────────────────────────────────────────────────────┐
│  AUTOMATION MODULE (Universal Workflow Engine)            │
│                                                          │
│  "WHEN event X occurs, DO actions Y in sequence Z"       │
│                                                          │
│  • ReactFlow visual workflow canvas                      │
│  • 47+ event triggers from ALL modules                   │
│  • 20+ cross-module actions                              │
│  • Conditional branching, delays, loops                  │
│  • Webhook & schedule triggers                           │
│  • Execution history & error handling                    │
│                                                          │
│  CONSUMES: marketing.* events (15+ new types)            │
│  PROVIDES: Execution engine for drip sequences           │
└─────────────────────────────────────────────────────────┘

Example: Welcome Email Series

  1. Marketing Module UI: Agent creates "Welcome Series" campaign — designs 5 emails, sets delays between them, targets "New Subscribers" audience
  2. Under the hood: Marketing module creates an automation workflow with the email steps + delay nodes
  3. Trigger: When marketing.subscriber.added event fires → automation engine picks up the workflow
  4. Execution: Automation engine runs each step (send email → wait 2 days → send email → wait 3 days → ...)
  5. Tracking: Each email send fires marketing.email.sent → tracking pixel fires marketing.email.opened → link click fires marketing.email.clicked
  6. Dashboard: Marketing hub shows open rates, click rates, conversion attribution

What Gets Added to Automation (NOT Replaced)

New event types added to src/modules/automation/lib/event-types.ts:

marketing.campaign.created
marketing.campaign.started
marketing.campaign.paused
marketing.campaign.completed
marketing.email.sent
marketing.email.opened
marketing.email.clicked
marketing.email.bounced
marketing.email.unsubscribed
marketing.email.complained
marketing.subscriber.added
marketing.subscriber.removed
marketing.list.created
marketing.sms.sent
marketing.sms.delivered
marketing.sms.failed
marketing.funnel.step_completed
marketing.funnel.converted

New action types added to src/modules/automation/lib/action-types.ts:

marketing.send_campaign        — Trigger a campaign send to an audience
marketing.add_to_list          — Add contact to marketing list
marketing.remove_from_list     — Remove contact from marketing list
marketing.add_to_drip          — Enroll contact in drip sequence
marketing.remove_from_drip     — Remove contact from drip sequence
marketing.update_subscriber    — Update subscriber preferences
marketing.create_campaign      — Programmatically create a campaign
marketing.send_sms             — Send SMS to contact

Access Control Design

Agency Dashboard (/dashboard/sites/[siteId]/marketing/)

Who: Agency owners, admins, members with manage_marketing permission
What they can do:

  • Full campaign management (create, edit, send, schedule, pause, cancel)
  • Email template design and management
  • Audience/list management
  • A/B testing configuration
  • View all analytics and reports
  • Configure drip sequences
  • Manage landing pages and funnels
  • Blog marketing tools (CTAs, lead capture)
  • SMS/WhatsApp campaigns
  • Marketing settings (sending limits, default sender info, compliance)

Routes:

/dashboard/sites/[siteId]/marketing/                    — Overview dashboard
/dashboard/sites/[siteId]/marketing/campaigns/          — Campaign list
/dashboard/sites/[siteId]/marketing/campaigns/new/      — Create campaign
/dashboard/sites/[siteId]/marketing/campaigns/[id]/     — Campaign detail + analytics
/dashboard/sites/[siteId]/marketing/templates/          — Email template library
/dashboard/sites/[siteId]/marketing/templates/[id]/     — Template editor
/dashboard/sites/[siteId]/marketing/audiences/          — Audience/list management
/dashboard/sites/[siteId]/marketing/audiences/[id]/     — Audience detail
/dashboard/sites/[siteId]/marketing/sequences/          — Drip sequences
/dashboard/sites/[siteId]/marketing/sequences/[id]/     — Sequence editor
/dashboard/sites/[siteId]/marketing/funnels/            — Landing page funnels
/dashboard/sites/[siteId]/marketing/funnels/[id]/       — Funnel editor
/dashboard/sites/[siteId]/marketing/analytics/          — Marketing analytics hub
/dashboard/sites/[siteId]/marketing/settings/           — Marketing settings

Client Portal (/portal/sites/[siteId]/marketing/)

Who: Clients with can_manage_marketing permission
What they can do (configurable per client):

  • View campaign performance (read-only by default)
  • View subscriber growth and list sizes
  • Create simple campaigns (if permitted)
  • Manage their own email lists
  • View marketing calendar
  • View conversion funnel reports

Routes:

/portal/sites/[siteId]/marketing/                       — Marketing overview
/portal/sites/[siteId]/marketing/campaigns/             — Campaign list (view or full)
/portal/sites/[siteId]/marketing/campaigns/[id]/        — Campaign detail
/portal/sites/[siteId]/marketing/subscribers/           — Subscriber management
/portal/sites/[siteId]/marketing/analytics/             — Analytics view

Super Admin (/admin/marketing/)

Who: Super admin only (profiles.role === 'super_admin')
What they can do:

  • Platform-wide email sending dashboard
  • Per-agency sending quotas and throttle controls
  • Deliverability monitoring (bounce rates, spam complaints across all agencies)
  • Abuse detection and auto-suspension
  • Global email template library (shared across all agencies)
  • Sending domain management and verification status
  • Resend account health metrics

Routes:

/admin/marketing/                                       — Platform email health dashboard
/admin/marketing/sending/                               — Sending metrics & quotas
/admin/marketing/deliverability/                        — Bounce/complaint monitoring
/admin/marketing/templates/                             — Global template library
/admin/marketing/domains/                               — Sending domain management

Public/Storefront (No Auth Required)

What visitors see:

  • Email opt-in forms (embedded in pages, blog posts, popups)
  • Landing pages (funnel entry points)
  • Email preference center (/unsubscribe?uid=... — already exists, will be enhanced)
  • Tracking pixel (invisible 1x1 image for open tracking)
  • Click redirect (link tracking through /api/marketing/track/click/[id])

Existing Code Integration Points

Files That Will Be MODIFIED (Not Replaced)

These existing files get NEW entries added. No existing functionality is removed or changed.

1. Automation Event Registry

File: src/modules/automation/lib/event-types.ts
Change: Add marketing category with 18 new event types
Risk: Zero — additive array entries

2. Automation Action Types

File: src/modules/automation/lib/action-types.ts
Change: Add marketing category with 8 new action types
Risk: Zero — additive array entries

3. Automation System Templates

File: src/modules/automation/lib/system-templates.ts
Change: Add 15+ pre-built marketing workflow templates
Risk: Zero — additive array entries

4. Email Types

File: src/lib/email/email-types.ts
Change: Add marketing email types (campaign_email, drip_email, sms_notification, etc.)
Risk: Zero — additive type union + interface additions

5. Email Templates

File: src/lib/email/templates.ts
Change: Add marketing email template renderers
Risk: Zero — additive registry entries

6. Dashboard Navigation

File: src/config/navigation.ts
Change: Add "Marketing" nav section under sites
Risk: Zero — additive nav items

7. Portal Navigation

File: src/config/portal-navigation.ts
Change: Add marketing portal nav items, gated by hasModule("marketing") && permissions.canManageMarketing
Risk: Zero — follows exact same pattern as other modules

8. Roles & Permissions

File: src/types/roles.ts
Change: Add manage_marketing permission
Risk: Zero — additive permission entry

9. Client Permissions

DB Migration: Add can_manage_marketing boolean column to clients table
Risk: Zero — nullable column with default false

10. Module Catalog

File: src/lib/modules/module-catalog.ts
Change: Add marketing module definition
Risk: Zero — additive catalog entry

11. Core Module Installation

File: src/lib/actions/sites.ts
Change: Add "marketing" to CORE_MODULE_SLUGS array
Risk: Zero — marketing auto-installs alongside CRM, Automation, Live Chat

12. CRM Contacts Table

DB Migration: Add 4 columns: email_opt_in, sms_opt_in, marketing_consent_date, consent_source
Risk: Zero — nullable columns, no existing data affected

13. Customer Context Bridge

File: src/modules/live-chat/lib/customer-context-bridge.ts
Change: Add email engagement data to AI context object
Risk: Low — additive data enrichment

14. Blog Post Editor

File: src/components/blog/post-editor.tsx
Change: Add CTA block and lead capture TipTap extensions to toolbar
Risk: Low — new toolbar buttons, existing functionality untouched

Files That Will Be CREATED (New)

All new marketing module code lives in these locations:

src/modules/marketing/
├── types/
│   ├── campaign-types.ts              — Campaign, template, audience types
│   ├── analytics-types.ts             — Tracking, metrics, attribution types
│   ├── sequence-types.ts              — Drip sequence types
│   ├── funnel-types.ts                — Landing page & funnel types
│   └── sms-types.ts                   — SMS/WhatsApp types
├── actions/
│   ├── campaign-actions.ts            — Campaign CRUD + send/schedule
│   ├── template-actions.ts            — Email template CRUD
│   ├── audience-actions.ts            — Audience/list management
│   ├── analytics-actions.ts           — Analytics queries
│   ├── sequence-actions.ts            — Drip sequence CRUD
│   ├── funnel-actions.ts              — Funnel CRUD
│   ├── subscriber-actions.ts          — Subscriber opt-in/out management
│   ├── sms-actions.ts                 — SMS campaign actions
│   └── settings-actions.ts            — Marketing settings
├── services/
│   ├── email-campaign-service.ts      — Bulk email sending with throttling
│   ├── tracking-service.ts            — Open/click pixel tracking
│   ├── bounce-handler.ts              — Resend webhook bounce/complaint handler
│   ├── audience-resolver.ts           — Resolves audiences to contact lists
│   ├── template-renderer.ts           — Renders campaign emails with merge vars
│   ├── attribution-service.ts         — Campaign attribution tracking
│   ├── sms-service.ts                 — Twilio SMS integration
│   └── ai-marketing-service.ts        — AI subject lines, send times, content
├── components/
│   ├── campaigns/
│   │   ├── campaign-list.tsx
│   │   ├── campaign-detail.tsx
│   │   ├── campaign-wizard.tsx        — Multi-step campaign creation
│   │   ├── campaign-analytics.tsx
│   │   └── ab-test-panel.tsx
│   ├── templates/
│   │   ├── template-list.tsx
│   │   ├── template-editor.tsx        — Drag-and-drop email builder
│   │   ├── template-preview.tsx
│   │   └── block-library.tsx          — Email builder blocks
│   ├── audiences/
│   │   ├── audience-list.tsx
│   │   ├── audience-builder.tsx       — Visual audience criteria builder
│   │   └── audience-detail.tsx
│   ├── sequences/
│   │   ├── sequence-list.tsx
│   │   ├── sequence-editor.tsx        — Visual drip sequence builder
│   │   └── sequence-analytics.tsx
│   ├── funnels/
│   │   ├── funnel-list.tsx
│   │   ├── funnel-editor.tsx
│   │   └── funnel-analytics.tsx
│   ├── analytics/
│   │   ├── marketing-dashboard.tsx    — Unified marketing hub
│   │   ├── campaign-metrics.tsx
│   │   ├── subscriber-growth.tsx
│   │   ├── channel-attribution.tsx
│   │   └── roi-calculator.tsx
│   ├── shared/
│   │   ├── merge-variable-picker.tsx  — Insert {{variables}} into templates
│   │   ├── send-time-picker.tsx       — Schedule with timezone support
│   │   ├── audience-selector.tsx      — Reusable audience picker
│   │   └── campaign-calendar.tsx      — Calendar view across all channels
│   └── blog/
│       ├── cta-block-extension.ts     — TipTap CTA block
│       ├── lead-capture-extension.ts  — TipTap lead capture form
│       └── content-to-email.tsx       — Blog post → newsletter converter
└── lib/
    ├── marketing-constants.ts         — Module constants, defaults
    ├── merge-variables.ts             — Available merge variables per context
    └── marketing-utils.ts             — Shared utilities

Dashboard Pages (new):

src/app/(dashboard)/dashboard/sites/[siteId]/marketing/
├── page.tsx                           — Marketing overview
├── campaigns/
│   ├── page.tsx                       — Campaign list
│   ├── new/page.tsx                   — Create campaign wizard
│   └── [campaignId]/page.tsx          — Campaign detail + analytics
├── templates/
│   ├── page.tsx                       — Template library
│   └── [templateId]/page.tsx          — Template editor
├── audiences/
│   ├── page.tsx                       — Audience list
│   └── [audienceId]/page.tsx          — Audience detail
├── sequences/
│   ├── page.tsx                       — Drip sequence list
│   └── [sequenceId]/page.tsx          — Sequence editor
├── funnels/
│   ├── page.tsx                       — Funnel list
│   └── [funnelId]/page.tsx            — Funnel editor
├── analytics/page.tsx                 — Marketing analytics hub
└── settings/page.tsx                  — Marketing settings

Portal Pages (new):

src/app/portal/sites/[siteId]/marketing/
├── page.tsx                           — Marketing overview
├── campaigns/
│   ├── page.tsx                       — Campaign list
│   └── [campaignId]/page.tsx          — Campaign detail
├── subscribers/page.tsx               — Subscriber management
└── analytics/page.tsx                 — Analytics view

Admin Pages (new):

src/app/(dashboard)/admin/marketing/
├── page.tsx                           — Platform email health
├── sending/page.tsx                   — Sending metrics & quotas
├── deliverability/page.tsx            — Bounce/complaint monitoring
├── templates/page.tsx                 — Global template library
└── domains/page.tsx                   — Sending domain management

API Routes (new):

src/app/api/marketing/
├── track/
│   ├── open/[trackingId]/route.ts     — 1x1 pixel open tracking
│   └── click/[trackingId]/route.ts    — Link click redirect tracking
├── webhooks/
│   └── resend/route.ts                — Resend bounce/complaint webhooks
├── subscribe/route.ts                 — Public subscriber opt-in endpoint
├── unsubscribe/route.ts               — Public unsubscribe endpoint
└── campaigns/
    └── send/route.ts                  — Campaign send execution (called by automation)

Phase MKT-01: Database Foundation, Module Registration & Type System

Purpose

Establish all database tables, RLS policies, TypeScript types, module registration, and permission wiring. This phase creates the skeleton that all subsequent phases build on.

Database Tables (15 tables, all prefixed mod_mktmod01_)

Table 1: mod_mktmod01_campaigns

The core campaign entity. Supports email, SMS, and multi-channel campaigns.

ColumnTypeConstraintsDescription
iduuidPK, default gen_random_uuid()Campaign ID
site_iduuidFK sites.id, NOT NULLSite this campaign belongs to
nametextNOT NULLCampaign name
descriptiontextCampaign description
typetextNOT NULL, CHECK (email, sms, whatsapp, multi_channel)Campaign channel type
statustextNOT NULL, DEFAULT 'draft', CHECK (draft, scheduled, sending, sent, paused, cancelled, archived)Campaign lifecycle
subject_linetextEmail subject line
preview_texttextEmail preview/preheader text
from_nametextSender name override
from_emailtextSender email override
reply_totextReply-to address
template_iduuidFK mod_mktmod01_email_templates.idEmail template used
content_htmltextRendered HTML content
content_texttextPlain text fallback
content_jsonjsonbStructured content (blocks for builder)
audience_iduuidFK mod_mktmod01_audiences.idTarget audience
segment_iduuidCRM segment ID (alternative to audience)
ab_test_enabledbooleanDEFAULT falseA/B testing active
ab_test_configjsonbA/B test variants (subject, content, send_time)
scheduled_attimestamptzWhen to send (null = manual)
started_attimestamptzWhen sending actually started
completed_attimestamptzWhen sending finished
total_recipientsintegerDEFAULT 0Total contacts targeted
total_sentintegerDEFAULT 0Emails actually sent
total_deliveredintegerDEFAULT 0Confirmed delivered
total_openedintegerDEFAULT 0Unique opens
total_clickedintegerDEFAULT 0Unique clicks
total_bouncedintegerDEFAULT 0Hard + soft bounces
total_unsubscribedintegerDEFAULT 0Unsubscribes from this campaign
total_complainedintegerDEFAULT 0Spam complaints
revenue_attributedintegerDEFAULT 0Revenue in CENTS attributed to this campaign
tagstext[]DEFAULT '{}'Campaign tags for filtering
metadatajsonbDEFAULT '{}'Extensible metadata
created_byuuidFK auth.users.idUser who created
created_attimestamptzDEFAULT now()
updated_attimestamptzDEFAULT now()

Indexes: site_id, status, type, scheduled_at, created_at
RLS: Agency members can CRUD for their sites. Clients read-only (if can_manage_marketing).

Table 2: mod_mktmod01_email_templates

Reusable email templates with drag-and-drop block structure.

ColumnTypeConstraintsDescription
iduuidPKTemplate ID
site_iduuidFK sites.idSite scope (NULL = global/platform template)
nametextNOT NULLTemplate name
descriptiontextTemplate description
categorytextDEFAULT 'custom', CHECK (welcome, promotional, newsletter, transactional, announcement, winback, seasonal, custom)Template category
thumbnail_urltextPreview image URL
content_jsonjsonbNOT NULLBlock-based template structure
content_htmltextPre-rendered HTML
content_texttextPlain text version
subject_linetextDefault subject line
preview_texttextDefault preview text
merge_variablestext[]DEFAULT '{}'Available merge vars (for UI hints)
is_systembooleanDEFAULT falsePlatform-provided template
is_activebooleanDEFAULT trueAvailable for use
usage_countintegerDEFAULT 0Times used in campaigns
created_byuuidCreator
created_attimestamptzDEFAULT now()
updated_attimestamptzDEFAULT now()

Indexes: site_id, category, is_system, is_active

Table 3: mod_mktmod01_audiences

Reusable audience definitions for targeting. Can wrap CRM segments or define custom criteria.

ColumnTypeConstraintsDescription
iduuidPKAudience ID
site_iduuidFK sites.id, NOT NULLSite scope
nametextNOT NULLAudience name
descriptiontextAudience description
typetextNOT NULL, CHECK (segment, list, all_subscribers, all_contacts, custom)Audience source
crm_segment_iduuidCRM segment ID (if type=segment)
filter_criteriajsonbCustom filter rules (same format as CRM segment filters)
filter_logictextDEFAULT 'and', CHECK (and, or)How to combine filters
exclude_criteriajsonbExclusion rules
contact_countintegerDEFAULT 0Cached count
last_evaluated_attimestamptzLast count refresh
is_activebooleanDEFAULT trueAvailable for use
created_byuuid
created_attimestamptzDEFAULT now()
updated_attimestamptzDEFAULT now()

Indexes: site_id, type, crm_segment_id

Table 4: mod_mktmod01_subscribers

Marketing email subscribers. Links to CRM contacts when possible.

ColumnTypeConstraintsDescription
iduuidPKSubscriber ID
site_iduuidFK sites.id, NOT NULLSite scope
emailtextNOT NULLEmail address
first_nametext
last_nametext
phonetextPhone for SMS
crm_contact_iduuidLinked CRM contact
statustextNOT NULL, DEFAULT 'active', CHECK (active, unsubscribed, bounced, complained, cleaned)Subscriber health
email_opt_inbooleanDEFAULT trueEmail marketing consent
sms_opt_inbooleanDEFAULT falseSMS marketing consent
consent_sourcetextHow they subscribed (form, import, api, manual, checkout, booking)
consent_datetimestamptzWhen they consented
consent_iptextIP at consent time
tagstext[]DEFAULT '{}'Subscriber tags
custom_fieldsjsonbDEFAULT '{}'Extensible custom data
engagement_scoreintegerDEFAULT 00-100, based on open/click history
last_email_sent_attimestamptzLast campaign email sent
last_email_opened_attimestamptzLast email opened
last_email_clicked_attimestamptzLast link clicked
total_emails_sentintegerDEFAULT 0
total_emails_openedintegerDEFAULT 0
total_emails_clickedintegerDEFAULT 0
bounce_countintegerDEFAULT 0
unsubscribed_attimestamptzWhen they unsubscribed (if applicable)
unsubscribe_reasontextReason for unsubscribing
created_attimestamptzDEFAULT now()
updated_attimestamptzDEFAULT now()

Indexes: site_id + email (UNIQUE), status, crm_contact_id, engagement_score
CRITICAL: UNIQUE constraint on (site_id, email) — one subscriber per email per site

Table 5: mod_mktmod01_lists

Marketing lists (manual subscriber groupings).

ColumnTypeConstraintsDescription
iduuidPKList ID
site_iduuidFK sites.id, NOT NULLSite scope
nametextNOT NULLList name
descriptiontext
typetextDEFAULT 'manual', CHECK (manual, import, form_capture, api)How list was created
subscriber_countintegerDEFAULT 0Cached count
is_double_opt_inbooleanDEFAULT falseRequires email confirmation
welcome_email_template_iduuidAuto-send on subscribe
tagstext[]DEFAULT '{}'
is_activebooleanDEFAULT true
created_byuuid
created_attimestamptzDEFAULT now()
updated_attimestamptzDEFAULT now()

Indexes: site_id, is_active

Table 6: mod_mktmod01_list_subscribers

Junction table linking subscribers to lists.

ColumnTypeConstraintsDescription
iduuidPK
list_iduuidFK mod_mktmod01_lists.id, NOT NULL
subscriber_iduuidFK mod_mktmod01_subscribers.id, NOT NULL
added_attimestamptzDEFAULT now()
sourcetextHow they were added (import, form, manual, api)

Indexes: list_id + subscriber_id (UNIQUE), subscriber_id

Table 7: mod_mktmod01_campaign_sends

Individual send records for each email in a campaign. For tracking open/click per recipient.

ColumnTypeConstraintsDescription
iduuidPKSend record ID (also used as tracking ID)
campaign_iduuidFK mod_mktmod01_campaigns.id, NOT NULL
subscriber_iduuidFK mod_mktmod01_subscribers.id, NOT NULL
emailtextNOT NULLEmail sent to (denormalized for speed)
statustextNOT NULL, DEFAULT 'queued', CHECK (queued, sent, delivered, opened, clicked, bounced, complained, unsubscribed, failed)
resend_message_idtextResend API message ID for tracking
sent_attimestamptzWhen actually sent
delivered_attimestamptzWhen confirmed delivered
first_opened_attimestamptzFirst open time
last_opened_attimestamptzMost recent open
open_countintegerDEFAULT 0Total opens (including re-opens)
first_clicked_attimestamptzFirst click time
last_clicked_attimestamptzMost recent click
click_countintegerDEFAULT 0Total clicks
clicked_linksjsonbDEFAULT '[]'Array of {url, clicked_at}
bounced_attimestamptz
bounce_typetexthard, soft
bounce_reasontext
unsubscribed_attimestamptz
complained_attimestamptz
ab_varianttextA/B test variant identifier (A, B, etc.)
metadatajsonbDEFAULT '{}'
created_attimestamptzDEFAULT now()

Indexes: campaign_id, subscriber_id, email, status, resend_message_id
CRITICAL: This table can grow VERY large. Partition by month or add cleanup policy.

Table 8: mod_mktmod01_sequences

Drip email sequences (multi-step automated campaigns).

ColumnTypeConstraintsDescription
iduuidPKSequence ID
site_iduuidFK sites.id, NOT NULL
nametextNOT NULLSequence name
descriptiontext
trigger_typetextNOT NULL, CHECK (subscriber_added, tag_added, form_submitted, event, manual)What starts the sequence
trigger_configjsonbDEFAULT '{}'Trigger-specific config (list_id, tag_name, form_id, event_type)
statustextDEFAULT 'draft', CHECK (draft, active, paused, archived)
stepsjsonbNOT NULL, DEFAULT '[]'Array of sequence steps (email, delay, condition, action)
automation_workflow_iduuidLinked automation workflow (generated)
total_enrolledintegerDEFAULT 0Total contacts that entered
total_completedintegerDEFAULT 0Completed all steps
total_convertedintegerDEFAULT 0Hit conversion goal
conversion_goaljsonb{event_type, conditions} defining success
enrollment_limitintegerMax contacts in sequence at once
re_enrollmentbooleanDEFAULT falseCan contacts re-enter
created_byuuid
created_attimestamptzDEFAULT now()
updated_attimestamptzDEFAULT now()

Step Structure (in steps JSONB):

[
  {
    "step_id": "uuid",
    "type": "email",
    "delay_amount": 0,
    "delay_unit": "immediately",
    "template_id": "uuid",
    "subject_line": "Welcome!",
    "ab_test": null
  },
  {
    "step_id": "uuid",
    "type": "delay",
    "delay_amount": 2,
    "delay_unit": "days"
  },
  {
    "step_id": "uuid",
    "type": "condition",
    "condition": { "field": "last_email_opened", "operator": "is_true" },
    "if_true_step": "uuid",
    "if_false_step": "uuid"
  },
  {
    "step_id": "uuid",
    "type": "email",
    "delay_amount": 0,
    "delay_unit": "immediately",
    "template_id": "uuid",
    "subject_line": "Did you see our offer?"
  }
]

Table 9: mod_mktmod01_sequence_enrollments

Tracks contacts currently enrolled in a drip sequence.

ColumnTypeConstraintsDescription
iduuidPK
sequence_iduuidFK mod_mktmod01_sequences.id, NOT NULL
subscriber_iduuidFK mod_mktmod01_subscribers.id, NOT NULL
current_step_idtextWhich step they're on
statustextDEFAULT 'active', CHECK (active, paused, completed, converted, exited, failed)
enrolled_attimestamptzDEFAULT now()
next_step_attimestamptzWhen next step should execute
completed_attimestamptz
exit_reasontextWhy they left early
steps_completedjsonbDEFAULT '[]'Array of {step_id, completed_at, status}
metadatajsonbDEFAULT '{}'

Indexes: sequence_id, subscriber_id, status, next_step_at
UNIQUE: (sequence_id, subscriber_id) when re_enrollment = false

Table 10: mod_mktmod01_funnels

Landing page conversion funnels.

ColumnTypeConstraintsDescription
iduuidPK
site_iduuidFK sites.id, NOT NULL
nametextNOT NULLFunnel name
descriptiontext
statustextDEFAULT 'draft', CHECK (draft, active, paused, archived)
stepsjsonbNOT NULL, DEFAULT '[]'Array of funnel steps
conversion_goaljsonbWhat counts as conversion
total_visitorsintegerDEFAULT 0
total_conversionsintegerDEFAULT 0
conversion_ratenumeric(5,2)DEFAULT 0Percentage
ab_test_enabledbooleanDEFAULT false
ab_test_configjsonbVariant configurations
created_byuuid
created_attimestamptzDEFAULT now()
updated_attimestamptzDEFAULT now()

Funnel Step Structure:

[
  {
    "step_id": "uuid",
    "type": "landing_page",
    "page_id": "uuid",
    "name": "Offer Page"
  },
  {
    "step_id": "uuid",
    "type": "form",
    "form_config": { "fields": [...] },
    "name": "Sign Up Form"
  },
  {
    "step_id": "uuid",
    "type": "thank_you",
    "page_id": "uuid",
    "name": "Thank You Page"
  }
]

Table 11: mod_mktmod01_funnel_visits

Tracking visitor progress through funnels.

ColumnTypeConstraintsDescription
iduuidPK
funnel_iduuidFK mod_mktmod01_funnels.id, NOT NULL
visitor_idtextNOT NULLAnonymous visitor ID or subscriber ID
subscriber_iduuidLinked subscriber (after identification)
current_step_idtextCurrent funnel step
statustextDEFAULT 'in_progress', CHECK (in_progress, completed, abandoned)
steps_visitedjsonbDEFAULT '[]'Array of {step_id, visited_at, time_spent_seconds}
sourcetextTraffic source (utm_source or referrer)
utm_paramsjsonbFull UTM parameters
entered_attimestamptzDEFAULT now()
converted_attimestamptz
abandoned_attimestamptz
ip_addresstextFor geographic analytics
user_agenttextDevice/browser info

Indexes: funnel_id, visitor_id, status, entered_at

Table 12: mod_mktmod01_campaign_links

Tracked links within campaign emails.

ColumnTypeConstraintsDescription
iduuidPKLink tracking ID
campaign_iduuidFK mod_mktmod01_campaigns.id, NOT NULL
original_urltextNOT NULLOriginal destination URL
tracking_urltextNOT NULLDecorated tracking URL
display_texttextLink text in email
positionintegerPosition in email (for heatmap)
total_clicksintegerDEFAULT 0
unique_clicksintegerDEFAULT 0
created_attimestamptzDEFAULT now()

Indexes: campaign_id, tracking_url

Table 13: mod_mktmod01_settings

Per-site marketing settings.

ColumnTypeConstraintsDescription
iduuidPK
site_iduuidFK sites.id, UNIQUE, NOT NULLOne settings row per site
default_from_nametextDefault sender name
default_from_emailtextDefault sender email
default_reply_totextDefault reply-to
sending_quota_dailyintegerDEFAULT 1000Max emails per day
sending_quota_monthlyintegerDEFAULT 25000Max emails per month
double_opt_in_enabledbooleanDEFAULT falseRequire email confirmation for new subscribers
auto_clean_bouncesbooleanDEFAULT trueAuto-remove hard bounced emails
auto_clean_complaintsbooleanDEFAULT trueAuto-remove spam complainers
unsubscribe_page_urltextCustom unsubscribe page
physical_addresstextRequired by CAN-SPAM
company_nametextFooter company name
gdpr_enabledbooleanDEFAULT falseEnable GDPR compliance features
consent_texttextCustom consent message for forms
timezonetextDEFAULT 'Africa/Lusaka'For scheduling
metadatajsonbDEFAULT '{}'
created_attimestamptzDEFAULT now()
updated_attimestamptzDEFAULT now()

Table 14: mod_mktmod01_sending_stats

Daily sending statistics for quota tracking and platform monitoring.

ColumnTypeConstraintsDescription
iduuidPK
site_iduuidFK sites.id, NOT NULL
datedateNOT NULLCalendar date
emails_sentintegerDEFAULT 0
emails_deliveredintegerDEFAULT 0
emails_bouncedintegerDEFAULT 0
emails_complainedintegerDEFAULT 0
sms_sentintegerDEFAULT 0
sms_deliveredintegerDEFAULT 0
sms_failedintegerDEFAULT 0

Indexes: site_id + date (UNIQUE), date

Table 15: mod_mktmod01_attribution

Campaign-to-revenue attribution tracking.

ColumnTypeConstraintsDescription
iduuidPK
site_iduuidFK sites.id, NOT NULL
campaign_iduuidFK mod_mktmod01_campaigns.idSource campaign
sequence_iduuidFK mod_mktmod01_sequences.idSource sequence
funnel_iduuidFK mod_mktmod01_funnels.idSource funnel
subscriber_iduuidFK mod_mktmod01_subscribers.idContact who converted
attribution_typetextNOT NULL, CHECK (first_touch, last_touch, linear)Attribution model
event_typetextNOT NULLWhat happened (order_created, booking_created, deal_won, form_submitted)
event_idtextID of the attributed event
revenueintegerDEFAULT 0Revenue in CENTS
attributed_attimestamptzDEFAULT now()
touchpoint_window_hoursintegerDEFAULT 168Hours between campaign interaction and conversion (default 7 days)

Indexes: site_id, campaign_id, sequence_id, subscriber_id, attributed_at

Module Registration

Add to modules_v2 table:

INSERT INTO modules_v2 (id, slug, name, description, category, ...) VALUES (
  gen_random_uuid(),
  'marketing',
  'Marketing',
  'Email campaigns, drip sequences, audiences, landing pages & funnels, multi-channel marketing with full analytics',
  'marketing',
  ...
);

Add to CORE_MODULE_SLUGS in src/lib/actions/sites.ts:

const CORE_MODULE_SLUGS = ["crm", "automation", "live-chat", "marketing"];

Permission Changes

Agency permissions — add to src/types/roles.ts:

manage_marketing; // Full marketing module access

Client portal — add column to clients table:

ALTER TABLE clients ADD COLUMN can_manage_marketing boolean DEFAULT false;

Client site permissions — add column to client_site_permissions table:

ALTER TABLE client_site_permissions ADD COLUMN can_manage_marketing boolean;

CRM Contact Enhancements

Add marketing consent columns to CRM contacts:

ALTER TABLE mod_crmmod01_contacts ADD COLUMN email_opt_in boolean DEFAULT false;
ALTER TABLE mod_crmmod01_contacts ADD COLUMN sms_opt_in boolean DEFAULT false;
ALTER TABLE mod_crmmod01_contacts ADD COLUMN marketing_consent_date timestamptz;
ALTER TABLE mod_crmmod01_contacts ADD COLUMN consent_source text;

Type System

Create comprehensive TypeScript interfaces in src/modules/marketing/types/. Every column above must have a corresponding camelCase TypeScript interface property. Follow exact patterns from existing modules (e.g., src/modules/ecommerce/types/).

RLS Policies

Every table needs row-level security policies following the exact same pattern used by other modules:

  • Agency members: CRUD access for their agency's sites
  • Clients: Read-only (or based on can_manage_marketing)
  • Service role: Full access (for automation engine)

MKT-01 Deliverables Checklist

  • Migration SQL file with all 15 tables + indexes + RLS
  • mod_mktmod01_settings auto-seed on marketing module installation (like seedDefaultDepartments does for live-chat)
  • TypeScript types for all entities (campaign-types.ts, analytics-types.ts, sequence-types.ts, funnel-types.ts)
  • Module registered in modules_v2 table and module catalog
  • "marketing" added to CORE_MODULE_SLUGS
  • manage_marketing permission added to roles
  • can_manage_marketing added to client permissions
  • CRM contact marketing columns migration
  • Dashboard navigation updated with Marketing section
  • Portal navigation updated with Marketing section (gated)
  • Admin navigation updated with Marketing section
  • Bootstrap function created (seeds settings on install)
  • Zero TypeScript errors
  • Successful build

Phase MKT-02: Email Campaign Engine

Priority: CRITICAL — Core of the marketing module Estimated Complexity: High Dependencies: Phase MKT-01 complete (all tables exist) Files Created: ~25 new files Files Modified: 2-3 existing files

2.1 Overview

This phase builds the complete email campaign creation, management, and sending system. It includes:

  • Campaign builder with audience selection
  • Visual email template designer (drag-and-drop block editor)
  • Template library with pre-built industry templates
  • Sending engine with Resend bulk API integration
  • A/B testing framework for subject lines and content
  • Bounce/complaint webhook handlers
  • Campaign scheduling and throttling

2.2 Campaign Builder UI

Location: src/app/dashboard/sites/[siteId]/marketing/campaigns/

Campaign List Page (page.tsx)

Route: /dashboard/sites/[siteId]/marketing/campaigns

Component Structure:

// page.tsx - Server component
// Fetches campaigns with stats summary

// Key features:
// - Tab filter: All | Draft | Scheduled | Sending | Sent | Paused
// - Search by campaign name
// - Sort by: created_at, scheduled_at, open_rate, click_rate
// - Bulk actions: Delete, Archive, Duplicate
// - Each row shows: name, status badge, audience count, open rate, click rate, sent date
// - "New Campaign" button → /campaigns/new

// Data fetch pattern (follows existing platform pattern):
const { data: campaigns } = await supabase
  .from("mod_mktmod01_campaigns")
  .select(
    `
    *,
    mod_mktmod01_campaign_stats(*)
  `,
  )
  .eq("site_id", siteId)
  .order("created_at", { ascending: false });

Status Badge Colors (follow existing design system):

  • draft → gray
  • scheduled → blue
  • sending → yellow/amber (animated pulse)
  • sent → green
  • paused → orange
  • cancelled → red

Campaign Creation Wizard (new/page.tsx)

Route: /dashboard/sites/[siteId]/marketing/campaigns/new

Multi-step wizard (follows existing platform wizard patterns):

Step 1: Campaign Details

// Fields:
// - name: string (required) — internal campaign name
// - subject_line: string (required) — email subject
// - preview_text: string (optional) — preview/preheader text
// - from_name: string (default: site name from settings)
// - from_email: string (default: site email from settings)
// - reply_to: string (optional)
// - campaign_type: 'regular' | 'ab_test' (radio toggle)
//
// AI Enhancement:
// - "Generate Subject Lines" button → calls Claude Haiku
// - Input: campaign name + target audience description
// - Output: 5 subject line suggestions with predicted engagement
// - User picks one or edits

Step 2: Audience Selection

// Audience source options:
// 1. "All Subscribers" — all contacts with email_subscribed = true
// 2. "CRM Segment" — select from existing CRM segments (dropdown)
// 3. "Contact Tag" — select tags to include/exclude
// 4. "Custom Filter" — build filter with conditions:
//    - Field (email, name, company, created_at, last_order_date, total_spent, etc.)
//    - Operator (equals, contains, greater_than, less_than, between, is_empty)
//    - Value
//    - AND/OR combiner for multiple conditions
//
// Live count preview: "This audience contains approximately X contacts"
// Exclude list: contacts who unsubscribed, bounced, or complained
//
// Data model: saves to campaign.audience_filter as JSONB:
// {
//   "type": "segment" | "all" | "tags" | "custom",
//   "segment_id": "uuid" | null,
//   "tags": { "include": ["tag1"], "exclude": ["tag2"] },
//   "conditions": [
//     { "field": "total_spent", "operator": "greater_than", "value": 100, "combiner": "AND" }
//   ]
// }

Step 3: Email Content

// Two options:
// 1. "Design from scratch" → opens visual email editor
// 2. "Use template" → opens template library picker
//
// Visual Email Editor (see Section 2.3 below)
// Template selected → loads into editor for customization

Step 4: Review & Schedule

// Summary of all settings
// Send options:
// 1. "Send Now" — immediate send
// 2. "Schedule" — date/time picker (site timezone from settings)
// 3. "Save as Draft" — save without scheduling
//
// "Send Test Email" button — sends to current user's email
// Estimated delivery time based on audience size and throttle settings
//
// On submit:
// - Creates campaign record with status 'scheduled' or 'draft'
// - If 'Send Now': triggers server action to begin sending
// - If 'Schedule': creates scheduled job entry
// - Fires automation event: marketing.campaign.created

Campaign Edit Page ([campaignId]/page.tsx)

Route: /dashboard/sites/[siteId]/marketing/campaigns/[campaignId]

Behavior by status:

  • draft → full edit, same wizard UI
  • scheduled → edit allowed, can reschedule or cancel
  • sending → read-only, live progress display
  • sent → read-only, redirects to stats/report view
  • paused → can resume or cancel

2.3 Visual Email Template Editor

Location: src/modules/marketing/components/email-editor/

This is a drag-and-drop block-based email editor. It generates clean, responsive HTML email markup.

Editor Architecture

email-editor/
├── EmailEditor.tsx              — Main editor wrapper
├── EditorCanvas.tsx             — Drop zone / preview area
├── EditorSidebar.tsx            — Block palette + settings panel
├── EditorToolbar.tsx            — Undo/redo, preview, save, device toggle
├── blocks/
│   ├── BlockRenderer.tsx        — Routes block type to component
│   ├── HeaderBlock.tsx          — Logo + navigation text
│   ├── TextBlock.tsx            — Rich text (heading, paragraph, list)
│   ├── ImageBlock.tsx           — Single image with alt text + link
│   ├── ButtonBlock.tsx          — CTA button (text, URL, style)
│   ├── DividerBlock.tsx         — Horizontal rule / spacer
│   ├── ColumnsBlock.tsx         — 2 or 3 column layout
│   ├── ProductBlock.tsx         — E-commerce product card(s)
│   ├── SocialBlock.tsx          — Social media icon links
│   ├── FooterBlock.tsx          — Unsubscribe link + address
│   ├── SpacerBlock.tsx          — Vertical spacing
│   ├── VideoBlock.tsx           — Video thumbnail with play button link
│   └── CodeBlock.tsx            — Raw HTML insertion (advanced)
├── properties/
│   ├── BlockProperties.tsx      — Settings panel for selected block
│   ├── TextProperties.tsx       — Font, size, color, alignment
│   ├── ImageProperties.tsx      — Upload, alt, link, border
│   ├── ButtonProperties.tsx     — Text, URL, bg color, border radius
│   └── LayoutProperties.tsx     — Padding, background, border
├── utils/
│   ├── block-types.ts           — TypeScript types for all blocks
│   ├── default-blocks.ts        — Default content for new blocks
│   ├── html-renderer.ts         — Converts block JSON → email HTML
│   ├── mjml-renderer.ts         — Optional: block JSON → MJML → HTML
│   └── sanitize.ts              — HTML sanitization for code blocks
└── hooks/
    ├── useEditorState.ts        — Block array state management
    ├── useDragDrop.ts           — Drag and drop logic
    └── useUndoRedo.ts           — Undo/redo stack

Block Data Model

// block-types.ts
interface EmailBlock {
  id: string; // nanoid
  type: BlockType;
  properties: Record<string, unknown>;
  children?: EmailBlock[]; // for ColumnsBlock
}

type BlockType =
  | "header"
  | "text"
  | "image"
  | "button"
  | "divider"
  | "columns"
  | "product"
  | "social"
  | "footer"
  | "spacer"
  | "video"
  | "code";

// Campaign content_json structure:
interface EmailContent {
  version: 1;
  globalStyles: {
    backgroundColor: string;
    fontFamily: string;
    contentWidth: number; // px, default 600
  };
  blocks: EmailBlock[];
}

// Example TextBlock properties:
interface TextBlockProps {
  content: string; // HTML string (sanitized)
  fontSize: number;
  fontWeight: "normal" | "bold";
  color: string;
  alignment: "left" | "center" | "right";
  padding: { top: number; right: number; bottom: number; left: number };
}

// Example ButtonBlock properties:
interface ButtonBlockProps {
  text: string;
  url: string;
  backgroundColor: string;
  textColor: string;
  borderRadius: number;
  alignment: "left" | "center" | "right";
  fullWidth: boolean;
  padding: { top: number; right: number; bottom: number; left: number };
}

HTML Email Rendering (html-renderer.ts)

// Converts EmailContent → valid email HTML
// CRITICAL: Must produce email-client-compatible HTML:
// - Inline CSS only (no <style> blocks — many clients strip them)
// - Table-based layout for Outlook compatibility
// - All images with explicit width/height
// - Fallback fonts
// - Max width 600px centered
// - Background colors via bgcolor attribute + CSS
// - Border radius via CSS only (Outlook ignores, degrades gracefully)
//
// MUST include:
// - {{unsubscribe_url}} placeholder in footer
// - {{tracking_pixel}} placeholder at bottom (for open tracking)
// - All links wrapped with {{track_click:ORIGINAL_URL}} placeholder
//
// These placeholders are replaced at send time with actual URLs
//
// Function signature:
export function renderEmailHtml(
  content: EmailContent,
  brandColors?: BrandColors,
): string;

Editor Features

1. Drag-and-drop blocks from sidebar palette to canvas
2. Click block to select → shows properties panel on right
3. Reorder blocks by dragging within canvas
4. Delete block (X button or Delete key)
5. Duplicate block
6. Undo/Redo (Ctrl+Z / Ctrl+Shift+Z)
7. Device preview toggle: Desktop (600px) | Mobile (375px)
8. "Preview in Browser" — opens rendered HTML in new tab
9. "Send Test" — sends to entered email address
10. Global styles: background color, font family, content width
11. Brand color integration — loads site's brand colors for quick access
12. Image upload via existing Supabase storage pattern
13. AI content generation — "Write this section" button on text blocks

2.4 Email Template Library

Location: src/modules/marketing/data/email-templates/

Pre-Built Templates (15 minimum)

// template-library.ts
export interface EmailTemplate {
  id: string;
  name: string;
  description: string;
  category: TemplateCategory;
  thumbnail: string; // path to preview image
  content: EmailContent; // block JSON
  tags: string[];
}

type TemplateCategory =
  | "welcome" // Welcome & onboarding
  | "newsletter" // Regular newsletters
  | "promotion" // Sales & promotions
  | "announcement" // Product/feature announcements
  | "event" // Event invitations
  | "ecommerce" // Order-related marketing
  | "reengagement" // Win-back campaigns
  | "seasonal"; // Holiday/seasonal

// REQUIRED templates (minimum 15):
// 1.  welcome-simple          — Clean welcome email
// 2.  welcome-onboarding      — Multi-step onboarding kickoff
// 3.  newsletter-standard     — Header + 3 articles + footer
// 4.  newsletter-minimal      — Simple text-focused newsletter
// 5.  promotion-sale          — Big hero image + CTA
// 6.  promotion-coupon        — Coupon/discount code highlight
// 7.  promotion-flash-sale    — Urgency-driven with countdown language
// 8.  announcement-product    — New product/feature showcase
// 9.  announcement-update     — Company/service update
// 10. event-invitation        — Event details with RSVP button
// 11. event-reminder          — Event reminder with countdown
// 12. ecommerce-abandoned     — Abandoned cart recovery
// 13. ecommerce-review        — Post-purchase review request
// 14. reengagement-winback    — "We miss you" re-engagement
// 15. seasonal-holiday        — Customizable holiday greeting

Custom Template Saving

// Users can save any campaign design as a reusable template
// Saved to mod_mktmod01_templates table
// Available in template picker for future campaigns
//
// API route: POST /api/sites/[siteId]/marketing/templates
// Body: { name, description, category, content_json }

2.5 Sending Engine

Location: src/modules/marketing/lib/sending-engine.ts

Architecture

// The sending engine handles bulk email delivery through Resend API.
// It processes campaigns in batches to respect rate limits and
// provide progress tracking.

// IMPORTANT: Uses existing Resend client from src/lib/email.ts
// Do NOT create a new Resend instance — import the existing one

import { resend } from "@/lib/email";

interface SendingConfig {
  batchSize: number; // emails per batch (default: 100)
  batchDelayMs: number; // delay between batches (default: 1000ms)
  maxConcurrent: number; // concurrent API calls per batch (default: 10)
  retryAttempts: number; // retry failed sends (default: 3)
  retryDelayMs: number; // delay between retries (default: 5000ms)
}

// Sending flow:
// 1. Resolve audience → list of contact emails
// 2. Filter out unsubscribed, bounced, complained contacts
// 3. Render email HTML from content_json
// 4. Replace personalization tokens ({{first_name}}, {{company}}, etc.)
// 5. Replace tracking placeholders (pixel, click URLs)
// 6. Split into batches
// 7. Send batches sequentially with delay
// 8. Update campaign stats in real-time
// 9. Log each send to mod_mktmod01_email_sends table
// 10. Fire automation event on completion

Audience Resolution

// audience-resolver.ts
export async function resolveAudience(
  supabase: SupabaseClient,
  siteId: string,
  audienceFilter: AudienceFilter,
): Promise<Contact[]> {
  // Base query: contacts for this site with email_subscribed = true
  let query = supabase
    .from("mod_crm01_contacts")
    .select("id, email, first_name, last_name, company, tags")
    .eq("site_id", siteId)
    .eq("email_subscribed", true)
    .is("email_bounced_at", null); // exclude bounced

  switch (audienceFilter.type) {
    case "all":
      // No additional filters
      break;
    case "segment":
      // Resolve segment conditions from CRM
      // Apply segment filter logic
      break;
    case "tags":
      if (audienceFilter.tags?.include?.length) {
        query = query.overlaps("tags", audienceFilter.tags.include);
      }
      if (audienceFilter.tags?.exclude?.length) {
        // Filter out contacts with excluded tags
        // Use .not() with overlaps
      }
      break;
    case "custom":
      // Apply custom conditions dynamically
      // Each condition maps to a Supabase filter
      break;
  }

  const { data } = await query;
  return data ?? [];
}

Personalization Engine

// personalization.ts
// Replaces tokens in email HTML with contact data
//
// Supported tokens:
// {{first_name}}      — contact.first_name or "there" fallback
// {{last_name}}       — contact.last_name or "" fallback
// {{full_name}}       — first + last or "there" fallback
// {{email}}           — contact.email
// {{company}}         — contact.company or "" fallback
// {{unsubscribe_url}} — generated unsubscribe link with token
// {{tracking_pixel}}  — 1x1 transparent gif with tracking URL
// {{site_name}}       — site name from settings
// {{current_year}}    — current year
//
// Implementation:
export function personalizeEmail(
  html: string,
  contact: Contact,
  campaign: Campaign,
  siteSettings: SiteSettings,
): string {
  const tokens: Record<string, string> = {
    first_name: contact.first_name || "there",
    last_name: contact.last_name || "",
    full_name: contact.first_name
      ? `${contact.first_name} ${contact.last_name || ""}`.trim()
      : "there",
    email: contact.email,
    company: contact.company || "",
    unsubscribe_url: generateUnsubscribeUrl(contact.id, campaign.id),
    tracking_pixel: generateTrackingPixelUrl(contact.id, campaign.id),
    site_name: siteSettings.site_name,
    current_year: new Date().getFullYear().toString(),
  };

  let result = html;
  for (const [key, value] of Object.entries(tokens)) {
    // Use global replace — token may appear multiple times
    // Sanitize value to prevent HTML injection
    const safeValue = escapeHtml(value);
    result = result.replaceAll(`{{${key}}}`, safeValue);
  }

  // Replace click tracking: {{track_click:https://example.com}}
  result = result.replace(/\{\{track_click:(.*?)\}\}/g, (_, url) =>
    generateClickTrackingUrl(contact.id, campaign.id, url),
  );

  return result;
}

Batch Sending Process

// sending-engine.ts
export async function sendCampaign(
  campaignId: string,
  siteId: string,
): Promise<void> {
  const supabase = createServiceClient(); // Service role for bulk ops

  // 1. Load campaign
  const campaign = await getCampaign(supabase, campaignId);
  if (campaign.status !== "scheduled" && campaign.status !== "sending") {
    throw new Error(`Cannot send campaign in ${campaign.status} status`);
  }

  // 2. Update status to sending
  await updateCampaignStatus(supabase, campaignId, "sending");

  // 3. Fire automation event
  await logAutomationEvent({
    event_type: "marketing.campaign.sending",
    site_id: siteId,
    metadata: { campaign_id: campaignId, campaign_name: campaign.name },
  });

  // 4. Resolve audience
  const contacts = await resolveAudience(
    supabase,
    siteId,
    campaign.audience_filter,
  );

  // 5. Initialize stats
  await initializeCampaignStats(supabase, campaignId, contacts.length);

  // 6. Render base HTML
  const baseHtml = renderEmailHtml(
    campaign.content_json,
    campaign.brand_colors,
  );

  // 7. Load site settings for personalization
  const siteSettings = await getSiteSettings(supabase, siteId);

  // 8. Process in batches
  const config = getSendingConfig();
  const batches = chunkArray(contacts, config.batchSize);

  let totalSent = 0;
  let totalFailed = 0;

  for (const batch of batches) {
    // Check if campaign was paused/cancelled
    const currentStatus = await getCampaignStatus(supabase, campaignId);
    if (currentStatus === "paused" || currentStatus === "cancelled") {
      break;
    }

    const results = await Promise.allSettled(
      batch.map(async (contact) => {
        const personalizedHtml = personalizeEmail(
          baseHtml,
          contact,
          campaign,
          siteSettings,
        );

        // A/B test: select variant if applicable
        const subject =
          campaign.campaign_type === "ab_test"
            ? selectABVariant(campaign, contact)
            : campaign.subject_line;

        try {
          const result = await resend.emails.send({
            from: `${campaign.from_name} <${campaign.from_email}>`,
            to: contact.email,
            subject: subject,
            html: personalizedHtml,
            replyTo: campaign.reply_to || undefined,
            headers: {
              "X-Campaign-Id": campaignId,
              "X-Contact-Id": contact.id,
              "List-Unsubscribe": `<${generateUnsubscribeUrl(contact.id, campaignId)}>`,
            },
          });

          // Log successful send
          await logEmailSend(supabase, {
            campaign_id: campaignId,
            contact_id: contact.id,
            email: contact.email,
            resend_id: result.data?.id,
            status: "sent",
            variant:
              campaign.campaign_type === "ab_test"
                ? getVariantLabel(contact)
                : null,
          });

          totalSent++;
          return { success: true, contactId: contact.id };
        } catch (error) {
          // Log failed send
          await logEmailSend(supabase, {
            campaign_id: campaignId,
            contact_id: contact.id,
            email: contact.email,
            status: "failed",
            error_message:
              error instanceof Error ? error.message : "Unknown error",
          });

          totalFailed++;
          return { success: false, contactId: contact.id, error };
        }
      }),
    );

    // Update running stats
    await updateCampaignProgress(supabase, campaignId, totalSent, totalFailed);

    // Throttle between batches
    if (batches.indexOf(batch) < batches.length - 1) {
      await delay(config.batchDelayMs);
    }
  }

  // 9. Finalize campaign
  const finalStatus = totalFailed === contacts.length ? "failed" : "sent";
  await finalizeCampaign(
    supabase,
    campaignId,
    finalStatus,
    totalSent,
    totalFailed,
  );

  // 10. Fire completion event
  await logAutomationEvent({
    event_type: "marketing.campaign.sent",
    site_id: siteId,
    metadata: {
      campaign_id: campaignId,
      total_sent: totalSent,
      total_failed: totalFailed,
      audience_size: contacts.length,
    },
  });
}

2.6 A/B Testing Framework

// ab-testing.ts
// When campaign_type is 'ab_test', the campaign has:
// - subject_line: variant A subject
// - ab_test_config: {
//     variant_b_subject: string,
//     split_percentage: number,    // 50 = 50/50 split (default)
//     winner_criteria: 'open_rate' | 'click_rate',
//     auto_send_winner: boolean,
//     test_duration_hours: number, // hours to wait before picking winner
//     test_sample_percentage: number // % of audience for test (rest gets winner)
//   }
//
// A/B test flow:
// 1. Split audience into test group (e.g., 20%) and remaining (80%)
// 2. Split test group 50/50 into variant A and variant B
// 3. Send variant A subject to half, variant B to other half
// 4. Wait test_duration_hours
// 5. Evaluate winner based on winner_criteria
// 6. If auto_send_winner: send winning variant to remaining 80%
// 7. If not auto: notify user, let them choose
//
// Variant assignment:
// - Deterministic based on contact.id hash (consistent assignment)
// - Or sequential: first half = A, second half = B
// - Stored in email_sends.variant column ('A' or 'B')

export function selectABVariant(campaign: Campaign, contact: Contact): string {
  // Simple hash-based split
  const hash = simpleHash(contact.id);
  const isVariantB =
    hash % 100 < (campaign.ab_test_config?.split_percentage ?? 50);
  return isVariantB
    ? campaign.ab_test_config!.variant_b_subject
    : campaign.subject_line;
}

2.7 API Routes

Location: src/app/api/sites/[siteId]/marketing/campaigns/

API Routes to create:

POST   /api/sites/[siteId]/marketing/campaigns
  → Create new campaign
  → Body: { name, subject_line, preview_text, from_name, from_email,
            reply_to, campaign_type, audience_filter, content_json,
            ab_test_config?, scheduled_at? }
  → Returns: campaign object

GET    /api/sites/[siteId]/marketing/campaigns
  → List campaigns with pagination
  → Query: ?status=draft&page=1&limit=20&search=keyword
  → Returns: { campaigns, total, page, limit }

GET    /api/sites/[siteId]/marketing/campaigns/[campaignId]
  → Get single campaign with stats
  → Returns: campaign object with stats joined

PATCH  /api/sites/[siteId]/marketing/campaigns/[campaignId]
  → Update campaign (only if draft/scheduled)
  → Body: partial campaign fields
  → Returns: updated campaign

DELETE /api/sites/[siteId]/marketing/campaigns/[campaignId]
  → Delete campaign (only if draft)
  → Soft delete: sets deleted_at timestamp

POST   /api/sites/[siteId]/marketing/campaigns/[campaignId]/send
  → Trigger campaign send
  → Validates campaign is ready (has content, audience, subject)
  → Starts sending engine
  → Returns: { status: 'sending', estimated_completion }

POST   /api/sites/[siteId]/marketing/campaigns/[campaignId]/pause
  → Pause sending campaign
  → Sets status to 'paused'

POST   /api/sites/[siteId]/marketing/campaigns/[campaignId]/resume
  → Resume paused campaign
  → Continues from where it stopped

POST   /api/sites/[siteId]/marketing/campaigns/[campaignId]/test
  → Send test email
  → Body: { email: "test@example.com" }
  → Sends single email with sample personalization

POST   /api/sites/[siteId]/marketing/campaigns/[campaignId]/duplicate
  → Duplicate campaign as new draft
  → Copies all settings except schedule

POST   /api/sites/[siteId]/marketing/templates
  → Save campaign design as reusable template
  → Body: { name, description, category, content_json }

GET    /api/sites/[siteId]/marketing/templates
  → List saved templates + built-in templates
  → Query: ?category=welcome&search=keyword

// Auth: All routes use existing withSiteAuth middleware
// All routes verify manage_marketing permission

2.8 Server Actions

Location: src/modules/marketing/actions/campaign-actions.ts

// Server actions for campaign operations (used by UI forms)
// Follow existing platform pattern: 'use server' + revalidatePath

"use server";

export async function createCampaignAction(
  siteId: string,
  data: CreateCampaignInput,
): Promise<ActionResult<Campaign>> {
  // 1. Verify auth + permission
  // 2. Validate input (zod schema)
  // 3. Insert campaign record
  // 4. If scheduled_at provided, create schedule entry
  // 5. Fire automation event: marketing.campaign.created
  // 6. revalidatePath for campaigns list
  // 7. Return campaign
}

export async function updateCampaignAction(
  campaignId: string,
  siteId: string,
  data: UpdateCampaignInput,
): Promise<ActionResult<Campaign>> {
  // 1. Verify auth + permission
  // 2. Verify campaign is editable (draft or scheduled)
  // 3. Validate input
  // 4. Update campaign record
  // 5. revalidatePath
  // 6. Return updated campaign
}

export async function sendCampaignAction(
  campaignId: string,
  siteId: string,
): Promise<ActionResult<{ status: string }>> {
  // 1. Verify auth + permission
  // 2. Validate campaign is ready:
  //    - Has subject_line
  //    - Has content_json with at least 1 block
  //    - Has audience_filter
  //    - Audience resolves to > 0 contacts
  // 3. Call sendCampaign() from sending engine
  // 4. Return status
}

export async function sendTestEmailAction(
  campaignId: string,
  siteId: string,
  testEmail: string,
): Promise<ActionResult<{ sent: boolean }>> {
  // 1. Verify auth
  // 2. Load campaign
  // 3. Render HTML with mock contact data
  // 4. Send via Resend to testEmail
  // 5. Return result
}

export async function duplicateCampaignAction(
  campaignId: string,
  siteId: string,
): Promise<ActionResult<Campaign>> {
  // 1. Verify auth
  // 2. Load source campaign
  // 3. Create copy with status 'draft', null scheduled_at
  // 4. Name: "Copy of {original_name}"
  // 5. Return new campaign
}

2.9 Webhook Handlers (Resend Events)

Location: src/app/api/webhooks/resend/route.ts

// Resend sends webhook events for:
// - email.delivered
// - email.opened
// - email.clicked
// - email.bounced
// - email.complained (spam report)
//
// Webhook URL: https://app.dramacagency.com/api/webhooks/resend
// Must verify webhook signature using Resend webhook secret
//
// NOTE: This route may already exist for transactional emails.
// If so, ADD marketing campaign handling to the existing handler.
// Check for X-Campaign-Id header to distinguish marketing vs transactional.

export async function POST(request: Request) {
  // 1. Verify Resend webhook signature
  const signature = request.headers.get("svix-signature");
  // ... verify with svix library or manual HMAC

  const event = await request.json();

  // 2. Check if this is a marketing email (has campaign headers)
  const campaignId = event.data?.headers?.["X-Campaign-Id"];
  if (!campaignId) {
    // Handle as transactional email event (existing logic)
    return handleTransactionalEvent(event);
  }

  // 3. Handle marketing email event
  const contactId = event.data?.headers?.["X-Contact-Id"];

  switch (event.type) {
    case "email.delivered":
      await updateEmailSendStatus(campaignId, contactId, "delivered");
      await incrementCampaignStat(campaignId, "delivered_count");
      break;

    case "email.opened":
      // Note: may fire multiple times per contact
      await recordEmailOpen(campaignId, contactId);
      break;

    case "email.clicked":
      await recordEmailClick(campaignId, contactId, event.data.click.link);
      break;

    case "email.bounced":
      await handleBounce(campaignId, contactId, event.data.bounce);
      // Mark contact as bounced in CRM
      await markContactBounced(contactId);
      break;

    case "email.complained":
      await handleComplaint(campaignId, contactId);
      // Auto-unsubscribe contact
      await unsubscribeContact(contactId);
      // Fire automation event
      await logAutomationEvent({
        event_type: "marketing.contact.complained",
        metadata: { campaign_id: campaignId, contact_id: contactId },
      });
      break;
  }

  return new Response("OK", { status: 200 });
}

2.10 Unsubscribe Handling

Location: src/app/api/unsubscribe/[token]/route.ts

// Public route — no auth required
// Token is a signed JWT containing: { contactId, campaignId }
//
// GET  /api/unsubscribe/[token] → renders unsubscribe confirmation page
// POST /api/unsubscribe/[token] → processes unsubscribe
//
// On unsubscribe:
// 1. Verify token signature (prevent abuse)
// 2. Set contact.email_subscribed = false
// 3. Set contact.email_unsubscribed_at = now()
// 4. Log to mod_mktmod01_unsubscribes table
// 5. Fire automation event: marketing.contact.unsubscribed
// 6. Show confirmation page with optional feedback form
//    (Why did you unsubscribe? Too many emails / Not relevant / Other)
//
// ALSO create a storefront-facing unsubscribe page:
// Location: src/app/unsubscribe/[token]/page.tsx
// This renders a branded page (loads site theme) with:
// - "You've been unsubscribed" message
// - Optional "Resubscribe" button
// - Feedback form

2.11 Campaign Scheduling

// scheduling.ts
// Two approaches (implement the simpler one first):
//
// APPROACH 1 (Recommended): Vercel Cron Job
// Add to vercel.json crons array:
// { "path": "/api/cron/marketing-scheduler", "schedule": "*/5 * * * *" }
//
// The cron job checks every 5 minutes for campaigns where:
// - status = 'scheduled'
// - scheduled_at <= now()
// Then triggers sendCampaign() for each
//
// APPROACH 2: Supabase pg_cron (if cron already used in platform)
// Create a PostgreSQL function that checks scheduled campaigns
//
// Implementation:
export async function checkScheduledCampaigns() {
  const supabase = createServiceClient();

  const { data: readyCampaigns } = await supabase
    .from("mod_mktmod01_campaigns")
    .select("id, site_id")
    .eq("status", "scheduled")
    .lte("scheduled_at", new Date().toISOString());

  for (const campaign of readyCampaigns ?? []) {
    try {
      await sendCampaign(campaign.id, campaign.site_id);
    } catch (error) {
      console.error(`Failed to send scheduled campaign ${campaign.id}:`, error);
      // Update status to 'failed' with error
      await updateCampaignStatus(supabase, campaign.id, "failed");
    }
  }
}

2.12 Phase MKT-02 Deliverables Checklist

- [ ] Campaign list page with filters and search
- [ ] Campaign creation wizard (4 steps)
- [ ] Campaign edit page (status-aware)
- [ ] Visual email editor with all 12 block types
- [ ] Block properties panels for each block type
- [ ] Drag-and-drop reordering
- [ ] Undo/redo functionality
- [ ] Desktop/mobile preview toggle
- [ ] HTML email renderer (email-client compatible)
- [ ] 15 pre-built email templates
- [ ] Custom template save/load
- [ ] Audience resolver (all, segment, tags, custom)
- [ ] Personalization engine with token replacement
- [ ] Batch sending engine with throttling
- [ ] A/B testing framework (subject line variants)
- [ ] All campaign API routes (CRUD + send/pause/resume/test/duplicate)
- [ ] All server actions
- [ ] Resend webhook handler (delivered/opened/clicked/bounced/complained)
- [ ] Unsubscribe flow (token-based, branded page, feedback)
- [ ] Campaign scheduling via Vercel cron
- [ ] Test email functionality
- [ ] Automation events fired: campaign.created, campaign.sending, campaign.sent
- [ ] Permission checks on all routes
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-03: Email Analytics & Tracking

Priority: CRITICAL — Provides ROI visibility for all campaigns Estimated Complexity: Medium-High Dependencies: Phase MKT-02 complete (campaigns can be sent) Files Created: ~15 new files Files Modified: 1-2 existing files

3.1 Overview

This phase builds the complete analytics and tracking system for email campaigns. It includes:

  • Open tracking via invisible pixel
  • Click tracking via redirect proxy
  • Real-time campaign dashboard with live stats
  • Individual campaign report pages
  • Contact-level engagement history
  • Aggregate analytics across all campaigns
  • Exportable reports (CSV)
  • Engagement scoring that feeds back into CRM

3.2 Open Tracking

Location: src/app/api/tracking/open/[token]/route.ts

// Public route — no auth required
// Returns a 1x1 transparent GIF pixel
// Token is a signed, compact identifier: base64(campaignId:contactId:sendId)
//
// When email client loads this image, it registers as an "open"
//
// IMPORTANT: Open tracking is inherently unreliable:
// - Some clients block images (Apple Mail Privacy Protection, etc.)
// - Some clients pre-fetch images (inflating opens)
// - Use as directional signal, not absolute truth
//
// Implementation:

export async function GET(
  request: Request,
  { params }: { params: { token: string } },
) {
  try {
    const { campaignId, contactId, sendId } = decodeTrackingToken(params.token);

    // Record open asynchronously (don't block pixel response)
    // Use waitUntil if available (Vercel Edge), otherwise fire-and-forget
    const recordPromise = recordOpen(campaignId, contactId, sendId, {
      userAgent: request.headers.get("user-agent"),
      ip: request.headers.get("x-forwarded-for"),
      timestamp: new Date(),
    });

    // Edge runtime: use waitUntil
    // Node runtime: fire and forget (or use after() from next/server)
    if (typeof globalThis.waitUntil === "function") {
      globalThis.waitUntil(recordPromise);
    }
  } catch (e) {
    // Silently fail — never break pixel response
  }

  // Return 1x1 transparent GIF
  const pixel = Buffer.from(
    "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
    "base64",
  );

  return new Response(pixel, {
    headers: {
      "Content-Type": "image/gif",
      "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
      Pragma: "no-cache",
      Expires: "0",
    },
  });
}

async function recordOpen(
  campaignId: string,
  contactId: string,
  sendId: string,
  metadata: OpenMetadata,
) {
  const supabase = createServiceClient();

  // Check if first open (for unique open tracking)
  const { data: existingSend } = await supabase
    .from("mod_mktmod01_email_sends")
    .select("id, opened_at")
    .eq("campaign_id", campaignId)
    .eq("contact_id", contactId)
    .single();

  if (!existingSend) return;

  // Record open event
  await supabase.from("mod_mktmod01_email_events").insert({
    campaign_id: campaignId,
    contact_id: contactId,
    send_id: existingSend.id,
    event_type: "open",
    metadata: {
      user_agent: metadata.userAgent,
      ip: metadata.ip,
    },
  });

  // Update first open time if not already set (unique open)
  if (!existingSend.opened_at) {
    await supabase
      .from("mod_mktmod01_email_sends")
      .update({ opened_at: metadata.timestamp.toISOString() })
      .eq("id", existingSend.id);

    // Increment unique open count in campaign stats
    await supabase.rpc("increment_campaign_stat", {
      p_campaign_id: campaignId,
      p_stat: "unique_opens",
    });

    // Fire automation event for first open
    await logAutomationEvent({
      event_type: "marketing.email.opened",
      metadata: { campaign_id: campaignId, contact_id: contactId },
    });
  }

  // Always increment total opens
  await supabase.rpc("increment_campaign_stat", {
    p_campaign_id: campaignId,
    p_stat: "total_opens",
  });
}

Database Function for Atomic Increment

-- Migration: increment_campaign_stat RPC function
-- This ensures atomic counter updates even under concurrent webhook load

CREATE OR REPLACE FUNCTION increment_campaign_stat(
  p_campaign_id UUID,
  p_stat TEXT
) RETURNS VOID AS $$
BEGIN
  -- Dynamic column update based on stat name
  -- Only allow known stat columns to prevent SQL injection
  IF p_stat NOT IN ('unique_opens', 'total_opens', 'unique_clicks', 'total_clicks',
                     'bounces', 'complaints', 'unsubscribes', 'delivered_count') THEN
    RAISE EXCEPTION 'Invalid stat column: %', p_stat;
  END IF;

  EXECUTE format(
    'UPDATE mod_mktmod01_campaign_stats SET %I = COALESCE(%I, 0) + 1 WHERE campaign_id = $1',
    p_stat, p_stat
  ) USING p_campaign_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

3.3 Click Tracking

Location: src/app/api/tracking/click/[token]/route.ts

// Public route — no auth required
// Redirects to the original URL after recording the click
// Token: base64(campaignId:contactId:sendId:encodedUrl)
//
// Security considerations:
// - Validate destination URL (no javascript:, data:, or internal routes)
// - Rate limit to prevent abuse
// - Log but don't block on failures

export async function GET(
  request: Request,
  { params }: { params: { token: string } },
) {
  try {
    const { campaignId, contactId, sendId, originalUrl } = decodeClickToken(
      params.token,
    );

    // Validate URL is safe to redirect to
    if (!isValidRedirectUrl(originalUrl)) {
      return new Response("Invalid URL", { status: 400 });
    }

    // Record click asynchronously
    const recordPromise = recordClick(
      campaignId,
      contactId,
      sendId,
      originalUrl,
      {
        userAgent: request.headers.get("user-agent"),
        ip: request.headers.get("x-forwarded-for"),
        referer: request.headers.get("referer"),
        timestamp: new Date(),
      },
    );

    if (typeof globalThis.waitUntil === "function") {
      globalThis.waitUntil(recordPromise);
    }

    // Redirect to original URL
    return Response.redirect(originalUrl, 302);
  } catch (e) {
    // On any error, try to redirect to site homepage or return error
    return new Response("Tracking error", { status: 500 });
  }
}

function isValidRedirectUrl(url: string): boolean {
  try {
    const parsed = new URL(url);
    // Block dangerous protocols
    if (!["http:", "https:"].includes(parsed.protocol)) return false;
    // Block internal/localhost URLs
    if (["localhost", "127.0.0.1", "0.0.0.0"].includes(parsed.hostname))
      return false;
    return true;
  } catch {
    return false;
  }
}

async function recordClick(
  campaignId: string,
  contactId: string,
  sendId: string,
  url: string,
  metadata: ClickMetadata,
) {
  const supabase = createServiceClient();

  // Record click event
  await supabase.from("mod_mktmod01_email_events").insert({
    campaign_id: campaignId,
    contact_id: contactId,
    send_id: sendId,
    event_type: "click",
    metadata: {
      url,
      user_agent: metadata.userAgent,
      ip: metadata.ip,
    },
  });

  // Update link stats
  await supabase.from("mod_mktmod01_link_tracking").upsert(
    {
      campaign_id: campaignId,
      url: url,
      total_clicks: 1,
      unique_clicks: 1,
    },
    {
      onConflict: "campaign_id,url",
      // On conflict, increment counters
    },
  );
  // Note: Supabase upsert doesn't support increment on conflict directly
  // Use RPC or do read-then-update for link tracking
  // Or use a separate increment_link_stat RPC function

  // Check if first click by this contact (unique click)
  const { data: existingSend } = await supabase
    .from("mod_mktmod01_email_sends")
    .select("clicked_at")
    .eq("campaign_id", campaignId)
    .eq("contact_id", contactId)
    .single();

  if (existingSend && !existingSend.clicked_at) {
    // First click — update send record and increment unique clicks
    await supabase
      .from("mod_mktmod01_email_sends")
      .update({ clicked_at: metadata.timestamp.toISOString() })
      .eq("campaign_id", campaignId)
      .eq("contact_id", contactId);

    await supabase.rpc("increment_campaign_stat", {
      p_campaign_id: campaignId,
      p_stat: "unique_clicks",
    });

    // Fire automation event for first click
    await logAutomationEvent({
      event_type: "marketing.email.clicked",
      metadata: { campaign_id: campaignId, contact_id: contactId, url },
    });
  }

  // Always increment total clicks
  await supabase.rpc("increment_campaign_stat", {
    p_campaign_id: campaignId,
    p_stat: "total_clicks",
  });
}

3.4 Campaign Report Dashboard

Location: src/app/dashboard/sites/[siteId]/marketing/campaigns/[campaignId]/report/page.tsx

Route: /dashboard/sites/[siteId]/marketing/campaigns/[campaignId]/report

Report Layout

┌─────────────────────────────────────────────────────────────┐
│ Campaign: "Summer Sale Announcement"                         │
│ Sent: June 15, 2026 at 2:00 PM · Audience: 2,450 contacts  │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │ Delivered │  │  Opened  │  │ Clicked  │  │ Bounced  │   │
│  │  2,438   │  │  1,205   │  │   342    │  │    12    │   │
│  │  99.5%   │  │  49.4%   │  │  14.0%   │  │   0.5%   │   │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘   │
│                                                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                  │
│  │ Unsubs   │  │Complaints│  │ Failed   │                  │
│  │    8     │  │    1     │  │    0     │                  │
│  │   0.3%   │  │   0.0%   │  │   0.0%   │                  │
│  └──────────┘  └──────────┘  └──────────┘                  │
│                                                              │
│  [Opens Over Time - Line Chart]                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │         ╱╲                                           │   │
│  │        ╱  ╲         ╱╲                               │   │
│  │       ╱    ╲       ╱  ╲                              │   │
│  │      ╱      ╲     ╱    ╲____                         │   │
│  │─────╱        ╲___╱          ╲_______                 │   │
│  │  Day 1  Day 2  Day 3  Day 4  Day 5  Day 6  Day 7   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  [Top Clicked Links]                                        │
│  ┌─────────────────────────────┬────────┬───────────┐      │
│  │ URL                         │ Clicks │ Unique    │      │
│  ├─────────────────────────────┼────────┼───────────┤      │
│  │ example.com/sale            │   203  │    178    │      │
│  │ example.com/products/new    │    89  │     74    │      │
│  │ example.com/about           │    50  │     42    │      │
│  └─────────────────────────────┴────────┴───────────┘      │
│                                                              │
│  [A/B Test Results] (if applicable)                         │
│  ┌───────────────────────────────────────────────┐         │
│  │ Variant A: "Summer Sale - 50% Off!"           │         │
│  │   Opens: 52.1%  Clicks: 15.3%  ★ WINNER      │         │
│  │                                                │         │
│  │ Variant B: "Don't Miss Our Biggest Sale"      │         │
│  │   Opens: 46.8%  Clicks: 12.7%                 │         │
│  └───────────────────────────────────────────────┘         │
│                                                              │
│  [Recipient Activity Table]                                 │
│  ┌──────────────────┬──────────┬────────┬─────────┐        │
│  │ Contact          │ Opened   │ Clicked│ Status  │        │
│  ├──────────────────┼──────────┼────────┼─────────┤        │
│  │ john@example.com │ Yes (2x) │ Yes    │ Active  │        │
│  │ jane@example.com │ Yes (1x) │ No     │ Active  │        │
│  │ bob@example.com  │ No       │ No     │ Bounced │        │
│  └──────────────────┴──────────┴────────┴─────────┘        │
│  [Show More] [Export CSV]                                   │
│                                                              │
│  [Actions] [Duplicate Campaign] [Export Report]             │
└─────────────────────────────────────────────────────────────┘

Component Structure

report/
├── page.tsx                    — Server component, data fetching
├── CampaignReportHeader.tsx    — Campaign info + status
├── StatCards.tsx                — KPI cards row (delivered, opens, clicks, etc.)
├── OpensTimeChart.tsx           — Line chart of opens over time
├── ClicksTable.tsx              — Top clicked links table
├── ABTestResults.tsx            — A/B test comparison (conditional)
├── RecipientTable.tsx           — Paginated recipient activity
├── DeviceBreakdown.tsx          — Pie chart: desktop vs mobile (from user-agent)
└── ExportButton.tsx             — CSV export functionality

Charts Library

Use Recharts (already likely in project) or shadcn charts.
Check existing dependencies first — use whatever charting
library the platform already uses. Do NOT add a new one.

3.5 Marketing Analytics Overview

Location: src/app/dashboard/sites/[siteId]/marketing/analytics/page.tsx

Route: /dashboard/sites/[siteId]/marketing/analytics

This is the aggregate view across ALL campaigns for a site.

Key Metrics

// Metrics to display:
// - Total emails sent (all time)
// - Average open rate (across all campaigns)
// - Average click rate
// - Total subscribers
// - Subscriber growth (chart: new subs per week/month)
// - Unsubscribe rate
// - Bounce rate
// - Best performing campaign (highest open rate)
// - Best performing day of week (when opens are highest)
// - Best performing hour of day
//
// Date range filter: Last 7 days | 30 days | 90 days | Year | Custom
// Compare to previous period toggle

interface MarketingAnalytics {
  totalSent: number;
  totalDelivered: number;
  averageOpenRate: number;
  averageClickRate: number;
  totalSubscribers: number;
  newSubscribers: number; // in date range
  totalUnsubscribes: number;
  bounceRate: number;
  complaintRate: number;
  campaigns: {
    total: number;
    sent: number;
    draft: number;
    scheduled: number;
  };
  topCampaigns: Array<{
    id: string;
    name: string;
    openRate: number;
    clickRate: number;
    sentAt: string;
  }>;
  engagementOverTime: Array<{
    date: string;
    sent: number;
    opens: number;
    clicks: number;
  }>;
}

Data Fetching

// analytics-queries.ts
// Aggregate queries for the analytics dashboard.
// These should be efficient — consider creating database views
// or materialized summaries if queries become slow.

export async function getMarketingAnalytics(
  supabase: SupabaseClient,
  siteId: string,
  dateRange: { from: Date; to: Date },
): Promise<MarketingAnalytics> {
  // Parallel queries for efficiency
  const [
    campaignStats,
    subscriberCount,
    subscriberGrowth,
    topCampaigns,
    engagementTimeline,
  ] = await Promise.all([
    getAggregateCampaignStats(supabase, siteId, dateRange),
    getSubscriberCount(supabase, siteId),
    getSubscriberGrowth(supabase, siteId, dateRange),
    getTopCampaigns(supabase, siteId, dateRange),
    getEngagementTimeline(supabase, siteId, dateRange),
  ]);

  return {
    ...campaignStats,
    totalSubscribers: subscriberCount,
    newSubscribers: subscriberGrowth,
    topCampaigns,
    engagementOverTime: engagementTimeline,
  };
}

3.6 Contact Engagement Profile

Enhancement to CRM contact detail page.

Location: Add tab/section to existing contact detail page

// When viewing a CRM contact, add a "Marketing" tab showing:
// - Email subscription status (subscribed/unsubscribed with date)
// - Engagement score (calculated from opens/clicks across campaigns)
// - Campaigns received: list with open/click status for each
// - Last email opened date
// - Last email clicked date
// - Total emails received, opened, clicked
// - Click-through rate for this contact
//
// This helps sales/support see contact engagement at a glance.
//
// Data query:
const { data: contactEngagement } = await supabase
  .from("mod_mktmod01_email_sends")
  .select(
    `
    *,
    mod_mktmod01_campaigns (name, subject_line, sent_at)
  `,
  )
  .eq("contact_id", contactId)
  .order("sent_at", { ascending: false })
  .limit(50);

3.7 Engagement Scoring

// engagement-scoring.ts
// Calculate an engagement score for each contact based on their
// interaction with marketing emails. This score feeds into CRM
// and can be used for audience segmentation.

// Score calculation (out of 100):
// - Base: 0
// - Each email opened in last 30 days: +5 (max 25)
// - Each link clicked in last 30 days: +10 (max 30)
// - Opened last campaign: +15
// - Clicked last campaign: +20
// - No opens in 90 days: -30
// - Bounced: -50
// - Complained: score = 0 (permanently cold)
//
// Engagement levels:
// 0-19:   Cold (gray)
// 20-39:  Cool (blue)
// 40-59:  Warm (yellow)
// 60-79:  Hot (orange)
// 80-100: On Fire (red)
//
// Score is stored on mod_crm01_contacts.marketing_engagement_score
// Updated after each campaign send completes and after each open/click event
// Can also be recalculated in bulk via admin action

export async function calculateEngagementScore(
  supabase: SupabaseClient,
  contactId: string,
): Promise<number> {
  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
  const ninetyDaysAgo = new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);

  // Get recent email interactions
  const { data: recentSends } = await supabase
    .from("mod_mktmod01_email_sends")
    .select("opened_at, clicked_at, status, sent_at")
    .eq("contact_id", contactId)
    .gte("sent_at", ninetyDaysAgo.toISOString())
    .order("sent_at", { ascending: false });

  if (!recentSends || recentSends.length === 0) return 0;

  let score = 0;

  // Recent opens (last 30 days)
  const recentOpens = recentSends.filter(
    (s) => s.opened_at && new Date(s.opened_at) >= thirtyDaysAgo,
  );
  score += Math.min(recentOpens.length * 5, 25);

  // Recent clicks (last 30 days)
  const recentClicks = recentSends.filter(
    (s) => s.clicked_at && new Date(s.clicked_at) >= thirtyDaysAgo,
  );
  score += Math.min(recentClicks.length * 10, 30);

  // Last campaign engagement
  const lastSend = recentSends[0];
  if (lastSend?.opened_at) score += 15;
  if (lastSend?.clicked_at) score += 20;

  // Inactivity penalty
  const hasRecentOpen = recentSends.some((s) => s.opened_at);
  if (!hasRecentOpen) score -= 30;

  // Bounce/complaint check
  if (recentSends.some((s) => s.status === "bounced")) score -= 50;
  if (recentSends.some((s) => s.status === "complained")) return 0;

  return Math.max(0, Math.min(100, score));
}

3.8 CSV Export

// export-utils.ts
// Export campaign report data as CSV

export async function exportCampaignReport(
  supabase: SupabaseClient,
  campaignId: string,
): Promise<string> {
  const { data: sends } = await supabase
    .from("mod_mktmod01_email_sends")
    .select(
      `
      email,
      status,
      sent_at,
      delivered_at,
      opened_at,
      clicked_at,
      bounced_at,
      variant,
      mod_crm01_contacts (first_name, last_name, company)
    `,
    )
    .eq("campaign_id", campaignId);

  // Generate CSV with columns:
  // Email, First Name, Last Name, Company, Status, Sent At,
  // Delivered At, Opened At, Clicked At, Bounced At, Variant

  const headers = [
    "Email",
    "First Name",
    "Last Name",
    "Company",
    "Status",
    "Sent At",
    "Delivered At",
    "Opened At",
    "Clicked At",
    "Bounced At",
    "Variant",
  ];

  const rows = (sends ?? []).map((s) => [
    s.email,
    s.mod_crm01_contacts?.first_name ?? "",
    s.mod_crm01_contacts?.last_name ?? "",
    s.mod_crm01_contacts?.company ?? "",
    s.status,
    s.sent_at ?? "",
    s.delivered_at ?? "",
    s.opened_at ?? "",
    s.clicked_at ?? "",
    s.bounced_at ?? "",
    s.variant ?? "",
  ]);

  return [
    headers.join(","),
    ...rows.map((r) => r.map(escapeCsv).join(",")),
  ].join("\n");
}

function escapeCsv(value: string): string {
  if (value.includes(",") || value.includes('"') || value.includes("\n")) {
    return `"${value.replace(/"/g, '""')}"`;
  }
  return value;
}

3.9 Phase MKT-03 Deliverables Checklist

- [ ] Open tracking pixel API route
- [ ] Click tracking redirect API route
- [ ] URL validation for click redirects (security)
- [ ] increment_campaign_stat RPC function (database migration)
- [ ] Campaign report page with all sections
- [ ] Stat cards component (delivered, opens, clicks, bounces, unsubs, complaints)
- [ ] Opens-over-time line chart
- [ ] Top clicked links table
- [ ] A/B test results comparison panel
- [ ] Recipient activity table with pagination
- [ ] Device breakdown chart (desktop vs mobile)
- [ ] Marketing analytics overview page (aggregate metrics)
- [ ] Date range filter for analytics
- [ ] Contact engagement profile (CRM tab/section enhancement)
- [ ] Engagement scoring algorithm
- [ ] Score stored on CRM contact record
- [ ] CSV export for campaign reports
- [ ] Automation events: marketing.email.opened, marketing.email.clicked
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-04: Drip Sequences & Marketing Automation Templates

Priority: HIGH — Enables automated marketing workflows Estimated Complexity: High Dependencies: Phase MKT-02 (campaigns), Phase MKT-03 (tracking) Files Created: ~20 new files Files Modified: 3-5 existing automation files

4.1 Overview

This phase bridges the marketing module with the existing automation engine. It creates:

  • Drip sequence builder (multi-email automated sequences)
  • Pre-built marketing automation workflow templates
  • New marketing-specific automation triggers and actions
  • Visual sequence timeline editor
  • Sequence performance tracking
  • Smart timing (send time optimization)

Critical Integration Point: This phase does NOT rebuild the automation engine. It EXTENDS it with marketing-specific triggers, actions, and pre-built templates. All drip sequences run ON TOP of the existing automation workflow system.

4.2 Drip Sequence Architecture

A drip sequence is a series of automated emails sent to contacts
based on a trigger event, with configurable delays between steps.

Example: "Welcome Onboarding Sequence"
  Trigger: contact.created (new CRM contact)
  ├── Day 0: Welcome email (immediate)
  ├── Day 2: "Getting Started" guide
  ├── Day 5: Feature highlight
  ├── Day 10: "How's it going?" check-in
  └── Day 14: Upgrade offer / CTA

Each step can have:
- Delay (hours/days after previous step or trigger)
- Email content (campaign-like content with template)
- Conditions (only send if contact opened previous email, etc.)
- Branch logic (if opened → path A, if not → path B)
- Exit conditions (stop sequence if contact converts, unsubscribes, etc.)

4.3 Drip Sequence Data Model

Uses existing tables: mod_mktmod01_sequences + mod_mktmod01_sequence_steps + mod_mktmod01_sequence_enrollments (created in Phase MKT-01)

// Sequence step types and their configuration:

interface SequenceStep {
  id: string;
  sequence_id: string;
  step_order: number;
  step_type: "email" | "delay" | "condition" | "action";
  config:
    | EmailStepConfig
    | DelayStepConfig
    | ConditionStepConfig
    | ActionStepConfig;
}

interface EmailStepConfig {
  subject_line: string;
  preview_text?: string;
  content_json: EmailContent; // Same format as campaigns
  from_name?: string; // Override default
  from_email?: string; // Override default
}

interface DelayStepConfig {
  delay_amount: number;
  delay_unit: "minutes" | "hours" | "days" | "weeks";
  // Smart timing: if true, sends at optimal time for contact's timezone
  smart_timing: boolean;
}

interface ConditionStepConfig {
  condition_type:
    | "opened_previous"
    | "clicked_previous"
    | "tag_has"
    | "tag_missing"
    | "field_equals"
    | "engagement_score";
  condition_value?: string;
  // Branch: contacts meeting condition continue, others skip to next matching step
  true_next_step?: number; // step_order to jump to
  false_next_step?: number; // step_order if condition fails
}

interface ActionStepConfig {
  action_type:
    | "add_tag"
    | "remove_tag"
    | "update_field"
    | "move_to_segment"
    | "notify_team"
    | "webhook";
  action_params: Record<string, unknown>;
}

4.4 Drip Sequence Builder UI

Location: src/app/dashboard/sites/[siteId]/marketing/sequences/

Sequence List Page

Route: /dashboard/sites/[siteId]/marketing/sequences
Features:
- List all sequences with: name, status (draft/active/paused),
  enrolled contacts, completion rate, created date
- Status filter tabs: All | Active | Draft | Paused
- Create new sequence button
- Duplicate sequence
- View sequence performance

Sequence Builder Page

Route: /dashboard/sites/[siteId]/marketing/sequences/new
Route: /dashboard/sites/[siteId]/marketing/sequences/[sequenceId]/edit
Visual vertical timeline builder:

┌─────────────────────────────────────────────┐
│ Sequence: Welcome Onboarding               │
│ Trigger: [Contact Created ▼]                │
│ Audience: [All New Contacts ▼]              │
├─────────────────────────────────────────────┤
│                                             │
│  ⬇ TRIGGER: Contact Created                │
│  │                                          │
│  ⬇ 📧 EMAIL: Welcome! (immediate)          │
│  │   Subject: "Welcome to {{site_name}}"    │
│  │   [Edit Email] [Preview]                 │
│  │                                          │
│  ⬇ ⏱ WAIT: 2 days                         │
│  │                                          │
│  ⬇ ❓ CONDITION: Opened previous email?    │
│  │   ├─ YES ──→                             │
│  │   │  ⬇ 📧 EMAIL: Getting Started Guide  │
│  │   │                                      │
│  │   └─ NO ───→                             │
│  │      ⬇ 📧 EMAIL: Did you see our email? │
│  │                                          │
│  ⬇ ⏱ WAIT: 3 days                         │
│  │                                          │
│  ⬇ 📧 EMAIL: Feature Spotlight             │
│  │                                          │
│  ⬇ 🏁 SEQUENCE COMPLETE                    │
│                                             │
│  [+ Add Step]                               │
│                                             │
│  Exit Conditions:                           │
│  ☑ Contact unsubscribes                     │
│  ☑ Contact makes a purchase                 │
│  ☐ Contact replies to an email              │
│                                             │
│  [Save Draft] [Activate Sequence]           │
└─────────────────────────────────────────────┘

Component Structure

sequences/
├── page.tsx                     — Sequence list
├── new/page.tsx                 — Create (renders builder)
├── [sequenceId]/
│   ├── page.tsx                 — View/stats
│   └── edit/page.tsx            — Edit (renders builder)
└── components/
    ├── SequenceBuilder.tsx       — Main builder component
    ├── SequenceTimeline.tsx      — Visual timeline renderer
    ├── StepCard.tsx              — Individual step card
    ├── EmailStepEditor.tsx       — Email step config (opens email editor)
    ├── DelayStepEditor.tsx       — Delay config (amount + unit picker)
    ├── ConditionStepEditor.tsx   — Condition config with branching
    ├── ActionStepEditor.tsx      — Action config (tag, field, etc.)
    ├── TriggerSelector.tsx       — Dropdown of available triggers
    ├── ExitConditions.tsx        — Exit condition checkboxes
    └── SequenceStats.tsx         — Per-step completion funnel

4.5 Sequence Execution Engine

// sequence-engine.ts
// Processes drip sequences by checking enrolled contacts
// against their current step and advancing them when delays complete.
//
// Execution approach: Cron-based step processor
// Runs every 5 minutes via Vercel cron (or same cron as campaign scheduler)
// Checks all active enrollments where next_step_at <= now()

export async function processSequenceSteps() {
  const supabase = createServiceClient();

  // Find enrollments ready for next step
  const { data: readyEnrollments } = await supabase
    .from("mod_mktmod01_sequence_enrollments")
    .select(
      `
      *,
      mod_mktmod01_sequences (*),
      mod_mktmod01_sequence_steps (*)
    `,
    )
    .eq("status", "active")
    .lte("next_step_at", new Date().toISOString())
    .limit(100); // Process in batches

  for (const enrollment of readyEnrollments ?? []) {
    try {
      await processEnrollmentStep(supabase, enrollment);
    } catch (error) {
      console.error(`Failed to process enrollment ${enrollment.id}:`, error);
      // Don't stop processing other enrollments
    }
  }
}

async function processEnrollmentStep(
  supabase: SupabaseClient,
  enrollment: SequenceEnrollment,
) {
  const steps = enrollment.mod_mktmod01_sequence_steps.sort(
    (a, b) => a.step_order - b.step_order,
  );

  const currentStep = steps.find(
    (s) => s.step_order === enrollment.current_step,
  );
  if (!currentStep) {
    // No more steps — complete the enrollment
    await completeEnrollment(supabase, enrollment.id);
    return;
  }

  // Check exit conditions first
  if (await shouldExitSequence(supabase, enrollment)) {
    await exitEnrollment(supabase, enrollment.id, "exit_condition_met");
    return;
  }

  switch (currentStep.step_type) {
    case "email":
      await sendSequenceEmail(supabase, enrollment, currentStep);
      break;
    case "delay":
      // Delay step — just advance (the delay was already waited)
      break;
    case "condition":
      await evaluateCondition(supabase, enrollment, currentStep);
      return; // evaluateCondition handles its own step advancement
    case "action":
      await executeAction(supabase, enrollment, currentStep);
      break;
  }

  // Advance to next step
  const nextStep = steps.find(
    (s) => s.step_order === enrollment.current_step + 1,
  );
  if (nextStep) {
    const nextStepAt = calculateNextStepTime(nextStep);
    await supabase
      .from("mod_mktmod01_sequence_enrollments")
      .update({
        current_step: nextStep.step_order,
        next_step_at: nextStepAt.toISOString(),
        last_step_at: new Date().toISOString(),
      })
      .eq("id", enrollment.id);
  } else {
    await completeEnrollment(supabase, enrollment.id);
  }
}

function calculateNextStepTime(step: SequenceStep): Date {
  const now = new Date();
  if (step.step_type === "delay") {
    const config = step.config as DelayStepConfig;
    const ms = {
      minutes: 60_000,
      hours: 3_600_000,
      days: 86_400_000,
      weeks: 604_800_000,
    }[config.delay_unit];
    return new Date(now.getTime() + config.delay_amount * ms);
  }
  // Non-delay steps execute immediately
  return now;
}

4.6 Enrollment Management

// enrollment.ts
// Enrolls contacts into sequences based on trigger events

// Enrollment triggers (integrated with automation engine):
// 1. contact.created → enroll in welcome sequence
// 2. marketing.contact.subscribed → enroll in subscriber sequence
// 3. ecommerce.order.completed → enroll in post-purchase sequence
// 4. ecommerce.cart.abandoned → enroll in abandoned cart sequence
// 5. marketing.campaign.opened → enroll in follow-up sequence
// 6. Manual enrollment → user selects contacts to enroll
// 7. Segment membership → auto-enroll when contact enters segment

export async function enrollContact(
  supabase: SupabaseClient,
  sequenceId: string,
  contactId: string,
  siteId: string,
  triggerEvent?: string,
): Promise<void> {
  // 1. Check if already enrolled (prevent duplicates)
  const { data: existing } = await supabase
    .from("mod_mktmod01_sequence_enrollments")
    .select("id, status")
    .eq("sequence_id", sequenceId)
    .eq("contact_id", contactId)
    .in("status", ["active", "paused"]);

  if (existing && existing.length > 0) {
    return; // Already enrolled
  }

  // 2. Get first step
  const { data: firstStep } = await supabase
    .from("mod_mktmod01_sequence_steps")
    .select("step_order")
    .eq("sequence_id", sequenceId)
    .order("step_order", { ascending: true })
    .limit(1)
    .single();

  // 3. Create enrollment
  await supabase.from("mod_mktmod01_sequence_enrollments").insert({
    sequence_id: sequenceId,
    contact_id: contactId,
    site_id: siteId,
    status: "active",
    current_step: firstStep?.step_order ?? 1,
    next_step_at: new Date().toISOString(), // Process immediately
    enrolled_at: new Date().toISOString(),
    trigger_event: triggerEvent,
  });

  // 4. Fire automation event
  await logAutomationEvent({
    event_type: "marketing.sequence.enrolled",
    site_id: siteId,
    metadata: {
      sequence_id: sequenceId,
      contact_id: contactId,
      trigger: triggerEvent,
    },
  });
}

4.7 New Automation Triggers & Actions

Modifications to existing automation files:

New Event Types (add to event-types.ts)

// Add these to the EVENT_REGISTRY:

// Marketing Events:
"marketing.campaign.created"; // When a campaign is created
"marketing.campaign.scheduled"; // When a campaign is scheduled
"marketing.campaign.sending"; // When sending begins
"marketing.campaign.sent"; // When sending completes
"marketing.campaign.paused"; // When sending is paused
"marketing.email.opened"; // When a contact opens an email (first time)
"marketing.email.clicked"; // When a contact clicks a link (first time)
"marketing.email.bounced"; // When an email bounces
"marketing.contact.subscribed"; // When a contact subscribes
"marketing.contact.unsubscribed"; // When a contact unsubscribes
"marketing.contact.complained"; // When a contact marks as spam
"marketing.sequence.enrolled"; // When contact enters a sequence
"marketing.sequence.completed"; // When contact completes a sequence
"marketing.sequence.exited"; // When contact exits early
"marketing.form.submitted"; // When a marketing form is submitted
"marketing.landing.visited"; // When a landing page is visited
"marketing.landing.converted"; // When a landing page conversion happens
"marketing.engagement.cold"; // When engagement score drops to cold

New Action Types (add to action-types.ts)

// Add these to ACTION_TYPES:

"marketing.send_campaign"; // Send a specific campaign to trigger contact
"marketing.enroll_sequence"; // Enroll contact in a drip sequence
"marketing.remove_from_sequence"; // Remove contact from a sequence
"marketing.add_to_audience"; // Add contact to a campaign audience
"marketing.update_engagement"; // Recalculate engagement score
"marketing.subscribe"; // Set contact as subscribed
"marketing.unsubscribe"; // Set contact as unsubscribed
"marketing.send_sequence_email"; // Send a specific sequence step email

4.8 Pre-Built Marketing Automation Templates

Location: src/modules/marketing/data/automation-templates.ts

These are added to the existing automation template system (system-templates.ts pattern). Each template creates a complete workflow when selected.

export const MARKETING_AUTOMATION_TEMPLATES = [
  // 1. Welcome Sequence
  {
    id: "mkt-welcome-sequence",
    name: "Welcome Email Sequence",
    description: "Automated 5-email onboarding sequence for new contacts",
    category: "marketing",
    icon: "Sparkles",
    trigger: "contact.created",
    steps: [
      { type: "email", delay: 0, subject: "Welcome to {{site_name}}!" },
      { type: "delay", days: 2 },
      { type: "email", subject: "Getting Started Guide" },
      { type: "delay", days: 3 },
      { type: "condition", check: "opened_previous" },
      { type: "email", subject: "Top Features You Should Try" },
      { type: "delay", days: 5 },
      { type: "email", subject: "How Can We Help?" },
      { type: "delay", days: 4 },
      { type: "email", subject: "Special Offer Just for You" },
    ],
  },

  // 2. Abandoned Cart Recovery
  {
    id: "mkt-abandoned-cart",
    name: "Abandoned Cart Recovery",
    description: "3-email sequence to recover abandoned shopping carts",
    category: "marketing",
    trigger: "ecommerce.cart.abandoned",
    steps: [
      { type: "delay", hours: 1 },
      { type: "email", subject: "You left something behind!" },
      { type: "delay", days: 1 },
      { type: "condition", check: "converted", exitIfTrue: true },
      { type: "email", subject: "Your cart is waiting" },
      { type: "delay", days: 2 },
      { type: "condition", check: "converted", exitIfTrue: true },
      { type: "email", subject: "Last chance: 10% off your cart" },
    ],
  },

  // 3. Post-Purchase Follow-Up
  {
    id: "mkt-post-purchase",
    name: "Post-Purchase Follow-Up",
    description: "Thank customers, request reviews, suggest related products",
    category: "marketing",
    trigger: "ecommerce.order.completed",
    steps: [
      { type: "delay", days: 1 },
      { type: "email", subject: "Thank you for your order!" },
      { type: "delay", days: 7 },
      { type: "email", subject: "How was your experience?" },
      { type: "delay", days: 14 },
      { type: "email", subject: "You might also like..." },
    ],
  },

  // 4. Re-Engagement Campaign
  {
    id: "mkt-reengagement",
    name: "Re-Engagement / Win-Back",
    description: "Re-engage contacts who haven't opened emails in 60+ days",
    category: "marketing",
    trigger: "marketing.engagement.cold",
    steps: [
      { type: "email", subject: "We miss you, {{first_name}}!" },
      { type: "delay", days: 5 },
      { type: "condition", check: "opened_previous" },
      { type: "email", subject: "Here's what you've been missing" },
      { type: "delay", days: 7 },
      { type: "condition", check: "no_opens_after_sequence" },
      { type: "action", action: "add_tag", tag: "inactive" },
    ],
  },

  // 5. Lead Nurture
  {
    id: "mkt-lead-nurture",
    name: "Lead Nurture Sequence",
    description: "Educate and warm up leads over 3 weeks",
    category: "marketing",
    trigger: "marketing.form.submitted",
    steps: [
      { type: "email", subject: "Thanks for your interest!" },
      { type: "delay", days: 3 },
      { type: "email", subject: "Understanding {{site_name}}" },
      { type: "delay", days: 4 },
      { type: "email", subject: "Customer Success Story" },
      { type: "delay", days: 5 },
      { type: "email", subject: "Ready to Take the Next Step?" },
    ],
  },

  // 6. Birthday / Anniversary
  {
    id: "mkt-birthday",
    name: "Birthday / Anniversary Email",
    description: "Automated birthday or signup anniversary greeting",
    category: "marketing",
    trigger: "contact.anniversary", // Custom trigger: fires on contact created_at anniversary
    steps: [{ type: "email", subject: "Happy Birthday, {{first_name}}! 🎉" }],
  },

  // 7. Newsletter Subscriber Double Opt-In
  {
    id: "mkt-double-optin",
    name: "Double Opt-In Confirmation",
    description: "Confirm subscription and welcome new subscribers",
    category: "marketing",
    trigger: "marketing.contact.subscribed",
    steps: [
      { type: "email", subject: "Confirm your subscription" },
      { type: "delay", days: 1 },
      { type: "condition", check: "confirmed_subscription" },
      { type: "email", subject: "You're in! Welcome to our newsletter" },
    ],
  },

  // 8. Event Registration Follow-Up
  {
    id: "mkt-event-followup",
    name: "Event Registration Follow-Up",
    description: "Confirmation, reminders, and post-event follow-up",
    category: "marketing",
    trigger: "booking.confirmed",
    steps: [
      { type: "email", subject: "Your registration is confirmed!" },
      { type: "delay", days_before_event: 1 },
      { type: "email", subject: "Reminder: Event Tomorrow!" },
      { type: "delay", days_after_event: 1 },
      { type: "email", subject: "Thanks for attending!" },
    ],
  },

  // 9. Feedback Request
  {
    id: "mkt-feedback",
    name: "Feedback/NPS Survey",
    description:
      "Request feedback after interaction (service, purchase, support)",
    category: "marketing",
    trigger: "custom", // Manually triggered or linked to various events
    steps: [
      { type: "delay", days: 2 },
      { type: "email", subject: "We'd love your feedback" },
      { type: "delay", days: 5 },
      { type: "condition", check: "opened_previous", exitIfTrue: true },
      { type: "email", subject: "Quick reminder: share your thoughts" },
    ],
  },

  // 10. Upsell / Cross-Sell
  {
    id: "mkt-upsell",
    name: "Upsell / Cross-Sell",
    description:
      "Recommend upgrades or related products based on purchase history",
    category: "marketing",
    trigger: "ecommerce.order.completed",
    steps: [
      { type: "delay", days: 14 },
      { type: "email", subject: "Upgrade your experience" },
      { type: "delay", days: 7 },
      { type: "condition", check: "clicked_previous" },
      { type: "email", subject: "Exclusive offer for loyal customers" },
    ],
  },

  // 11. Subscription Renewal Reminder
  {
    id: "mkt-renewal-reminder",
    name: "Subscription Renewal Reminder",
    description: "Remind customers before subscription expires",
    category: "marketing",
    trigger: "ecommerce.subscription.expiring",
    steps: [
      { type: "email", subject: "Your subscription expires in 30 days" },
      { type: "delay", days: 15 },
      { type: "email", subject: "Don't lose access — renew now" },
      { type: "delay", days: 12 },
      { type: "email", subject: "Last chance to renew!" },
    ],
  },

  // 12. Course/Content Drip
  {
    id: "mkt-content-drip",
    name: "Content / Course Drip",
    description:
      "Deliver educational content over time (lessons, tips, guides)",
    category: "marketing",
    trigger: "marketing.form.submitted",
    steps: [
      { type: "email", subject: "Lesson 1: Getting Started" },
      { type: "delay", days: 3 },
      { type: "email", subject: "Lesson 2: Intermediate Concepts" },
      { type: "delay", days: 3 },
      { type: "email", subject: "Lesson 3: Advanced Techniques" },
      { type: "delay", days: 3 },
      { type: "email", subject: "Lesson 4: Putting It All Together" },
      { type: "delay", days: 3 },
      { type: "email", subject: "Course Complete — What's Next?" },
    ],
  },

  // 13. Referral Program
  {
    id: "mkt-referral",
    name: "Referral Program Invitation",
    description: "Invite happy customers to refer friends",
    category: "marketing",
    trigger: "ecommerce.order.completed",
    steps: [
      { type: "delay", days: 21 },
      { type: "condition", check: "engagement_score_above", value: 60 },
      { type: "email", subject: "Share the love — refer a friend!" },
    ],
  },

  // 14. Support Ticket Follow-Up
  {
    id: "mkt-support-followup",
    name: "Support Ticket Satisfaction",
    description: "Follow up after support ticket resolution",
    category: "marketing",
    trigger: "support.ticket.resolved",
    steps: [
      { type: "delay", days: 1 },
      { type: "email", subject: "Was your issue resolved?" },
    ],
  },

  // 15. Seasonal Campaign Planner
  {
    id: "mkt-seasonal",
    name: "Seasonal / Holiday Campaign",
    description: "Template for seasonal marketing pushes",
    category: "marketing",
    trigger: "manual",
    steps: [
      { type: "email", subject: "Our Holiday Sale is Here!" },
      { type: "delay", days: 3 },
      { type: "email", subject: "Sale Extended — Last Chance!" },
      { type: "delay", days: 2 },
      { type: "email", subject: "Final Hours: Don't Miss Out" },
    ],
  },
];

4.9 Sequence Performance Tracking

Route: /dashboard/sites/[siteId]/marketing/sequences/[sequenceId]
Sequence Performance View:

┌──────────────────────────────────────────────────────────┐
│ Sequence: Welcome Onboarding                              │
│ Status: Active · Enrolled: 324 · Completed: 198 · 61%    │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  Step Funnel:                                             │
│  ┌─────────────────────────────────────────────────┐     │
│  │ Step 1: Welcome Email          324 sent  89% open│     │
│  │ ████████████████████████████████████████████████ │     │
│  │                                                  │     │
│  │ Step 2: Getting Started        312 sent  72% open│     │
│  │ ████████████████████████████████████████        │     │
│  │                                                  │     │
│  │ Step 3: Feature Highlight      298 sent  65% open│     │
│  │ ██████████████████████████████████             │     │
│  │                                                  │     │
│  │ Step 4: Check-In               280 sent  58% open│     │
│  │ ████████████████████████████████               │     │
│  │                                                  │     │
│  │ Step 5: Special Offer          265 sent  45% open│     │
│  │ ████████████████████████                       │     │
│  └─────────────────────────────────────────────────┘     │
│                                                           │
│  Drop-off points · Exit reasons · Per-step click rates    │
│                                                           │
│  [Enrolled Contacts] [Export Data] [Edit Sequence]        │
└──────────────────────────────────────────────────────────┘

4.10 Cron Job Updates

// Update the marketing cron job to also process sequences:
// /api/cron/marketing-scheduler

export async function GET() {
  // 1. Check scheduled campaigns (existing from MKT-02)
  await checkScheduledCampaigns();

  // 2. Process drip sequence steps (new)
  await processSequenceSteps();

  // 3. Process A/B test winners (from MKT-02)
  await checkABTestResults();

  return new Response("OK");
}

// vercel.json cron entry (already added, just confirm):
// { "path": "/api/cron/marketing-scheduler", "schedule": "*/5 * * * *" }

4.11 Phase MKT-04 Deliverables Checklist

- [ ] Sequence list page with status filters
- [ ] Sequence builder with visual timeline
- [ ] Email step editor (reuses email editor from MKT-02)
- [ ] Delay step editor (amount + unit)
- [ ] Condition step editor (branching logic)
- [ ] Action step editor (tag, field, segment)
- [ ] Exit conditions configuration
- [ ] Sequence execution engine (processSequenceSteps)
- [ ] Contact enrollment logic (prevents duplicates)
- [ ] Smart timing implementation
- [ ] 15 pre-built marketing automation templates
- [ ] New automation event types added to EVENT_REGISTRY (18 events)
- [ ] New automation action types added to ACTION_TYPES (8 actions)
- [ ] Sequence performance page with step funnel
- [ ] Cron job updated for sequence processing
- [ ] Manual enrollment UI (select contacts → enroll in sequence)
- [ ] Sequence pause/resume/stop functionality
- [ ] Automation events: sequence.enrolled, sequence.completed, sequence.exited
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-05: Marketing Hub Dashboard

Priority: HIGH — Central command center for all marketing activity Estimated Complexity: Medium Dependencies: Phases MKT-01 through MKT-04 (needs data to display) Files Created: ~12 new files Files Modified: 1-2 files

5.1 Overview

The Marketing Hub is the main landing page when users navigate to the Marketing section. It provides a unified overview of all marketing activity, quick actions, and performance insights. Think of it as the "Marketing Home" — the first thing users see.

5.2 Hub Layout

Route: /dashboard/sites/[siteId]/marketing
┌─────────────────────────────────────────────────────────────┐
│ Marketing Hub                                   [+ Quick Actions ▼]│
│                                                              │
│  ┌─── Overview Stats ────────────────────────────────────┐  │
│  │ Total Subscribers │ Emails Sent (30d) │ Avg Open Rate │  │
│  │      2,450       │      12,340       │    42.3%      │  │
│  │   +127 this month│   +3,200 vs prev  │  +2.1% vs prev│  │
│  │                  │                    │               │  │
│  │ Avg Click Rate   │ Active Sequences  │ Revenue Attr. │  │
│  │     8.7%        │       4           │   $12,450     │  │
│  │  +0.5% vs prev  │   324 enrolled    │  +$3,200      │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌─── Recent Campaigns ─────────────────────────────────┐  │
│  │ Campaign              Status    Open%  Click%  Sent   │  │
│  │ Summer Sale Blast     ● Sent    49.4%  14.0%  Jun 15 │  │
│  │ Weekly Newsletter #12 ● Sent    38.2%  9.1%   Jun 12 │  │
│  │ Product Launch        ⬤ Sched  --     --     Jun 20 │  │
│  │ Flash Sale            ○ Draft   --     --     --     │  │
│  │                                    [View All Campaigns]  │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌─── Active Sequences ──────┐ ┌─── Quick Actions ───────┐ │
│  │ Welcome Onboarding  324 ✓ │ │ 📧 Create Campaign       │ │
│  │ Cart Recovery       89 ↻  │ │ 📋 View Subscribers      │ │
│  │ Post-Purchase       156 ✓ │ │ 🔄 Create Sequence       │ │
│  │ Re-Engagement       45 ↻  │ │ 📊 View Analytics        │ │
│  │         [View All]        │ │ 📝 Create Landing Page   │ │
│  └───────────────────────────┘ │ 🤖 AI Campaign Ideas     │ │
│                                 └───────────────────────────┘ │
│                                                              │
│  ┌─── Engagement Over Time (30 days) ───────────────────┐  │
│  │ [Line chart: sends, opens, clicks over time]          │  │
│  │ Toggle: 7d | 30d | 90d                                │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌─── Subscriber Growth ────┐ ┌─── Top Content ──────────┐ │
│  │ [Area chart: new subs    │ │ Most clicked links       │ │
│  │  per week over 90 days]  │ │ Most opened campaigns    │ │
│  │                          │ │ Best performing templates │ │
│  └──────────────────────────┘ └───────────────────────────┘ │
│                                                              │
│  ┌─── AI Insights ──────────────────────────────────────┐  │
│  │ 💡 Your open rates are 15% above industry average     │  │
│  │ 💡 Tuesday 10AM is your best send time                │  │
│  │ 💡 "Personalized" subject lines get 23% more opens    │  │
│  │ 💡 45 contacts haven't engaged in 60+ days            │  │
│  │                            [View All Insights]         │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

5.3 Component Structure

marketing/
├── page.tsx                          — Marketing Hub (server component)
├── components/
│   ├── MarketingHubStats.tsx         — Overview stat cards with trends
│   ├── RecentCampaigns.tsx           — Last 5 campaigns table
│   ├── ActiveSequences.tsx           — Active sequence summary cards
│   ├── QuickActions.tsx              — Action buttons grid
│   ├── EngagementChart.tsx           — Engagement line chart with toggle
│   ├── SubscriberGrowthChart.tsx     — Subscriber growth area chart
│   ├── TopContentPanel.tsx           — Best performing content
│   ├── AIInsightsPanel.tsx           — AI-generated marketing insights
│   └── MarketingHubSkeleton.tsx      — Loading skeleton

5.4 AI Insights Engine

// ai-insights.ts
// Analyzes marketing data and generates actionable insights
// Called periodically (daily) or on-demand from the hub
//
// Uses Claude Haiku for fast, cost-effective insight generation

import { generateWithAI } from "@/lib/ai"; // Existing AI helper

export async function generateMarketingInsights(
  siteId: string,
): Promise<MarketingInsight[]> {
  const supabase = createServiceClient();

  // Gather data points
  const [stats, recentCampaigns, subscriberTrend, engagementDistribution] =
    await Promise.all([
      getAggregateCampaignStats(supabase, siteId, { days: 90 }),
      getRecentCampaigns(supabase, siteId, 20),
      getSubscriberTrend(supabase, siteId, 90),
      getEngagementDistribution(supabase, siteId),
    ]);

  // Build analysis prompt
  const prompt = `Analyze this email marketing data and provide 3-5 actionable insights.
Data:
- Average open rate: ${stats.averageOpenRate}%
- Average click rate: ${stats.averageClickRate}%
- Subscriber count: ${stats.totalSubscribers}
- New subscribers (90d): ${subscriberTrend.newCount}
- Unsubscribes (90d): ${subscriberTrend.unsubCount}
- Best campaign: "${recentCampaigns[0]?.name}" (${recentCampaigns[0]?.openRate}% opens)
- Worst campaign: "${recentCampaigns[recentCampaigns.length - 1]?.name}" (${recentCampaigns[recentCampaigns.length - 1]?.openRate}% opens)
- Engagement distribution: Cold ${engagementDistribution.cold}%, Warm ${engagementDistribution.warm}%, Hot ${engagementDistribution.hot}%
- Best send day: ${stats.bestSendDay}
- Best send hour: ${stats.bestSendHour}

Provide insights as JSON array: [{ "type": "positive|warning|suggestion", "text": "..." }]
Keep each insight under 100 characters. Be specific and actionable.`;

  const result = await generateWithAI({
    model: "haiku", // Fast model for insights
    prompt,
    maxTokens: 500,
  });

  return JSON.parse(result);
}

interface MarketingInsight {
  type: "positive" | "warning" | "suggestion";
  text: string;
}

5.5 Navigation Structure

The marketing section uses a sub-navigation pattern consistent with other modules:

// Marketing sub-navigation items (added to dashboard navigation):
const marketingNavItems = [
  { label: "Hub", href: "/marketing", icon: "LayoutDashboard" },
  { label: "Campaigns", href: "/marketing/campaigns", icon: "Mail" },
  { label: "Sequences", href: "/marketing/sequences", icon: "GitBranch" },
  { label: "Templates", href: "/marketing/templates", icon: "FileText" },
  { label: "Landing Pages", href: "/marketing/landing-pages", icon: "Globe" },
  { label: "Forms", href: "/marketing/forms", icon: "FormInput" },
  { label: "Analytics", href: "/marketing/analytics", icon: "BarChart3" },
  { label: "Settings", href: "/marketing/settings", icon: "Settings" },
];

// All routes are relative to /dashboard/sites/[siteId]/

5.6 Quick Actions Implementation

// Quick Actions should:
// 1. "Create Campaign" → navigates to /marketing/campaigns/new
// 2. "View Subscribers" → navigates to CRM contacts filtered by email_subscribed=true
// 3. "Create Sequence" → navigates to /marketing/sequences/new
// 4. "View Analytics" → navigates to /marketing/analytics
// 5. "Create Landing Page" → navigates to /marketing/landing-pages/new (Phase MKT-06)
// 6. "AI Campaign Ideas" → opens dialog with AI-generated campaign suggestions
//
// AI Campaign Ideas dialog:
// - Takes optional topic/goal input
// - Generates 5 campaign ideas with subject lines
// - User can "Create" any suggestion → pre-fills campaign wizard

5.7 Revenue Attribution

// revenue-attribution.ts
// Tracks revenue attributed to marketing campaigns.
// Attribution model: Last-touch within 7-day window
//
// When an e-commerce order is placed:
// 1. Check if the ordering contact clicked a marketing email in last 7 days
// 2. If yes, attribute the order revenue to that campaign
// 3. Store in mod_mktmod01_campaign_stats.revenue_attributed
//
// This requires a hook in the e-commerce order completion flow:
// After order.completed → check for recent marketing click → attribute

export async function attributeRevenue(
  supabase: SupabaseClient,
  contactId: string,
  orderId: string,
  orderTotal: number,
  siteId: string,
): Promise<void> {
  const sevenDaysAgo = new Date();
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

  // Find most recent campaign click within attribution window
  const { data: recentClick } = await supabase
    .from("mod_mktmod01_email_sends")
    .select("campaign_id, clicked_at")
    .eq("contact_id", contactId)
    .not("clicked_at", "is", null)
    .gte("clicked_at", sevenDaysAgo.toISOString())
    .order("clicked_at", { ascending: false })
    .limit(1)
    .single();

  if (recentClick) {
    // Attribute revenue to campaign
    await supabase.rpc("add_campaign_revenue", {
      p_campaign_id: recentClick.campaign_id,
      p_amount: orderTotal,
    });

    // Log attribution
    await supabase.from("mod_mktmod01_campaign_stats").update({
      // RPC function handles atomic increment
    });

    // Fire event for tracking
    await logAutomationEvent({
      event_type: "marketing.revenue.attributed",
      site_id: siteId,
      metadata: {
        campaign_id: recentClick.campaign_id,
        order_id: orderId,
        amount: orderTotal,
        contact_id: contactId,
      },
    });
  }
}

5.8 Marketing Settings Page

Route: /dashboard/sites/[siteId]/marketing/settings
// Settings sections:
//
// 1. Email Defaults
//    - Default from name
//    - Default from email
//    - Default reply-to
//    - Email footer text (company address for CAN-SPAM compliance)
//    - Default unsubscribe message
//
// 2. Sending Configuration
//    - Daily send limit (protect deliverability)
//    - Batch size
//    - Throttle delay between batches
//    - Send time optimization (on/off)
//
// 3. Tracking
//    - Open tracking enabled (default: true)
//    - Click tracking enabled (default: true)
//    - UTM parameters auto-append (on/off)
//    - Default UTM source (e.g., "email")
//    - Default UTM medium (e.g., "marketing")
//
// 4. Compliance
//    - Double opt-in required (on/off)
//    - Unsubscribe page message (customizable)
//    - Physical mailing address (CAN-SPAM)
//    - GDPR consent message
//
// 5. Integrations
//    - Resend API key status (connected/not connected)
//    - Webhook URL display (for manual configuration if needed)
//    - Social media accounts connected (for future social features)
//
// All settings saved to mod_mktmod01_settings table

5.9 Phase MKT-05 Deliverables Checklist

- [ ] Marketing Hub main page with all sections
- [ ] Overview stat cards with trend comparisons
- [ ] Recent campaigns table
- [ ] Active sequences summary
- [ ] Quick actions grid
- [ ] Engagement over time chart (7d/30d/90d toggle)
- [ ] Subscriber growth chart
- [ ] Top content panel
- [ ] AI Insights panel with generated insights
- [ ] AI Campaign Ideas dialog
- [ ] Revenue attribution logic
- [ ] Marketing sub-navigation structure
- [ ] Marketing Settings page with all sections
- [ ] Loading skeleton for hub
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-06: Landing Pages & Opt-In Forms

Priority: HIGH — Primary lead capture mechanism Estimated Complexity: High Dependencies: Phase MKT-01 (tables), Phase MKT-04 (sequence enrollment) Files Created: ~20 new files Files Modified: 2-3 existing files (storefront routing)

6.1 Overview

Landing pages and opt-in forms are the primary lead capture tools. This phase builds:

  • Landing page builder with visual editor
  • Opt-in/signup form builder
  • Pop-up and embedded form widgets
  • Form submission handling with CRM integration
  • Landing page analytics (visits, conversions)
  • UTM tracking for traffic source attribution

6.2 Landing Page Builder

Location: src/app/dashboard/sites/[siteId]/marketing/landing-pages/

Routes:
  /marketing/landing-pages              — List all landing pages
  /marketing/landing-pages/new          — Create new
  /marketing/landing-pages/[id]/edit    — Edit existing
  /marketing/landing-pages/[id]/stats   — View analytics

Landing Page Data Model

// Uses mod_mktmod01_landing_pages table from Phase MKT-01
// Key fields:
// - title: page title
// - slug: URL path (e.g., "summer-sale" → site.com/lp/summer-sale)
// - content_json: page blocks (similar to email editor but for web pages)
// - form_config: opt-in form configuration embedded in page
// - seo_config: { meta_title, meta_description, og_image }
// - published: boolean
// - conversion_goal: 'form_submit' | 'button_click' | 'page_scroll'

Landing Page Builder Components

landing-pages/
├── components/
│   ├── LandingPageEditor.tsx       — Main editor (similar to studio builder but simplified)
│   ├── LandingPagePreview.tsx      — Live preview
│   ├── PageBlocks/
│   │   ├── HeroSection.tsx         — Full-width hero with CTA
│   │   ├── FeatureGrid.tsx         — Feature/benefit cards
│   │   ├── TestimonialSection.tsx  — Customer testimonials
│   │   ├── PricingTable.tsx        — Pricing comparison
│   │   ├── FAQSection.tsx          — Accordion FAQ
│   │   ├── CountdownTimer.tsx      — Urgency timer
│   │   ├── VideoEmbed.tsx          — YouTube/Vimeo embed
│   │   ├── ImageGallery.tsx        — Image showcase
│   │   ├── OptInFormBlock.tsx      — Embedded opt-in form
│   │   ├── CTASection.tsx          — Call to action strip
│   │   └── SocialProof.tsx         — Logos, stats, trust badges
│   ├── PageSettings.tsx            — SEO, slug, conversion goal
│   └── PageTemplates.tsx           — Pre-built landing page templates

Pre-Built Landing Page Templates (8 minimum)

1. lead-magnet       — Download/resource opt-in
2. webinar           — Event/webinar registration
3. product-launch    — New product announcement
4. coming-soon       — Pre-launch with email capture
5. sale-promo        — Sale/promotion with countdown
6. ebook-download    — E-book/whitepaper download
7. free-trial        — SaaS/service free trial signup
8. consultation      — Book a consultation/demo

Public Landing Page Rendering

Public URL: https://{site-domain}/lp/{slug}
// Storefront route: src/app/(storefront)/lp/[slug]/page.tsx
// This is a PUBLIC page — no auth required
// Renders the landing page content with:
// - Full page layout from content_json
// - Opt-in form (if configured)
// - Tracking pixel for visit counting
// - UTM parameter capture from URL
// - Conversion tracking script
// - SEO meta tags from seo_config
// - Open Graph tags for social sharing

// On page load:
// 1. Increment visit counter
// 2. Capture UTM params from URL (utm_source, utm_medium, utm_campaign, etc.)
// 3. Store visit in mod_mktmod01_landing_page_visits (or inline in landing_pages stats)
// 4. Fire: marketing.landing.visited event

export default async function LandingPage({
  params,
}: {
  params: { slug: string };
}) {
  // Look up landing page by slug
  // Must also resolve which site this belongs to (from domain or subdomain)
  // Render page blocks
  // Include conversion tracking
}

6.3 Opt-In Form Builder

Location: src/app/dashboard/sites/[siteId]/marketing/forms/

Routes:
  /marketing/forms              — List all forms
  /marketing/forms/new          — Create new form
  /marketing/forms/[id]/edit    — Edit form
  /marketing/forms/[id]/stats   — Form analytics

Form Types

// Uses mod_mktmod01_forms table from Phase MKT-01

type FormType =
  | "inline" // Embedded in page content
  | "popup" // Modal popup (trigger: time delay, scroll %, exit intent)
  | "slide_in" // Slide-in from bottom/side
  | "top_bar" // Sticky banner at top of page
  | "full_page"; // Full-page overlay

interface FormConfig {
  type: FormType;
  fields: FormField[];
  style: FormStyle;
  trigger: FormTrigger; // When popup/slide-in appears
  success_action: SuccessAction; // What happens after submit
  tags_to_add: string[]; // Tags added to contact on submit
  sequence_to_enroll?: string; // Auto-enroll in drip sequence
  double_optin: boolean; // Require email confirmation
}

interface FormField {
  name: string;
  label: string;
  type: "email" | "text" | "phone" | "select" | "checkbox" | "hidden";
  required: boolean;
  placeholder?: string;
  options?: string[]; // For select fields
}

interface FormTrigger {
  type: "immediate" | "time_delay" | "scroll_percent" | "exit_intent" | "click";
  delay_seconds?: number; // For time_delay
  scroll_percent?: number; // For scroll_percent (e.g., 50)
  element_selector?: string; // For click trigger
  show_once_per_session: boolean;
  show_once_per_visitor: boolean;
}

interface SuccessAction {
  type: "message" | "redirect" | "close";
  message?: string; // Thank you message
  redirect_url?: string; // Redirect URL
}

Form Builder UI

┌──────────────────────────────────────────────────────┐
│ Form Builder                                          │
├──────────────┬───────────────────────────────────────┤
│              │                                        │
│ Form Type:   │  PREVIEW:                              │
│ ○ Inline     │  ┌────────────────────────────────┐   │
│ ● Popup      │  │                                │   │
│ ○ Slide-in   │  │  Get Our Free Guide! 📚        │   │
│ ○ Top Bar    │  │                                │   │
│ ○ Full Page  │  │  Name: [_______________]       │   │
│              │  │  Email: [______________]       │   │
│ Fields:      │  │                                │   │
│ [+] Add Field│  │  [Download Now →]              │   │
│ · Email (req)│  │                                │   │
│ · Name       │  │  We respect your privacy.      │   │
│ · Company    │  │                                │   │
│              │  └────────────────────────────────┘   │
│ Trigger:     │                                        │
│ After 5 sec  │                                        │
│ Show once    │                                        │
│              │                                        │
│ On Submit:   │                                        │
│ ● Message    │                                        │
│ ○ Redirect   │                                        │
│              │                                        │
│ Auto-actions:│                                        │
│ Tag: lead    │                                        │
│ Sequence:    │                                        │
│ [Welcome ▼]  │                                        │
│              │                                        │
│ [Save] [Get Embed Code]                               │
├──────────────┴───────────────────────────────────────┤

Embed Code Generation

// When user clicks "Get Embed Code", generate:

// Option 1: JavaScript embed (recommended)
// <script src="https://app.dramacagency.com/api/forms/embed/{formId}.js"></script>

// Option 2: iframe embed
// <iframe src="https://app.dramacagency.com/api/forms/embed/{formId}" ...></iframe>

// Option 3: HTML + API (for developers)
// Raw HTML form that POSTs to /api/forms/submit/{formId}

// The JavaScript embed:
// - Injects form HTML into page
// - Handles popup/slide-in/top-bar triggers
// - Handles submission via fetch() API
// - Shows success message or redirect
// - Respects show_once settings via localStorage
// - Minimal footprint (~5KB gzipped)

Form Submission Handler

// API: POST /api/forms/submit/[formId]
// Public route — no auth (used from external websites)
// MUST have CORS headers for cross-origin embeds
// MUST have rate limiting to prevent abuse

export async function POST(
  request: Request,
  { params }: { params: { formId: string } },
) {
  // 1. Load form config
  // 2. Validate submission against form fields
  // 3. Rate limit check (IP-based, max 10 submissions per minute)
  // 4. Create or update CRM contact:
  //    - If email exists → update existing contact
  //    - If new → create contact with email_subscribed = true
  // 5. Add configured tags
  // 6. If double_optin → send confirmation email, don't subscribe yet
  // 7. If sequence configured → enroll in sequence
  // 8. Log form submission to mod_mktmod01_form_submissions
  // 9. Increment form stats (total_submissions, unique conversions)
  // 10. Fire automation event: marketing.form.submitted
  // 11. Return success response

  // CORS headers:
  return new Response(
    JSON.stringify({ success: true, message: form.success_action.message }),
    {
      headers: {
        "Access-Control-Allow-Origin": "*", // Or restrict to site domain
        "Access-Control-Allow-Methods": "POST",
        "Access-Control-Allow-Headers": "Content-Type",
        "Content-Type": "application/json",
      },
    },
  );
}

6.4 UTM Tracking

// utm-tracking.ts
// Captures UTM parameters from URLs and stores them on:
// - Form submissions (which source led to the signup)
// - Landing page visits
// - CRM contact records (first_touch_utm, last_touch_utm)

interface UTMParams {
  utm_source?: string; // e.g., "google", "facebook", "newsletter"
  utm_medium?: string; // e.g., "cpc", "email", "social"
  utm_campaign?: string; // e.g., "summer-sale-2026"
  utm_term?: string; // e.g., search keyword
  utm_content?: string; // e.g., "header-banner" vs "footer-link"
}

// Auto-append UTM params to all marketing email links:
// utm_source=email&utm_medium=marketing&utm_campaign={campaign_name}
// This is configured in Marketing Settings (MKT-05)

6.5 Phase MKT-06 Deliverables Checklist

- [ ] Landing page list page
- [ ] Landing page builder with visual editor
- [ ] 8 pre-built landing page templates
- [ ] Public landing page rendering (storefront route)
- [ ] Landing page SEO/meta tag configuration
- [ ] Form builder UI with live preview
- [ ] 5 form types (inline, popup, slide-in, top bar, full page)
- [ ] Form field configuration (email, text, phone, select, checkbox)
- [ ] Form trigger configuration (time delay, scroll, exit intent)
- [ ] Embed code generation (JS, iframe, raw HTML)
- [ ] Form submission API (public, CORS, rate-limited)
- [ ] CRM integration (create/update contact on form submit)
- [ ] Auto-tag and auto-sequence-enroll on form submit
- [ ] Double opt-in flow
- [ ] UTM parameter tracking and storage
- [ ] Landing page visit tracking
- [ ] Form submission analytics
- [ ] Automation events: form.submitted, landing.visited, landing.converted
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-07: Blog Marketing Enhancement

Priority: MEDIUM-HIGH — Maximizes existing blog investment Estimated Complexity: Medium Dependencies: Phase MKT-02 (email templates), Phase MKT-06 (forms) Files Created: ~10 new files Files Modified: 5-8 existing blog files

7.1 Overview

This phase enhances the existing blog system with marketing capabilities. It does NOT rebuild the blog — it adds marketing layers on top of what exists:

  • In-post CTA blocks (call-to-action embedded in blog posts)
  • Blog subscriber opt-in (newsletter signup on blog pages)
  • Content-to-email conversion (turn blog post into email campaign)
  • Exit-intent popup configuration for blog pages
  • RSS feed for email newsletter automation
  • Blog post performance tracking (reads → engagement → conversion)
  • SEO content scoring with AI suggestions

7.2 In-Post CTA Blocks

Enhancement to existing blog editor

// Add new block type to the blog post editor (Studio Builder / rich text editor)
// Blog CTA Block: a styled card within blog content that drives action

interface BlogCTABlock {
  type: "marketing_cta";
  style: "banner" | "card" | "inline" | "sidebar";
  heading: string;
  body: string;
  button_text: string;
  button_url: string; // Link to landing page, form, or external URL
  form_id?: string; // Optional: embed an opt-in form instead of link
  background_color?: string; // Uses brand colors by default
  image_url?: string;
  position:
    | "after_paragraph_2"
    | "after_paragraph_5"
    | "end_of_post"
    | "custom";
}

// Predefined CTA templates:
// - "Subscribe to Newsletter" banner
// - "Download Free Guide" card
// - "Book a Consultation" card
// - "Related Product" card (pulls from e-commerce)
// - "Share This Post" social buttons

7.3 Blog Subscriber Widget

// Add newsletter opt-in component to blog layout
// Appears: below posts, in sidebar, or as floating widget

// Component: BlogSubscribeWidget.tsx
// Location: Added to blog layout/template

// Features:
// - Simple email input + subscribe button
// - Customizable heading and description
// - Connects to marketing form system (creates form automatically)
// - Auto-tags contact as "blog-subscriber"
// - Auto-enrolls in blog subscriber drip sequence
// - Shows on all blog pages (configurable to specific posts/categories)

7.4 Content-to-Email Conversion

// One-click: turn a blog post into an email campaign
// "Share as Email" button on blog post editor/detail page

export async function convertBlogToEmail(
  blogPostId: string,
  siteId: string,
): Promise<Campaign> {
  const post = await getBlogPost(blogPostId);

  // Convert blog HTML content to email blocks:
  // - Title → Header block
  // - Featured image → Image block
  // - Content paragraphs → Text blocks
  // - Embedded images → Image blocks
  // - End → "Read full post on our blog" CTA button
  // - Footer → Standard email footer with unsubscribe

  const emailContent = blogHtmlToEmailBlocks(post.content, post.featured_image);

  const campaign = await createCampaign({
    site_id: siteId,
    name: `Blog: ${post.title}`,
    subject_line: post.title,
    preview_text: post.excerpt || post.meta_description,
    content_json: emailContent,
    campaign_type: "regular",
    status: "draft",
  });

  return campaign;
}

7.5 Blog SEO Content Scoring

// AI-powered content scoring for blog posts
// Analyzes post and provides marketing-focused SEO recommendations

interface ContentScore {
  overall: number; // 0-100
  readability: number; // Flesch-Kincaid
  seo_score: number; // Keyword density, meta tags, headings
  engagement_potential: number; // Predicted shareability
  recommendations: string[];
}

// Scoring criteria:
// - Title length (50-60 chars optimal)
// - Meta description present and optimal length
// - H2/H3 heading structure
// - Image alt text present
// - Internal links count
// - Word count (1,000+ recommended)
// - Keyword usage (if target keyword set)
// - CTA present in post
// - Readability level
//
// AI enhancement: Claude Haiku analyzes content and suggests:
// - Better title options
// - Missing keywords to add
// - Sections to expand
// - CTA placement recommendations
// - Social media post suggestions for sharing

7.6 RSS Feed for Email

// Generate RSS feed for the blog that can be consumed by:
// - Email newsletter automation (send latest posts weekly)
// - External subscribers
// - Social media auto-posting tools

// Route: /api/sites/[siteId]/blog/rss.xml
// Public route — returns XML RSS feed

// Marketing integration:
// Create automated workflow template:
// Trigger: "New blog post published"
// Action: "Send blog digest email to blog-subscriber segment"
// Schedule: Weekly digest or per-post notification

7.7 Phase MKT-07 Deliverables Checklist

- [ ] Blog CTA block type added to editor (4 styles)
- [ ] Pre-built CTA templates (5 templates)
- [ ] Blog subscriber widget component
- [ ] Blog subscribe form auto-creation
- [ ] Content-to-email conversion function
- [ ] "Share as Email" button on blog post editor
- [ ] Blog HTML to email blocks converter
- [ ] SEO content scoring engine
- [ ] AI content recommendations
- [ ] RSS feed API route
- [ ] Blog digest automation template
- [ ] Blog post engagement tracking (read time, scroll depth)
- [ ] Exit-intent popup config for blog pages
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-08: SMS & WhatsApp Channel (Foundation)

Priority: MEDIUM — Channel expansion for higher engagement Estimated Complexity: Medium Dependencies: Phase MKT-01 (settings), Phase MKT-04 (sequences) Files Created: ~12 new files Files Modified: 2-3 existing files

8.1 Overview

This phase adds SMS and WhatsApp as marketing channels. Rather than building full-featured SMS/WhatsApp campaign builders (which mirror email), this phase focuses on:

  • SMS/WhatsApp as steps within existing sequences and automations
  • SMS campaign sending (text-only, simpler than email)
  • WhatsApp template message sending (Meta Business API)
  • Contact phone number collection and opt-in
  • SMS/WhatsApp delivery tracking
  • Provider abstraction (Twilio for SMS, Meta API for WhatsApp — configurable)

Provider Note: Use a provider abstraction layer so SMS provider can be swapped (Twilio, Vonage, MessageBird, etc.).

8.2 SMS Architecture

// sms-provider.ts
// Abstract interface for SMS providers

interface SMSProvider {
  sendSMS(
    to: string,
    message: string,
    options?: SMSOptions,
  ): Promise<SMSSendResult>;
  getDeliveryStatus(messageId: string): Promise<DeliveryStatus>;
  getBalance(): Promise<number>;
}

interface SMSOptions {
  from?: string; // Sender ID or phone number
  campaign_id?: string; // For tracking
  contact_id?: string; // For tracking
}

interface SMSSendResult {
  success: boolean;
  messageId?: string;
  error?: string;
}

// Twilio implementation:
class TwilioSMSProvider implements SMSProvider {
  async sendSMS(to: string, message: string, options?: SMSOptions) {
    // Uses Twilio REST API
    // Credentials from mod_mktmod01_settings: twilio_account_sid, twilio_auth_token, twilio_phone_number
  }
}

// Future: VonageSMSProvider, MessageBirdSMSProvider, etc.

8.3 WhatsApp Architecture

// whatsapp-provider.ts
// Uses Meta Business API (Cloud API) for WhatsApp Business

interface WhatsAppProvider {
  sendTemplateMessage(
    to: string,
    templateName: string,
    language: string,
    components: TemplateComponent[],
  ): Promise<WhatsAppSendResult>;

  sendTextMessage(to: string, text: string): Promise<WhatsAppSendResult>;
}

// Important: WhatsApp Business API requires:
// 1. Approved business account
// 2. Pre-approved message templates (for marketing messages)
// 3. 24-hour window for freeform messages (only after user initiates)
//
// For marketing: ONLY template messages are allowed
// Templates must be pre-approved by Meta before use

8.4 SMS/WhatsApp as Sequence Steps

// Extend sequence step types to support SMS and WhatsApp:

interface SMSStepConfig {
  message: string; // SMS text (max 160 chars for single, or multi-segment)
  personalization: boolean; // Replace {{first_name}} etc.
}

interface WhatsAppStepConfig {
  template_name: string; // Pre-approved template
  template_language: string;
  template_components: TemplateComponent[];
}

// In SequenceBuilder, add:
// - "SMS" step type with text input
// - "WhatsApp" step type with template selector
// These appear alongside Email, Delay, Condition, Action steps

8.5 SMS Campaign Builder

Route: /dashboard/sites/[siteId]/marketing/sms
// Simplified campaign builder for SMS:
// - Campaign name
// - Message text (with character counter: X/160)
// - Personalization token insertion
// - Audience selection (reuses same audience resolver, but filters for contacts with phone_number)
// - Schedule or send now
// - Test SMS button

// SMS campaigns use the same mod_mktmod01_campaigns table
// with channel = 'sms' instead of 'email'

8.6 Contact Phone Opt-In

// Extend opt-in forms to collect phone numbers:
// - Add phone field type with country code selector
// - SMS opt-in checkbox: "I agree to receive SMS messages"
// - WhatsApp opt-in checkbox: "I agree to receive WhatsApp messages"
// - Store on CRM contact: phone_number, sms_subscribed, whatsapp_subscribed
//
// Compliance:
// - TCPA compliance for US SMS
// - Opt-in language clearly visible
// - Easy opt-out (reply STOP for SMS)

8.7 Settings

Marketing Settings → SMS/WhatsApp section:

SMS:
- Provider: [Twilio ▼]
- Account SID: [****]
- Auth Token: [****]
- Phone Number: [+1234567890]
- Daily SMS limit: [500]

WhatsApp:
- Business Account ID: [****]
- Phone Number ID: [****]
- Access Token: [****]
- Approved Templates: [list with sync button]

8.8 Phase MKT-08 Deliverables Checklist

- [ ] SMS provider abstraction layer
- [ ] Twilio SMS provider implementation
- [ ] WhatsApp provider (Meta Cloud API) implementation
- [ ] SMS step type in sequence builder
- [ ] WhatsApp step type in sequence builder
- [ ] SMS campaign builder (simplified)
- [ ] Phone number collection in form builder
- [ ] SMS/WhatsApp opt-in fields on forms
- [ ] Delivery tracking for SMS/WhatsApp
- [ ] SMS/WhatsApp settings in marketing settings
- [ ] Contact phone opt-in management
- [ ] Automation actions: sms.send, whatsapp.send_template
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-09: AI Marketing Intelligence

Priority: MEDIUM — Differentiating feature for the platform Estimated Complexity: Medium Dependencies: Phases MKT-02 through MKT-05 (needs campaign data for analysis) Files Created: ~10 new files Files Modified: 2-3 existing files

9.1 Overview

This phase adds AI-powered intelligence across the marketing module:

  • AI subject line generator (with A/B score prediction)
  • AI content writer for email campaigns
  • AI audience suggestion ("who should receive this?")
  • Send time optimization (predict best time per contact)
  • Campaign performance predictor
  • Smart segmentation suggestions
  • Competitor-aware content suggestions
  • AI-powered campaign briefing (natural language → campaign setup)

9.2 AI Subject Line Generator

// subject-line-ai.ts
// Generates subject line options with predicted performance

export async function generateSubjectLines(
  input: SubjectLineInput,
): Promise<SubjectLineSuggestion[]> {
  const prompt = `Generate 5 email subject lines for a marketing campaign.

Campaign context:
- Campaign goal: ${input.goal}
- Target audience: ${input.audienceDescription}
- Key message: ${input.keyMessage}
- Brand voice: ${input.brandVoice || "professional and friendly"}
- Industry: ${input.industry || "general"}

For each subject line, provide:
1. The subject line (max 60 characters)
2. A preview text suggestion (max 100 characters)
3. Predicted open rate category: "high", "medium", or "low"
4. Why this subject line works (max 30 words)

Techniques to use across suggestions:
- Curiosity gap
- Personalization (use {{first_name}})
- Numbers/statistics
- Questions
- Urgency (without being spammy)

Return as JSON array.`;

  const result = await generateWithAI({
    model: "haiku",
    prompt,
    maxTokens: 800,
  });

  return JSON.parse(result);
}

9.3 AI Email Content Writer

// email-content-ai.ts
// Generates email body content given a brief

export async function generateEmailContent(
  input: EmailContentInput,
): Promise<EmailContent> {
  // Takes:
  // - Campaign goal/purpose
  // - Key message/offer
  // - Target audience description
  // - Desired tone
  // - CTA (what should reader do)
  // - Brand voice from site settings
  //
  // Returns: Complete EmailContent (block JSON) that can be
  // loaded directly into the email editor
  //
  // AI generates the content text, but structures it into
  // the email block format (header, paragraphs, images, CTA button, footer)
  //
  // Uses Claude Sonnet for higher quality content generation
}

// Also: "Improve this section" — user selects a text block,
// clicks "AI Improve", and gets a rewritten version
export async function improveEmailText(
  currentText: string,
  instruction: string, // e.g., "make it more persuasive", "shorten", "add urgency"
): Promise<string> {
  // Returns improved text
}

9.4 Send Time Optimization

// send-time-optimizer.ts
// Predicts the best time to send emails for maximum engagement
// Uses historical open data to find patterns

export async function getOptimalSendTime(
  supabase: SupabaseClient,
  siteId: string,
  contactId?: string, // Optional: optimize for specific contact
): Promise<{ day: number; hour: number; confidence: number }> {
  // If contact-specific: analyze their personal open times
  // If general: analyze site-wide open patterns
  // Query: email_events where event_type = 'open'
  // Group by: day_of_week, hour_of_day
  // Find: peak engagement windows
  // Returns: recommended day (0=Sun, 6=Sat) and hour (0-23)
  // With confidence score (based on data volume)
  // If insufficient data (< 100 opens): return industry defaults
  // Default: Tuesday/Wednesday at 10 AM local time
}

// Integration: Campaign scheduler can auto-select optimal send time
// UI: "Send at optimal time" checkbox in schedule step

9.5 Smart Audience Suggestions

// audience-ai.ts
// Suggests the best audience for a campaign based on content and goal

export async function suggestAudience(
  campaignContent: EmailContent,
  campaignGoal: string,
  siteId: string,
): Promise<AudienceSuggestion[]> {
  // 1. Analyze campaign content to understand topic/offer
  // 2. Look at CRM segments and contact attributes
  // 3. Suggest segments or filters that match
  //
  // Example suggestions:
  // - "Engaged subscribers (opened email in last 30 days)" - 1,200 contacts
  // - "Customers who purchased in Product Category X" - 340 contacts
  // - "New subscribers this month (not yet sent campaign)" - 89 contacts
  //
  // Each suggestion includes estimated size and reasoning
}

9.6 AI Campaign Briefing

// campaign-briefing-ai.ts
// Natural language → campaign setup
// User describes what they want in plain text, AI creates the campaign

export async function createCampaignFromBrief(
  brief: string, // e.g., "Send a sale announcement to all customers,
  //  20% off everything this weekend, urgent tone"
  siteId: string,
): Promise<CampaignDraft> {
  // AI parses the brief and generates:
  // - Campaign name
  // - Subject line (+ variants for A/B)
  // - Preview text
  // - Audience filter suggestion
  // - Email content (full block JSON)
  // - Schedule suggestion (if timing mentioned)
  //
  // Returns as draft campaign — user reviews and adjusts before sending
}

9.7 Phase MKT-09 Deliverables Checklist

- [ ] AI subject line generator (5 suggestions with predictions)
- [ ] AI preview text generator
- [ ] AI email content writer (full campaign content from brief)
- [ ] "AI Improve" button on text blocks in editor
- [ ] Send time optimization algorithm
- [ ] "Send at optimal time" option in campaign scheduler
- [ ] Smart audience suggestions
- [ ] AI campaign briefing (natural language → campaign setup)
- [ ] AI marketing insights (enhanced from MKT-05 hub)
- [ ] Social media post suggestions from email content
- [ ] All AI features use existing AI infrastructure (generateWithAI)
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-10: Super Admin Marketing View

Priority: MEDIUM — Platform-wide health monitoring Estimated Complexity: Low-Medium Dependencies: Phases MKT-01 through MKT-05 Files Created: ~8 new files Files Modified: 1-2 existing admin files

10.1 Overview

Super admins need platform-wide visibility into email marketing health:

  • Email sending reputation monitoring
  • Cross-site marketing activity overview
  • Deliverability health dashboard
  • Abuse/spam complaint monitoring
  • Platform-wide sending limits
  • Resend API usage and quotas

10.2 Admin Marketing Dashboard

Route: /admin/marketing
┌───────────────────────────────────────────────────────────────┐
│ Platform Marketing Overview       [Super Admin]                │
├───────────────────────────────────────────────────────────────┤
│                                                                │
│  Platform Email Health                                         │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐            │
│  │ Delivery │ │ Bounce  │ │Complaint│ │ Rep     │            │
│  │  Rate   │ │  Rate   │ │  Rate   │ │ Score   │            │
│  │  98.7%  │ │  0.8%   │ │  0.02%  │ │ 95/100  │            │
│  │  ✅ Good │ │ ✅ Good  │ │ ✅ Good  │ │ ✅ Good  │            │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘            │
│                                                                │
│  Thresholds:                                                   │
│  ⚠️  Bounce rate > 2% triggers warning                        │
│  🔴 Complaint rate > 0.1% triggers alert                      │
│  🔴 Complaint rate > 0.3% auto-pauses all marketing sends     │
│                                                                │
│  Top Sites by Send Volume (last 30 days):                      │
│  ┌──────────────────┬──────────┬────────┬──────────┐          │
│  │ Site             │ Emails   │ Bounce │ Complaint│          │
│  ├──────────────────┼──────────┼────────┼──────────┤          │
│  │ Client A Site    │ 15,200   │ 0.5%   │ 0.01%   │          │
│  │ Client B Store   │ 8,400    │ 1.2%   │ 0.03%   │          │
│  │ Client C Blog    │ 3,100    │ 0.3%   │ 0.00%   │          │
│  └──────────────────┴──────────┴────────┴──────────┘          │
│                                                                │
│  Platform Sending Limits:                                      │
│  - Resend plan: [Pro / Enterprise]                             │
│  - Monthly limit: 50,000 | Used: 26,700 (53.4%)               │
│  - Rate limit: 100/sec                                         │
│                                                                │
│  [View Full Logs] [Manage Sending Limits] [Configure Alerts]  │
└───────────────────────────────────────────────────────────────┘

10.3 Platform Safety Controls

// admin-safety.ts
// Auto-protection mechanisms for platform email reputation

export async function checkPlatformHealth(): Promise<PlatformHealth> {
  // Run by admin cron or triggered by webhook events
  // 1. Calculate rolling bounce rate (last 7 days)
  // 2. Calculate rolling complaint rate (last 7 days)
  // 3. If complaint rate > 0.3%:
  //    - Auto-pause ALL marketing sends across ALL sites
  //    - Notify super admin via email
  //    - Log incident
  // 4. If bounce rate > 5%:
  //    - Auto-pause site-specific sends (the offending site)
  //    - Notify super admin
  // 5. If single site sends > daily limit:
  //    - Queue remaining emails for next day
  //    - Notify site owner
}

// Per-site sending limits (set by super admin):
// - max_daily_emails: number (default: 5000)
// - max_monthly_emails: number (default: 50000)
// - can_send_marketing: boolean (admin can disable marketing for a site)
// - warmup_mode: boolean (gradually increase sending limits for new sites)

10.4 Phase MKT-10 Deliverables Checklist

- [ ] Admin marketing dashboard page
- [ ] Platform email health metrics
- [ ] Per-site sending volume table
- [ ] Resend API usage display
- [ ] Auto-pause on high complaint rate
- [ ] Per-site sending limits
- [ ] Admin alerts configuration
- [ ] Health check cron job
- [ ] "Pause/resume marketing for site" admin action
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-11: Client Portal Marketing Views

Priority: MEDIUM — Client self-service marketing Estimated Complexity: Medium Dependencies: Phases MKT-01 through MKT-05 Files Created: ~15 new files Files Modified: 2-3 existing portal files

11.1 Overview

Client portal users (site owners/managers) get a permission-gated view of marketing features. They can:

  • View campaign reports (if can_manage_marketing permission)
  • Create and send campaigns (if can_manage_marketing permission)
  • View subscriber list
  • See marketing analytics
  • Manage sequences (view, pause, resume)
  • Access form embed codes

11.2 Portal Routes

/portal/sites/[siteId]/marketing              — Marketing hub (simplified)
/portal/sites/[siteId]/marketing/campaigns     — Campaign list + create
/portal/sites/[siteId]/marketing/campaigns/new — Create campaign
/portal/sites/[siteId]/marketing/campaigns/[id]— Campaign report
/portal/sites/[siteId]/marketing/sequences     — Sequence list
/portal/sites/[siteId]/marketing/subscribers   — Subscriber list
/portal/sites/[siteId]/marketing/analytics     — Analytics overview
/portal/sites/[siteId]/marketing/forms         — Forms + embed codes

11.3 Permission Gating

// Portal marketing pages check:
// 1. Site membership (user has access to this site)
// 2. can_manage_marketing permission (from portal_permissions)
//
// Pattern (follows existing portal permission checks):

export default async function PortalMarketingPage({ params }) {
  const { siteId } = params;
  const user = await getPortalUser();

  // Check site access
  const siteAccess = await checkSiteAccess(user.id, siteId);
  if (!siteAccess) redirect('/portal');

  // Check marketing permission
  const canManageMarketing = await checkPortalPermission(
    user.id, siteId, 'can_manage_marketing'
  );
  if (!canManageMarketing) {
    return <PermissionDenied feature="Marketing" />;
  }

  // Render marketing content...
}

11.4 Portal vs Dashboard Differences

Portal marketing views are SIMPLIFIED versions of dashboard views:

Dashboard (Agency)              → Portal (Client)
─────────────────────────────────────────────────
Full campaign builder           → Same builder (if permitted)
All analytics with raw data     → Simplified analytics (key metrics only)
Sequence builder (full)         → Sequence list (view/pause only, no build)
Landing page builder            → Landing page list + embed codes
Form builder                    → Form list + embed codes
AI insights (full)              → AI insights (read-only)
Marketing settings              → Limited settings (from name, footer text)
Admin safety controls           → NOT VISIBLE
Revenue attribution detailed    → Revenue attribution summary

11.5 Portal Navigation Addition

// Add to portal-navigation.ts (existing file):
// Under the site section, add:

{
  label: 'Marketing',
  href: '/portal/sites/[siteId]/marketing',
  icon: 'Megaphone',
  permission: 'can_manage_marketing',
  children: [
    { label: 'Overview', href: '/marketing' },
    { label: 'Campaigns', href: '/marketing/campaigns' },
    { label: 'Sequences', href: '/marketing/sequences' },
    { label: 'Subscribers', href: '/marketing/subscribers' },
    { label: 'Analytics', href: '/marketing/analytics' },
    { label: 'Forms', href: '/marketing/forms' },
  ],
}

11.6 Phase MKT-11 Deliverables Checklist

- [ ] Portal marketing hub page (simplified)
- [ ] Portal campaign list and create pages
- [ ] Portal campaign report page
- [ ] Portal sequence list page (view/pause only)
- [ ] Portal subscriber list page
- [ ] Portal analytics overview page
- [ ] Portal forms page with embed codes
- [ ] Permission gating on all portal marketing routes
- [ ] Portal navigation updated with marketing section
- [ ] Simplified settings page (from name, footer text)
- [ ] Zero TypeScript errors
- [ ] Successful build

Phase MKT-12: Social Media Integration

Priority: MEDIUM — Cross-channel marketing coordination Estimated Complexity: Medium Dependencies: Phase MKT-05 (hub), Phase MKT-07 (blog enhancement) Files Created: ~15 new files Files Modified: 2-3 existing files

12.1 Overview

Social media integration adds:

  • Social media account connections (Facebook, Instagram, Twitter/X, LinkedIn)
  • Post scheduling from the marketing dashboard
  • Blog post auto-sharing to social channels
  • Social media content calendar
  • Basic social analytics (click-throughs from social links)
  • Social sharing widgets for storefront/blog pages

Scope Note: This is NOT a full social media management tool (like Hootsuite). It focuses on marketing coordination — scheduling posts that support email campaigns and blog content.

12.2 Social Account Connections

// social-connections.ts
// OAuth-based connection to social media platforms

interface SocialConnection {
  id: string;
  site_id: string;
  platform: "facebook" | "instagram" | "twitter" | "linkedin";
  account_name: string;
  access_token: string; // Encrypted in database
  refresh_token?: string;
  token_expires_at?: string;
  page_id?: string; // Facebook Page ID
  connected_at: string;
  status: "active" | "expired" | "disconnected";
}

// Connection flow:
// 1. User clicks "Connect Facebook" in marketing settings
// 2. OAuth redirect to platform's authorization page
// 3. User grants permissions
// 4. Callback stores tokens in social_connections table
// 5. Account appears as connected
//
// Token refresh: cron job checks for expiring tokens and refreshes

12.3 Social Post Scheduling

Route: /dashboard/sites/[siteId]/marketing/social
// Social post creation:
interface SocialPost {
  id: string;
  site_id: string;
  content: string; // Post text
  media_urls?: string[]; // Attached images/videos
  platforms: SocialPlatform[]; // Which platforms to post to
  scheduled_at?: string; // When to post (null = draft)
  published_at?: string; // When actually posted
  status: "draft" | "scheduled" | "published" | "failed";
  link_url?: string; // Link to include
  utm_params?: UTMParams; // Auto-appended to links
  campaign_id?: string; // Associated email campaign (for coordination)
  blog_post_id?: string; // Associated blog post (for auto-sharing)
}

// Features:
// - Character count per platform (280 for Twitter, 2200 for Instagram, etc.)
// - Platform-specific preview
// - Multi-platform posting (same content, adjusted per platform)
// - Image/media attachment via existing upload system
// - Schedule with date/time picker
// - Auto-append UTM parameters to links
// - "Share this campaign" — generates social post from email campaign content
// - "Share this blog post" — generates social post from blog post

12.4 Content Calendar

Route: /dashboard/sites/[siteId]/marketing/calendar
Unified marketing calendar showing:
- Email campaigns (scheduled)
- Drip sequence emails
- Social media posts
- Blog posts (scheduled)
- Landing page launches

Calendar view: Month | Week | Day
Color coding by channel:
  📧 Blue = Email campaign
  📱 Green = Social post
  📝 Purple = Blog post
  🔄 Orange = Sequence email
  📄 Gray = Landing page

Drag to reschedule social posts.
Click to view/edit any item.

12.5 Auto-Share Blog Posts

// blog-auto-share.ts
// When a blog post is published, automatically create/schedule social posts

export async function autoShareBlogPost(
  blogPostId: string,
  siteId: string,
): Promise<void> {
  const post = await getBlogPost(blogPostId);
  const connections = await getActiveSocialConnections(siteId);

  for (const connection of connections) {
    // Generate platform-specific content
    const content = await generateSocialContent(post, connection.platform);

    await createSocialPost({
      site_id: siteId,
      content,
      platforms: [connection.platform],
      link_url: post.url,
      utm_params: {
        utm_source: connection.platform,
        utm_medium: "social",
        utm_campaign: "blog-auto-share",
      },
      blog_post_id: blogPostId,
      status: "scheduled",
      scheduled_at: new Date().toISOString(), // Post immediately, or stagger
    });
  }
}

// AI generates platform-appropriate captions:
async function generateSocialContent(
  post: BlogPost,
  platform: SocialPlatform,
): Promise<string> {
  const constraints = {
    twitter: { maxLength: 280, style: "concise with hashtags" },
    facebook: { maxLength: 500, style: "engaging and conversational" },
    instagram: { maxLength: 2200, style: "descriptive with hashtags" },
    linkedin: { maxLength: 1300, style: "professional and insightful" },
  };

  return generateWithAI({
    model: "haiku",
    prompt: `Write a ${platform} post promoting this blog article.
      Title: ${post.title}
      Summary: ${post.excerpt}
      Style: ${constraints[platform].style}
      Max length: ${constraints[platform].maxLength} characters.
      Include relevant hashtags.`,
    maxTokens: 300,
  });
}

12.6 Social Sharing Widgets

// Storefront/Blog components:
// - ShareButtons.tsx — Social share buttons on blog posts and products
// - SocialFollow.tsx — "Follow us" icons linking to social profiles
//
// These pull connected social account URLs from marketing settings
// and render branded share/follow buttons

12.7 Phase MKT-12 Deliverables Checklist

- [ ] Social connection OAuth flows (Facebook, Twitter, LinkedIn, Instagram)
- [ ] Social connections settings page
- [ ] Token storage (encrypted) and refresh logic
- [ ] Social post creation UI
- [ ] Multi-platform posting with per-platform preview
- [ ] Social post scheduling
- [ ] Content calendar page (unified view)
- [ ] Calendar view with month/week/day modes
- [ ] Blog auto-share to connected social accounts
- [ ] AI social caption generation per platform
- [ ] Social sharing widgets for storefront
- [ ] UTM auto-append on social links
- [ ] Campaign-to-social content generation
- [ ] Basic social click-through tracking
- [ ] Zero TypeScript errors
- [ ] Successful build

Cross-Module Integration Reference

This section documents every integration point between the marketing module and existing platform modules. Implementing agents MUST read this before modifying any existing file.

Existing Files to Modify (Complete List)

The following existing files require modifications. Changes are ADDITIVE only — never remove existing code.

1. Module Registry

File: src/modules/core/module-registry.ts (or wherever CORE_MODULE_SLUGS is defined)

Change: Add "marketing" to CORE_MODULE_SLUGS array
Impact: Marketing module auto-installs when a new site is created
Pattern: Follow exactly how other modules (crm, ecommerce, blog, etc.) are registered

2. Roles & Permissions

File: src/lib/roles.ts

Change: Add 'manage_marketing' to agency role permissions array
Impact: Agency users can access marketing features
Pattern: Copy how 'manage_crm' or 'manage_ecommerce' is added

3. Portal Permissions

File: src/lib/portal-permissions.ts

Change: Add 'can_manage_marketing' to available portal permissions
Impact: Client portal users can be granted marketing access
Pattern: Copy how 'can_manage_ecommerce' is added

4. Dashboard Navigation

File: src/config/navigation.ts (or wherever dashboard nav is configured)

Change: Add marketing nav section with sub-items
Impact: Marketing appears in dashboard sidebar
Items: Hub, Campaigns, Sequences, Templates, Landing Pages, Forms, Analytics, Settings
Pattern: Copy how CRM or E-Commerce nav section is structured

5. Portal Navigation

File: src/config/portal-navigation.ts

Change: Add marketing nav section (gated by can_manage_marketing)
Impact: Marketing appears in portal sidebar for permitted users
Items: Overview, Campaigns, Sequences, Subscribers, Analytics, Forms
Pattern: Copy how other gated portal sections work

6. Admin Navigation

File: src/config/admin-navigation.ts (or admin layout)

Change: Add marketing health/overview link
Impact: Super admins see platform marketing health

7. Automation Event Registry

File: src/modules/automation/data/event-types.ts

Change: Add 18 new marketing.* event types to EVENT_REGISTRY
Impact: Automation workflows can trigger on marketing events
Events listed in Phase MKT-04, Section 4.7

8. Automation Action Types

File: src/modules/automation/data/action-types.ts

Change: Add 8 new marketing.* action types to ACTION_TYPES
Impact: Automation workflows can perform marketing actions
Actions listed in Phase MKT-04, Section 4.7

9. Automation System Templates

File: src/modules/automation/data/system-templates.ts

Change: Add 15 marketing automation workflow templates
Impact: Users see marketing templates in automation template picker
Templates listed in Phase MKT-04, Section 4.8

10. CRM Contact Model

Database migration — new columns on mod_crm01_contacts:

ALTER TABLE mod_crm01_contacts ADD COLUMN email_subscribed BOOLEAN DEFAULT true;
ALTER TABLE mod_crm01_contacts ADD COLUMN email_subscribed_at TIMESTAMPTZ;
ALTER TABLE mod_crm01_contacts ADD COLUMN email_unsubscribed_at TIMESTAMPTZ;
ALTER TABLE mod_crm01_contacts ADD COLUMN email_bounced_at TIMESTAMPTZ;
ALTER TABLE mod_crm01_contacts ADD COLUMN marketing_engagement_score INTEGER DEFAULT 0;
ALTER TABLE mod_crm01_contacts ADD COLUMN first_touch_utm JSONB;
ALTER TABLE mod_crm01_contacts ADD COLUMN last_touch_utm JSONB;
ALTER TABLE mod_crm01_contacts ADD COLUMN phone_number TEXT;
ALTER TABLE mod_crm01_contacts ADD COLUMN sms_subscribed BOOLEAN DEFAULT false;
ALTER TABLE mod_crm01_contacts ADD COLUMN whatsapp_subscribed BOOLEAN DEFAULT false;

11. Resend Webhook Handler

File: src/app/api/webhooks/resend/route.ts (if it exists)

Change: Add marketing email event handling alongside transactional
Impact: Opens, clicks, bounces, complaints tracked for marketing emails
Detection: Check for X-Campaign-Id header to distinguish marketing vs transactional
If this file doesn't exist: Create it fresh with both marketing and transactional handling

12. Vercel Config

File: next-platform-dashboard/vercel.json

Change: Add cron job entry for marketing scheduler
Addition: { "path": "/api/cron/marketing-scheduler", "schedule": "*/5 * * * *" }
Impact: Scheduled campaigns, sequence steps, and A/B tests process every 5 minutes

13. E-Commerce Order Hook

File: Where e-commerce order completion is processed

Change: After order completes, call attributeRevenue() to check for marketing attribution
Impact: Revenue from marketing-driven sales is tracked
Pattern: Fire-and-forget — don't block order process on attribution

14. Blog Post Publishing Hook

File: Where blog post status changes to "published"

Change: After publish, optionally trigger social auto-share
Impact: Blog posts auto-shared to connected social accounts
Condition: Only if auto-share is enabled in marketing settings

Testing Requirements

Unit Tests

For each phase, create unit tests covering:

Phase MKT-01:
- Module registration includes 'marketing'
- Permission checks pass/fail correctly
- Settings CRUD operations

Phase MKT-02:
- Campaign CRUD operations
- Audience resolver (all 4 types: all, segment, tags, custom)
- Personalization token replacement (all tokens)
- HTML safety (XSS prevention in personalization)
- A/B variant selection (consistent for same contact)
- Batch splitting logic
- Unsubscribe token generation and verification

Phase MKT-03:
- Tracking token encode/decode
- URL validation for click tracking (block javascript:, data:, localhost)
- Engagement score calculation (all scenarios)
- CSV export formatting
- Campaign stat increment atomicity

Phase MKT-04:
- Sequence step execution order
- Delay calculation (minutes, hours, days, weeks)
- Condition evaluation (opened, clicked, tag, field)
- Enrollment deduplication
- Exit condition checking

Phase MKT-05:
- Analytics date range filtering
- Revenue attribution (within/outside window)
- AI insights prompt construction

Phase MKT-06:
- Form submission validation
- Rate limiting
- CORS headers
- UTM parameter extraction
- Landing page slug resolution

Phase MKT-07:
- Blog to email content conversion
- SEO score calculation

Phase MKT-08:
- SMS character counting
- Phone number validation
- Provider abstraction interface compliance

Phase MKT-09:
- AI prompt construction (all generators)
- Response parsing (JSON from AI)

Phase MKT-10:
- Platform health calculation
- Auto-pause triggers
- Sending limit enforcement

Phase MKT-11:
- Portal permission gating
- Simplified view data filtering

Phase MKT-12:
- Social post character limits per platform
- UTM parameter appending
- OAuth token refresh logic

Integration Tests

Test flows that span multiple modules:

1. Full Campaign Flow:
   Create campaign → set audience → design email → send →
   verify emails sent → verify tracking works → verify stats update

2. Sequence Enrollment Flow:
   Create sequence → activate → trigger event →
   verify enrollment → verify step execution → verify completion

3. Form to Sequence Flow:
   Submit opt-in form → verify CRM contact created →
   verify tags added → verify sequence enrollment →
   verify first email sent

4. Revenue Attribution Flow:
   Send campaign → contact clicks link → contact makes purchase →
   verify revenue attributed to campaign

5. Blog to Email Flow:
   Create blog post → convert to email → send campaign →
   verify content matches

6. Unsubscribe Flow:
   Contact receives email → clicks unsubscribe → confirms →
   verify contact unsubscribed → verify no more emails sent to contact

7. Bounce/Complaint Flow:
   Send email → webhook fires bounce → verify contact marked bounced →
   verify excluded from future sends

8. Portal Permission Flow:
   Client user without marketing permission → verify cannot access →
   Grant permission → verify can access → verify sees simplified views

E2E Tests (Playwright)

Key user journeys to test:

1. Agency creates and sends email campaign
2. Agency sets up drip sequence
3. Agency views campaign analytics
4. Client portal user views marketing dashboard
5. Visitor submits opt-in form on landing page
6. Visitor clicks tracking link and gets redirected
7. User unsubscribes via email link
8. Agency creates landing page and publishes it
9. Agency connects social account and schedules post

Implementation Sequence Summary

Phase MKT-01  → Database & Registration (FOUNDATION — do first)
     ↓
Phase MKT-02  → Email Campaign Engine (CORE — most code)
     ↓
Phase MKT-03  → Analytics & Tracking (depends on MKT-02)
     ↓
Phase MKT-04  → Drip Sequences & Automation (depends on MKT-02 + 03)
     ↓
Phase MKT-05  → Marketing Hub Dashboard (needs data from MKT-02-04)
     ↓
Phase MKT-06  → Landing Pages & Forms (independent, needs MKT-01)
     ↓
Phase MKT-07  → Blog Enhancement (needs MKT-02 + 06)
     ↓
Phase MKT-08  → SMS & WhatsApp (extends MKT-04 sequences)
     ↓
Phase MKT-09  → AI Intelligence (needs data from MKT-02-05)
     ↓
Phase MKT-10  → Super Admin Views (needs MKT-02-03 data)
     ↓
Phase MKT-11  → Client Portal Views (mirrors MKT-05 + 02)
     ↓
Phase MKT-12  → Social Media Integration (needs MKT-05 + 07)

Phases that can be parallelized:

  • MKT-06 can start alongside MKT-03 (both need only MKT-01)
  • MKT-10 and MKT-11 can be done in parallel (both read, don't overlap)
  • MKT-08 and MKT-09 can be done in parallel (different feature areas)

Session Handoff Protocol

When starting a new session to implement a phase:

  1. Read memory bank files (required):

    • /memory-bank/projectbrief.md
    • /memory-bank/systemPatterns.md
    • /memory-bank/techContext.md
    • /memory-bank/activeContext.md
    • /memory-bank/progress.md
  2. Read this guide — at minimum, read:

    • The architecture overview (Section 0)
    • The specific phase section you're implementing
    • The cross-module integration section
    • The testing requirements for your phase
  3. Check previous phase completion:

    • Look for completed phase deliverables
    • Verify database tables exist (if needed)
    • Run TypeScript check to ensure clean baseline
  4. Implement the phase:

    • Follow the specification exactly
    • Use existing platform patterns (snake_case mapping, auth guards, etc.)
    • Fire automation events where specified
    • Check permissions on all routes
  5. After completion:

    • Run tsc --noEmit — zero errors required
    • Run next build — must succeed
    • Update /memory-bank/activeContext.md with completed phase
    • Update /memory-bank/progress.md with marketing module status

End of PHASE-MKT-MASTER-GUIDE.md Total Phases: 12 | Estimated New Files: ~160 | Estimated Modified Files: ~14

Related Documents