Build a complete email marketing system with drag-and-drop editor, subscriber management, and campaign analytics
* Preview is for reference only. Actual results may vary depending on the AI model, variable values, and tools used.
You are an expert full-stack developer specializing in email marketing tools for small and medium enterprises. Build a complete Email Newsletter Builder & Campaign Manager web application tailored for Malaysian SME owners who need a cost-effective alternative to Mailchimp or MailerLite. The system must handle subscriber management, visual email composition, campaign scheduling, delivery via Resend API, and detailed performance analytics — all compliant with Malaysia's PDPA.
## Tech Stack
- Framework: Next.js 14 (App Router) with TypeScript
- Styling: Tailwind CSS + shadcn/ui (new-york style, slate base)
- Database & Auth: Supabase (Postgres + Row Level Security)
- Email Delivery: Resend API (resend.com)
- File Handling: Papaparse for CSV import/export
- Rich Editor: Custom block-based builder (no heavy dependencies)
- Hosting: Vercel
## Database Schema
Create the following Supabase tables with full RLS policies:
### subscriber_lists
- id: uuid primary key default gen_random_uuid()
- name: text not null
- description: text
- created_at: timestamptz default now()
- owner_id: uuid references auth.users
- subscriber_count: integer default 0 (denormalized, updated by trigger)
### subscribers
- id: uuid primary key default gen_random_uuid()
- list_id: uuid references subscriber_lists(id) on delete cascade
- email: text not null
- name: text
- tags: text[] default '{}'
- status: text check (status in ('active','unsubscribed','bounced','complained')) default 'active'
- subscribed_at: timestamptz default now()
- unsubscribed_at: timestamptz
- double_opt_in_confirmed: boolean default false
- double_opt_in_sent_at: timestamptz
- metadata: jsonb default '{}'
- Create index: idx_subscribers_list_id on subscribers(list_id)
- Create index: idx_subscribers_email on subscribers(email)
- Create index: idx_subscribers_status on subscribers(status)
- Unique constraint: (list_id, email)
### email_templates
- id: uuid primary key default gen_random_uuid()
- name: text not null
- description: text
- blocks: jsonb not null default '[]' (array of block objects)
- thumbnail_url: text
- is_system: boolean default false
- created_at: timestamptz default now()
- owner_id: uuid references auth.users
### campaigns
- id: uuid primary key default gen_random_uuid()
- subject: text not null
- preview_text: text
- from_name: text not null
- from_email: text not null
- reply_to: text
- list_id: uuid references subscriber_lists(id)
- template_id: uuid references email_templates(id)
- body_html: text (rendered HTML at send time)
- body_blocks: jsonb (block editor state)
- status: text check (status in ('draft','scheduled','sending','sent','cancelled')) default 'draft'
- scheduled_at: timestamptz
- sent_at: timestamptz
- ab_test_enabled: boolean default false
- ab_subject_b: text
- ab_winner_criteria: text check (ab_winner_criteria in ('open_rate','click_rate'))
- created_at: timestamptz default now()
- owner_id: uuid references auth.users
- Create index: idx_campaigns_status on campaigns(status)
- Create index: idx_campaigns_scheduled_at on campaigns(scheduled_at)
### campaign_stats
- id: uuid primary key default gen_random_uuid()
- campaign_id: uuid references campaigns(id) on delete cascade unique
- total_sent: integer default 0
- total_delivered: integer default 0
- total_opens: integer default 0
- unique_opens: integer default 0
- total_clicks: integer default 0
- unique_clicks: integer default 0
- bounces: integer default 0
- unsubscribes: integer default 0
- complaints: integer default 0
- open_rate: numeric(5,2) generated always as (case when total_sent > 0 then round(unique_opens::numeric / total_sent * 100, 2) else 0 end) stored
- click_rate: numeric(5,2) generated always as (case when total_sent > 0 then round(unique_clicks::numeric / total_sent * 100, 2) else 0 end) stored
- updated_at: timestamptz default now()
### campaign_events
- id: uuid primary key default gen_random_uuid()
- campaign_id: uuid references campaigns(id) on delete cascade
- subscriber_id: uuid references subscribers(id) on delete set null
- event_type: text check (event_type in ('sent','delivered','open','click','bounce','unsubscribe','complaint'))
- link_url: text
- occurred_at: timestamptz default now()
- ip_address: text
- user_agent: text
- Create index: idx_campaign_events_campaign_id on campaign_events(campaign_id)
- Create index: idx_campaign_events_subscriber_id on campaign_events(subscriber_id)
- Create index: idx_campaign_events_event_type on campaign_events(event_type)
### campaign_links
- id: uuid primary key default gen_random_uuid()
- campaign_id: uuid references campaigns(id) on delete cascade
- original_url: text not null
- tracking_token: text not null unique default encode(gen_random_bytes(12), 'hex')
- click_count: integer default 0
- Create index: idx_campaign_links_token on campaign_links(tracking_token)
## Core Features to Build
### 1. Subscriber List Management
- CRUD for subscriber lists with per-list subscriber counts
- Add individual subscribers via form (email, name, tags)
- Bulk import via CSV upload using Papaparse (map columns: email, name, tags)
- Export list to CSV with all subscriber fields
- Filter subscribers by status (active, unsubscribed, bounced) and tags
- Tag management: add/remove tags from subscribers, bulk tag operations
- Subscriber detail page showing campaign history and engagement
- Double opt-in flow: send confirmation email via Resend, confirm via tokenized link
### 2. Block-Based Email Template Builder
- Block types: Header (logo + brand color), Hero (full-width image + heading + CTA), Text (rich paragraph), Image (centered or full-width), Button (CTA with custom label, URL, color), Two-Column (product grid with image, name, price in RM), Divider, Footer (social links, unsubscribe, company address)
- Left sidebar: block palette with drag-to-add
- Center canvas: rendered email preview (600px max-width, white background)
- Right sidebar: selected block properties (text, colors, image URL, link URL)
- Real-time HTML generation from blocks array
- Save as reusable template with name and thumbnail
- Mobile preview toggle (320px preview mode)
- Auto-inject unsubscribe link into every Footer block (PDPA mandatory)
- Auto-inject tracking pixel (1x1 transparent PNG) before closing body tag
### 3. Campaign Creation & Scheduling
- Multi-step campaign wizard: Settings → Design → Review → Schedule/Send
- Step 1 Settings: campaign name, from name, from email ({{company_name}} default), reply-to, subject line, preview text, select subscriber list
- Step 2 Design: choose saved template or build from scratch in block editor
- Step 3 Review: preview desktop/mobile, estimated recipients, spam score warning
- Step 4: Send now or schedule for specific date/time (Malaysia timezone UTC+8)
- A/B subject line test: enter Subject A and Subject B, split 20% of list for test, send winner to remaining 80% after 4 hours based on open rate or click rate
- Send test email to a specified address before sending to full list
### 4. Email Delivery via Resend
- Server Action: sendCampaign(campaignId) — fetch active subscribers, render HTML blocks to final HTML, wrap all links with tracking URLs (/api/track/click?token=...), inject tracking pixel (/api/track/open?cid=...&sid=...), batch send via Resend with rate limiting (100 emails per second max)
- Use Resend batch send endpoint for efficiency
- Store each send as campaign_event with type 'sent'
- Handle Resend webhooks: delivered, bounced, complained — update subscriber status and campaign_stats
### 5. Open & Click Tracking
- Open tracking: GET /api/track/open?cid=[campaignId]&sid=[subscriberId] — return 1x1 transparent GIF, insert campaign_event(open), increment campaign_stats.total_opens and unique_opens
- Click tracking: GET /api/track/click?token=[trackingToken]&sid=[subscriberId] — lookup campaign_links by token, insert campaign_event(click), increment click counts, redirect to original_url
- One-click unsubscribe: GET /api/unsubscribe?token=[token] — verify HMAC token, set subscriber status to 'unsubscribed', record unsubscribed_at, show confirmation page (no login required)
- List-Unsubscribe header on all outgoing emails (RFC 8058 compliance)
### 6. PDPA Compliance (Malaysia Personal Data Protection Act)
- Double opt-in option per list (configurable)
- Unsubscribe link mandatory in every email (auto-injected)
- Suppression list: permanently unsubscribed emails never receive mail again
- Data export: subscribers can request their data (admin exports on their behalf)
- Retention policy note in admin UI (recommend deleting inactive subscribers after 2 years)
- Privacy policy URL field in campaign settings
- Physical address or P.O. Box required in footer (CAN-SPAM/PDPA alignment)
### 7. Campaign Analytics Dashboard
- Summary cards: Total Subscribers, Total Campaigns Sent, Average Open Rate, Average Click Rate
- Campaign list table: subject, status, sent date, recipients, open rate %, click rate %, unsubscribes — sortable columns
- Per-campaign detail: open rate timeline chart (opens over 48 hours), top clicked links table, subscriber engagement breakdown (opened, clicked, unsubscribed, bounced)
- Subscriber growth chart: new subscribers per week for past 12 weeks
- Best send time recommendation: show heatmap of open rates by day of week and hour
- A/B test results: side-by-side comparison of Subject A vs Subject B performance
## UI/UX Design
- Top navigation: dark navy (#0F172A) with brand logo and nav links (Campaigns, Subscribers, Templates, Analytics)
- Campaigns list page: Table with status badges (Draft=slate, Scheduled=blue, Sent=green), quick action dropdown (Edit, Duplicate, View Stats, Delete)
- Block editor: three-panel layout — block palette left (160px), canvas center (fluid), properties panel right (280px)
- Subscriber management: data table with search, filter chips for status and tags, bulk action toolbar (tag, export, delete)
- Analytics: card grid at top, chart below using Recharts, campaign comparison table
- Empty states with clear CTA: 'Import your first subscribers' button on empty list
- Loading skeletons for all data-heavy tables and charts
- Toast notifications for send success, import complete, errors
- Responsive: desktop-first but usable on tablet for quick checks
## Variables
- Company/Brand Name: {{company_name}} (default: 时尚坊 / Fashion Hub)
- Sender Email: {{sender_email}} (default: hello@fashionhub.my)
- Max Subscribers: {{subscriber_limit}} (default: 5000)
- Email Provider: {{email_provider}} (default: Resend)
## Deliverables
Generate the complete application including all route files, Supabase schema SQL, Resend integration, tracking API routes, block editor component, and campaign analytics. Include .env.example with NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, RESEND_API_KEY, UNSUBSCRIBE_HMAC_SECRET. The system must be production-ready for a Malaysian SME sending up to {{subscriber_limit}} subscribers per campaign.