Marketing Features
Complete guide to audience management, campaigns, email marketing, promo codes, and conversion surfaces

1. Executive Overview
What It Does
The Marketing Features module provides a comprehensive marketing, outreach, and audience engagement system for the Hyphen Publishing Platform. It enables publication administrators to:
- Build and manage audience contact lists from CSV imports, campaign submissions, and newsletter signups
- Create targeted user segments with advanced filtering across both registered readers and external audience contacts
- Launch outreach campaigns with customizable landing pages, dynamic forms, event content, incentives, and social integration
- Send email campaigns to segmented audiences using templated emails with engagement tracking
- Manage newsletter editions authored in Strapi CMS, sent as email campaigns, and archived for readers
- Create and distribute promotional codes with configurable discount types and validity rules
- Deploy conversion surfaces (modals, banners, slide-ins) with targeting rules and trigger-based activation on the Reader Portal
- Track engagement analytics across campaigns, email deliveries, and conversion surfaces with integrated GA4 and internal metrics
Who Uses It
| Role | Access Level |
|---|---|
| Marketing Admin | Full access: create, edit, publish, delete campaigns; manage audience, segments, email campaigns, promo codes, conversion surfaces |
| Editor / Content Manager | Campaign read access; may publish campaigns depending on RBAC |
| Viewer | Read-only access to campaigns and metrics |
| Reader (Public) | Accesses campaign landing pages, newsletter archive, receives emails/notifications |
Business Problem It Solves
Publications need to grow their readership, retain subscribers, and promote events/content. This module provides a unified platform to:
- Acquire new readers through outreach campaigns (student programs, events, lead generation)
- Convert anonymous visitors to subscribers through strategically-placed conversion surfaces
- Engage existing audiences through segmented email campaigns and newsletters
- Track marketing ROI through integrated analytics across all channels
2. Feature Scope
In Scope
| Feature | Status |
|---|---|
| Audience Contact Management (CRUD, CSV import/export) | Implemented |
| User Segment Builder (advanced multi-source filtering) | Implemented |
| Outreach Campaign CRUD (7-step wizard, 5+ campaign types) | Implemented |
| Campaign Landing Pages (reader portal, template-based) | Implemented |
| Campaign Publishing Workflow (draft → active → completed → archived) | Implemented |
| Campaign Submissions & Review (approval/rejection with email) | Implemented |
| Campaign Invitations (segment-targeted email invitations) | Implemented |
| Email Campaigns (template-based, scheduled/immediate send) | Implemented |
| Newsletter Editions (Strapi CMS content, send-as-campaign bridge) | Implemented |
| Promotional Codes (percentage/fixed/trial_days, auto-generation) | Implemented |
| Conversion Surfaces (6 surface types, 7 trigger types, targeting) | Implemented |
| Campaign Types (admin-manageable, seeded from enum) | Implemented |
| Social Media Integration (social post creation from campaigns) | Implemented |
| QR Code Generation (for campaign landing pages) | Implemented |
| Campaign Statistics & Analytics | Implemented |
| Email Engagement Webhooks (SendGrid, Mailchimp) | Implemented |
| Cron-based Automation (scheduled emails, campaign lifecycle, invitations) | Implemented |
Out of Scope
- A/B testing for conversion surfaces (planned for future)
- Geographic targeting for conversion surfaces (planned)
- Webhook events for campaign lifecycle (planned)
- Content-aware inline embed positioning (planned)
- Admin preview mode for conversion surfaces (planned)
Related Modules / Dependencies
| Module | Dependency Type |
|---|---|
| User & Subscription Management (04) | Segments query Reader and Subscription models; promo codes create subscriptions |
| Admin Settings & RBAC (11) | All marketing endpoints enforce permission-based access control |
| Notifications & Email System (14) | Email campaigns use shared email service (SendGrid/SMTP); webhooks update delivery metrics |
| Reader Portal (13) | Campaign landing pages, newsletter archive, conversion surfaces render here |
| Social Media Management (08) | Campaigns link to SocialCampaign for cross-channel posting |
| Homepage Layout & Templates (03) | Campaign pages extend the page template system (DynamicTemplate, SectionRenderer) |
| E-Commerce / Shopify (09) | Conversion surfaces can link to products |
| Strapi CMS | Newsletter editions are authored and stored in Strapi |
3. Roles & Permissions
Marketing Permissions
| Permission Key | Description | Required For |
|---|---|---|
MARKETING_SEGMENTS_READ | View segments | Segments list, segment detail |
MARKETING_SEGMENTS_CREATE | Create segments | New segment |
MARKETING_SEGMENTS_UPDATE | Edit segments | Edit segment filters |
MARKETING_SEGMENTS_DELETE | Delete segments | Remove unused segments |
MARKETING_CAMPAIGNS_READ | View campaigns, audience, promo codes | Campaign list, detail, audience list |
MARKETING_CAMPAIGNS_CREATE | Create outreach campaigns | New campaign wizard |
MARKETING_CAMPAIGNS_UPDATE | Edit campaigns, publish/unpublish | Edit campaign, status transitions |
MARKETING_CAMPAIGNS_DELETE | Delete campaigns | Remove campaigns |
MARKETING_SUBMISSIONS_READ | View submissions | Submissions tab |
MARKETING_SUBMISSIONS_REVIEW | Approve/reject submissions | Submission approval workflow |
MARKETING_SUBMISSIONS_EXPORT | Export submission data | CSV export |
MARKETING_EMAIL_CAMPAIGNS_READ | View email campaigns | Email campaign list |
MARKETING_EMAIL_CAMPAIGNS_CREATE | Create email campaigns | New email campaign |
MARKETING_EMAIL_CAMPAIGNS_UPDATE | Edit email campaigns | Edit draft campaigns |
MARKETING_EMAIL_CAMPAIGNS_DELETE | Delete email campaigns | Remove unsent campaigns |
MARKETING_EMAIL_CAMPAIGNS_SEND | Send/schedule email campaigns | Send action |
Additional Related Permissions
| Permission Key | Used By |
|---|---|
SETTINGS_READ | View email templates and preferences |
SETTINGS_UPDATE | Create/edit email templates and preferences |
RBAC Configuration
Permissions are assigned to roles in Admin Console → Settings → Roles & Permissions. Each role can be granted specific marketing permissions. The Marketing Hub page itself controls card visibility based on permissions — users without MARKETING_CAMPAIGNS_READ will not see the Campaigns card.
4. Architecture & Design Overview
System Architecture
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN CONSOLE │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌─────────────┐ │
│ │ Marketing│ │ Email │ │ Segments │ │ Conversion │ │
│ │ Hub │ │ Campaigns │ │ Builder │ │ Surfaces │ │
│ └────┬─────┘ └──────┬───────┘ └─────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ ┌────┴──────────────┴───────────────┴───────────────┴──────┐ │
│ │ /api/marketing/* (REST APIs) │ │
│ └────┬──────────────┬───────────────┬───────────────┬──────┘ │
│ │ │ │ │ │
│ ┌────┴──────────────┴───────────────┴───────────────┴──────┐ │
│ │ Prisma ORM (PostgreSQL) │ │
│ │ OutreachCampaign | EmailCampaign | UserSegment | │ │
│ │ AudienceContact | ConversionSurface | etc. │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────┴────┐ ┌───────────┐ ┌──────────────┐ │
│ │ Strapi │ │ SendGrid/ │ │ S3 Storage │ │
│ │ CMS │ │ SMTP │ │ (uploads) │ │
│ └─────────┘ └───────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ READER PORTAL │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Campaign │ │ Newsletter │ │ Conversion Surface │ │
│ │ Landing │ │ Archive │ │ Renderer (global) │ │
│ │ Pages │ │ Pages │ │ Modals/Banners/etc. │ │
│ └──────────────┘ └──────────────┘ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Key Entities & Models
| Model | Purpose | Storage |
|---|---|---|
OutreachCampaign | Outreach campaigns with landing page config, form fields, event details | Prisma |
CampaignSubmission | Form submissions from campaign landing pages | Prisma |
CampaignInvitation | Email invitations sent to segments for campaigns | Prisma |
CampaignType | Admin-manageable campaign type definitions | Prisma |
CampaignNotifyRequest | "Notify me" requests from expired campaign visitors | Prisma |
UserSegment | Audience segment with JSON filter criteria | Prisma |
AudienceContact | External contacts imported via CSV or created from submissions | Prisma |
EmailCampaign | Email campaigns with template, segment targeting, delivery metrics | Prisma |
EmailTemplate | Reusable email templates with Handlebars variable support | Prisma |
PromotionalCode | Auto-generated promo codes linked to campaigns | Prisma |
DiscountCoupon | Admin-created reusable discount codes | Prisma |
ConversionSurface | Promotional surfaces (modals, banners) with targeting/trigger config | Prisma |
ConversionEvent | Interaction events (impression, click, dismiss, conversion) | Prisma |
NewsletterEdition | Newsletter content editions | Prisma (local mirror) + Strapi (authoring) |
Workflow / State Models
Outreach Campaign Status Flow:
draft ──publish──→ active ──unpublish──→ paused ──publish──→ active
│ │
complete archive
│ │
▼ ▼
completed ──archive──→ archivedEmail Campaign Status Flow:
draft ──schedule──→ scheduled ──cron trigger──→ sending ──→ sent
│ │
└──send now──→ sending ──────────────────────→ sent/failedCampaign Submission Status Flow:
pending_review ──approve──→ approved
│ │
├──reject───→ rejected └──generate promo──→ (with PromotionalCode)
│
└──request info──→ needs_info ──resubmit──→ pending_reviewConversion Surface Status Flow:
draft ──activate──→ active ──pause──→ paused ──activate──→ active
│ │ │
└──schedule──→ scheduled archive
│ │
└──cron──→ active ▼
│ archived
expires
│
▼
expired5. Prerequisites & Setup Requirements
Critical Prerequisites (Must be configured before marketing features work)
1. Email Provider Configuration
Marketing relies heavily on email delivery. Without a configured email provider, email campaigns, invitations, and submission notifications will fail.
| Setting | Location | Required |
|---|---|---|
SENDGRID_API_KEY | Environment variable | Yes (if using SendGrid) |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS | Environment variable | Yes (if using SMTP) |
EMAIL_FROM_ADDRESS | Environment variable | Yes |
EMAIL_FROM_NAME | Environment variable | Yes |
Verify: Admin Console → Settings → Email → SMTP Status. Should show configured: true.
2. Email Templates
Campaign-related emails (approval, rejection, invitation, notification) require email templates to be seeded.
Action: Seed default templates by calling POST /api/marketing/email-templates/seed or creating them manually in Admin Console → Settings → Email Templates.
Required template keys:
campaign_submission_approvedcampaign_submission_rejectedcampaign_invitationcampaign_notify_new_campaign
3. Email Preferences & Branding
Configure email branding (logo, colors, publication name, footer, unsubscribe link) in Admin Console → Settings → Email Preferences to ensure all outgoing emails look professional and comply with CAN-SPAM requirements.
4. Strapi CMS Configuration (for Newsletters only)
Newsletter editions are authored in Strapi. Required settings:
| Setting | Location | Required For |
|---|---|---|
STRAPI_URL | Environment variable | Newsletter CRUD |
STRAPI_API_TOKEN | Environment variable | Newsletter CRUD |
Newsletter editions content type must exist in Strapi with fields: title, slug, previewText, content, plainTextContent, editionNumber, topic, publishDate, coverImage, status, isFeatured, priority.
5. S3 Storage Configuration (for file uploads)
Campaign form submissions may include file uploads (selfies, ID cards, documents).
| Setting | Location | Required For |
|---|---|---|
AWS_S3_BUCKET | Environment variable | Campaign file uploads |
AWS_ACCESS_KEY_ID | Environment variable | Campaign file uploads |
AWS_SECRET_ACCESS_KEY | Environment variable | Campaign file uploads |
AWS_REGION | Environment variable | Campaign file uploads |
6. Cron Job Configuration
Automated tasks require the cron system to be running.
| Cron Endpoint | Purpose | Recommended Interval |
|---|---|---|
GET /api/cron/send-scheduled-emails | Send due email campaigns | Every 1-5 minutes |
GET /api/cron/campaign-status | Auto-complete expired campaigns, send notifications | Every 15-30 minutes |
GET /api/cron/send-invitations | Send scheduled invitations | Every 5 minutes |
GET /api/cron/expire-invitations | Expire overdue invitations | Every hour |
GET /api/cron/invitation-reminders | Send weekly invitation reminders | Weekly |
All cron endpoints require: Authorization: Bearer <CRON_SECRET>
7. Webhook Configuration (for email engagement tracking)
To track email opens, clicks, bounces, and unsubscribes:
SendGrid:
- Register webhook URL:
{ADMIN_CONSOLE_URL}/api/webhooks/sendgrid - In SendGrid Dashboard → Settings → Mail Settings → Event Webhook
- Optional: Set
SENDGRID_WEBHOOK_VERIFICATION_KEYfor signature verification
Mailchimp/Mandrill:
- Register webhook URL:
{ADMIN_CONSOLE_URL}/api/webhooks/mailchimp - In Mandrill Dashboard → Settings → Webhooks
- Optional: Set
MAILCHIMP_WEBHOOK_KEYfor HMAC-SHA1 verification
8. Reader Portal URL Configuration
Campaign landing pages render on the Reader Portal. The Admin Console needs to know the Reader Portal URL for preview links and public campaign URLs.
| Setting | Location |
|---|---|
NEXT_PUBLIC_READER_URL | Admin Console environment variable |
NEXT_PUBLIC_ADMIN_API_URL | Reader Portal environment variable (to fetch campaign data from Admin API) |
9. RBAC Permission Assignment
Ensure the appropriate roles have marketing permissions assigned. Navigate to Admin Console → Settings → Roles & Permissions and grant the marketing permission group to roles that need access.
10. GA4 Configuration (for conversion surface analytics)
Conversion surfaces fire custom GA4 events. If you use Google Analytics:
| Setting | Location |
|---|---|
NEXT_PUBLIC_GA_MEASUREMENT_ID | Reader Portal environment variable |
GA4 events tracked: conversion_surface_impression, conversion_surface_click, conversion_surface_dismiss, conversion_surface_conversion, coupon_copied, newsletter_signup, registration_start.
6. Sub-Module: Audience Contacts

Purpose
Manage external audience contacts (people not yet registered as readers) for use in segments, campaign invitations, and email campaigns.
Access
Admin Console → Marketing → Segments → Contacts tab (Also accessible via Marketing → Audience, which redirects to the Contacts tab)
Key Features
6.1 View Contacts
- Table view with columns: Name, Email, Phone, Institution, Tags, Source, Created
- Search by name or email
- Filter by tags
- Sort by any column
- Pagination
6.2 Add Single Contact
- Click "Add Contact" button
- Fill in fields: Email (required), Name, Phone, Institution, Designation, City, State, Country, Tags
- Click Save
- Contact is created with
source: 'manual'
6.3 Bulk Import via CSV
- Click "Upload CSV" button
- Drag-and-drop or browse to select a CSV file
- Column Mapping Step: The system auto-detects columns. Map CSV columns to contact fields (email, name, phone, institution, etc.)
- Preview Step: Review first 5 rows of mapped data
- Click Upload
- System upserts contacts by email (creates new, updates existing)
- Results summary shows: created, updated, skipped, errors
CSV Requirements:
- Must have an
emailcolumn (required) - All other columns are optional
- Duplicate emails are updated (not duplicated)
- Tags can be specified as comma-separated values
6.4 Edit Contact
- Click any contact row to open the Edit Contact modal
- Modify fields and click Save
6.5 Delete Contact
- Select one or more contacts → click Delete in bulk actions
- Or click delete icon on individual row
- Soft delete by default (
isActive = false) - Hard delete available with
?hard=truequery parameter (API only)
6.6 Export Contacts
- Click Export to download all active contacts as CSV
6.7 Auto-Created Contacts
Contacts are automatically created (upserted) from:
- Campaign form submissions: Tagged
campaign_submission, with source detail of campaign name - Newsletter signups: Tagged
newsletter_signup
These auto-created contacts appear in the Contacts tab and can be included in segments.
Business Rules
- Email must be unique across all contacts
- Contacts with
isActive = falseare excluded from segments and email sends - Unsubscribed contacts (
unsubscribedAtset) are excluded from non-forced email sends - A contact can be linked to a Reader record via
readerId(one-to-one)
7. Sub-Module: User Segments

Purpose
Create targeted audience groups by combining filter criteria across both registered readers and audience contacts. Segments are used for email campaigns and campaign invitations.
Access
Admin Console → Marketing → Segments → Segments tab
Key Features
7.1 View Segments
- List view with columns: Name, Member Count, Filters (summarized), Created By, Last Counted
- Search by segment name
- Click any segment to view/edit
7.2 Create Segment
- Click "New Segment"
- Enter segment Name and optional Description
- Build Filters using the sidebar filter groups:
| Filter Group | Filter Options |
|---|---|
| Contact Source | Readers, Audience Contacts, All |
| Campaign | Participated in specific campaign |
| User Tier | Free, Basic, Premium, etc. |
| Account Status | Active, Inactive, Banned |
| Subscription Status | Active, Expired, Cancelled, Trial |
| Email Verified | Yes / No |
| Newsletter Opt-In | Subscribed / Not subscribed |
| Audience Tags | Tags assigned to audience contacts |
| Registration Date | Date range filter |
| Last Active Date | Date range filter |
- Filters use AND logic between groups and OR logic within a group
- The right sidebar shows a live member count (debounced, updates as you change filters)
- Click "Preview Members" to see a sample of matching members
- Click Save Segment
7.3 Edit Segment
- Open segment → modify filters → member count updates in real-time → Save
7.4 Preview Members
- Click "Preview Members" or open the segment → see paginated list of matching members
- Shows both readers and audience contacts with source labels
- Can exclude specific members from the segment preview
7.5 Export Segment
- "Export" downloads segment members as CSV
- Includes both readers and audience contacts with a
sourcecolumn
7.6 Delete Segment
- Only allowed if the segment is not used by active campaigns or invitations
- Attempting to delete a segment in use shows an error message
Business Rules
- Segment member count is auto-calculated when filters change
- Segments resolve members dynamically at query time (not static lists)
- When Contact Source includes "Audience Contacts" or "All", the segment queries both
ReaderandAudienceContacttables - The
lastCountedAttimestamp tracks when the member count was last refreshed - Segment filter criteria are stored as a JSON object on the
UserSegmentmodel
8. Sub-Module: Outreach Campaigns

Purpose
Create multi-purpose outreach campaigns with customizable landing pages, dynamic forms, incentives, event content, and post-event materials.
Access
Admin Console → Marketing → Campaigns
Campaign Types
| Type | Use Case |
|---|---|
student_outreach | Student recruitment programs (with institution verification) |
event | Conference, webinar, workshop registration (with rich event content) |
lead_gen | Lead generation with form capture |
institutional | Institutional partnership campaigns |
general | General-purpose outreach |
Additionally, admins can create custom campaign types via the Campaign Types management page.
8.1 Create Campaign (7-Step Wizard)
Navigate to Marketing → Campaigns → New Campaign
Step 1: Basics
- Campaign Name (required)
- URL Slug (required, unique, auto-generated from name)
- Campaign Type (select from admin-managed types)
- Description
- Start Date (required)
- End Date (required)
Step 2: Form Fields Build the campaign registration form using the form field builder:
- Add fields with types: text, email, phone, url, number, date, textarea, select, radio, checkbox, photo_selfie, photo_id_card, file_upload, institution_picker
- Configure per field: label, placeholder, required, help text, validation rules (minLength, maxLength, min, max, pattern), options (for select/radio/checkbox)
- Reorder fields via drag-and-drop
- Also configure Notify Form fields (shown on expired campaigns)
Step 3: Landing Page
- Landing Page Title
- Landing Page Subtitle
- Landing Page Content (rich text / HTML)
- Hero Image (via Media Picker)
- Success Title & Message (shown after form submission)
- Expired Title & Message (shown when campaign is past end date)
- Testimonials (add multiple: name, role, location, quote, rating, avatar via Media Picker)
Step 4: Event Details (for event-type campaigns)
- Speakers: Name, Role, Bio, Talk Title, Photo (Media Picker), Social Links (Twitter, LinkedIn, Website)
- Schedule: Multi-day agenda with sessions (time, title, description, speaker dropdown, location, type: keynote/panel/workshop/break/networking)
- Partners: Name, Logo (Media Picker), Tier (title_sponsor/partner/supporter/media_partner), Website
- Why Attend: Benefits list (title, description, icon)
- Venue Info: Name, Address, City, Map URL, Directions
- Live Stream: URL, Type (YouTube/Vimeo/Custom)
Step 5: Post-Event Content (for completed events)
- Gallery: Multiple images via Media Picker with captions
- Winners: Display mode (ranked/flat/categorized), Winner entries (name, award, category, rank, description, photo)
- Recording URL: Link to event recording
- Resources: Downloadable files (title, URL, type, description, thumbnail)
Step 6: Incentive
- Incentive Type: none, free_trial, percentage_discount, fixed_discount
- Incentive Value (e.g., 30 days trial, 20% off)
- Incentive Description
- Promo Code Prefix (for auto-generated codes)
- Promo Validity Days
- Linked Subscription Plan
Step 7: Review
- Comprehensive preview of all configured sections
- Click Create Campaign to save as draft
8.2 Edit Campaign
Navigate to Marketing → Campaigns → [Campaign Name] → Edit
All fields from the creation wizard are editable. Changes are saved immediately on form submission.
Important: Status changes are NOT allowed through the edit form. Use the Publish/Unpublish actions on the campaign detail page.
8.3 Campaign Detail Page (7 Tabs)
Navigate to Marketing → Campaigns → [Campaign Name]
| Tab | Description |
|---|---|
| Details | Campaign info, form fields preview, incentive details, custom messages |
| Submissions | Submission management with approval workflow (see Section 10) |
| Invitations | Email invitation management (see Section 11) |
| Layout & Style | Page template picker and visual editor for campaign landing page |
| QR Code | Auto-generated QR code linking to campaign landing page URL |
| Statistics | Campaign metrics: total submissions, pending/approved/rejected counts, conversion rate |
| Social | Link to social campaign, compose social posts about the campaign |
8.4 Layout & Style Tab
The Layout & Style tab integrates with the platform's page template system:
- Select Template: Choose from available campaign-compatible templates
- Configure Sections: Customize the order and content of campaign page sections
- Save Draft: Save template changes without publishing
- Publish: Push template changes live to the reader portal
- Preview: Opens the reader portal campaign page in a new tab for visual review
Note: The preview requires the Reader Portal to be running. If preview shows "Preview Unavailable", verify NEXT_PUBLIC_READER_URL is correctly configured and the Reader Portal is running.
8.5 QR Code Tab
Generates a QR code linking to {READER_URL}/campaigns/{slug}. Can be downloaded for use in physical marketing materials.
8.6 Statistics Tab
Shows real-time campaign metrics:
- Total Submissions
- Pending Review Count
- Approved Count
- Rejected Count
- Conversion Count (submissions that resulted in subscriptions)
- Counts are sourced from actual submission records (not denormalized counters)
9. Sub-Module: Campaign Publishing & Landing Pages
Purpose
Control campaign visibility and manage the lifecycle of campaign landing pages accessible to the public.
Publishing Workflow
Publish a Campaign
- Navigate to the campaign detail page
- Ensure the campaign is in Draft or Paused status
- Click "Publish"
- System validates campaign completeness:
- Name is set
- Slug is set
- Start date and end date are set
- At least one form field is configured
- If validation passes: status changes to Active
- The campaign landing page is now live at
{READER_URL}/campaigns/{slug}
Unpublish a Campaign
- From an Active campaign, click "Unpublish"
- Status changes to Paused
- The landing page shows a "Campaign Paused" message to visitors
Complete a Campaign
- From an Active campaign, click "Complete"
- Status changes to Completed
- The landing page shows an "Event Recap" or expired state with notify form
Archive a Campaign
- From a Completed or Paused campaign, click "Archive"
- Status changes to Archived
- Campaign is hidden from public access
Landing Page URL
Every campaign has a public URL: {READER_URL}/campaigns/{slug}
This URL is displayed prominently on the campaign detail page with a copy button.
Landing Page Behavior by Status
| Campaign Status | Landing Page Behavior |
|---|---|
| Draft | Not accessible (404) |
| Active (before start date) | Shows "Upcoming" state with countdown, notify form |
| Active (within date range) | Full campaign page with form, event content if applicable |
| Active (event-type, during event) | "Happening Now" badge, live stream embed, schedule with "now" indicator |
| Paused | Shows "Campaign Paused" message |
| Completed | Shows event recap: gallery, winners, resources, testimonials, notify form |
| Archived | Not accessible (404) |
Event-Type Campaign: 3-State Rendering
Event-type campaigns have enhanced rendering based on timing:
Upcoming State (before start date): Hero → Why Attend → Info → Countdown → Speakers → Schedule → Venue → Registration Form → Partners → Notify Form
Active State (during event): Hero (with "Happening Now") → Live Stream → Why Attend → Info → Schedule (with "now" indicator) → Speakers → Venue → Form (if capacity) → Partners → Testimonials
Completed State (after end date): Hero (recap) → Info → Gallery → Winners → Testimonials → Resources → Speakers → Partners → Notify Form (for next event)
10. Sub-Module: Campaign Submissions & Review
Purpose
Process and review form submissions from campaign landing pages, including approval workflows and promo code generation.
Access
Campaign Detail → Submissions tab
10.1 View Submissions
- Table view with columns: Submitted At, Submitter Info, Status, Reviewed By, Actions
- Filter by status: All / Pending Review / Approved / Rejected / Needs Info
- Search submissions
- Status count badges on each filter tab
10.2 Review a Submission
- Click on a submission row to expand details
- View all form field responses, uploaded files (selfie, ID card, documents)
- Choose an action:
| Action | Result |
|---|---|
| Approve | Status → approved. Optional: send approval email using campaign_submission_approved template |
| Reject | Status → rejected. Requires rejection reason. Optional: send rejection email with reason |
| Request Info | Status → needs_info. Notifies submitter to provide additional information |
10.3 Generate Promo Code
After approving a submission:
- Click "Generate Promo Code"
- System generates a unique code using the campaign's
promoCodePrefix - Code is linked to the submission (one-to-one)
- Code type, value, and validity are based on campaign incentive settings
- Approval email includes the generated promo code
10.4 Bulk Operations
- Select multiple submissions → Bulk Approve or Bulk Reject
- Requires
MARKETING_SUBMISSIONS_REVIEWpermission
10.5 Submission Data Flow
Reader submits form on landing page
→ POST `/api/marketing/campaigns/public/{slug}/submit`
→ Creates CampaignSubmission record
→ Increments campaign counters (totalSubmissions, pendingCount)
→ Upserts AudienceContact from form data (email, name, phone)
→ Admin reviews in Submissions tab
→ Approve → sends approval email → optionally generates promo code
→ Reject → sends rejection email with reason11. Sub-Module: Campaign Invitations
Purpose
Send targeted email invitations to segmented audiences to drive campaign participation.
Access
Campaign Detail → Invitations tab
11.1 View Invitations
- List of all invitations for the campaign
- Status badges: Draft / Scheduled / Sending / Sent / Failed
- Delivery metrics per invitation: Recipient Count, Delivered, Opened, Clicked, Registered
11.2 Create Invitation
- Click "New Invitation"
- Select Segment: Choose from existing segments (which now include audience contacts)
- View segment member count to confirm audience size
- Write Subject Line: Supports
{{campaign_name}}variable - Compose Email Content: Rich text with variable support:
{{user_name}}— Recipient name{{campaign_name}}— Campaign name (auto-resolved){{campaign_url}}— Landing page URL with UTM parameters{{incentive_description}}— Incentive details
- Preview: See rendered email with sample data
- Choose action:
- Save as Draft — Save without sending
- Send Now — Send immediately
- Schedule — Set a future date/time for automated send
11.3 Send Invitation
- From a draft invitation, click "Send"
- System resolves segment members at send time
- Emails are dispatched using
campaign_invitationtemplate - Campaign URL includes UTM parameters:
?utm_source=email&utm_medium=invitation&utm_campaign={slug}&source=email_invite - Status transitions:
draft → sending → sent(orfailed) - Recipient emails are snapshotted on the invitation record for audit
11.4 Scheduled Invitations
- Scheduled invitations are picked up by the
send-invitationscron job - Processed when
scheduledAt <= nowand status isscheduled
11.5 Template Variable Resolution
The invitation composer now correctly resolves {{campaign_name}} to the actual campaign name in both the slider header and email content display. This was fixed as part of the marketing feature review.
12. Sub-Module: Email Campaigns

Purpose
Send template-based or custom HTML email campaigns to segmented audiences with scheduling and engagement tracking.
Access
Admin Console → Marketing → Email Campaigns
12.1 View Email Campaigns
- List with columns: Name, Subject, Segment, Status, Sent At, Open Rate, Click Rate
- Status tabs: Draft / Scheduled / Sending / Sent / Failed
- Count badges per tab
12.2 Create Email Campaign
- Click "New Email Campaign"
- Configure:
- Name (required)
- Subject Line (required, supports
{{variable}}interpolation) - Email Template — select from existing templates OR provide custom HTML
- Target Segment — select segment for audience targeting
- Save as draft
12.3 Send Email Campaign
Send Now:
- Open a draft email campaign
- Click "Send Now"
- System resolves segment members
- Enforces newsletter opt-in check (unless
forceSend=true) - Sends emails in batches via SendGrid/SMTP
- Status:
draft → sending → sent
Schedule:
- Open a draft email campaign
- Click "Schedule"
- Set the scheduled date/time
- Status:
draft → scheduled - The
send-scheduled-emailscron picks it up when due - Cron atomically claims the campaign (
scheduled → sending) to prevent double-send - Status:
sending → sent
12.4 Duplicate Campaign
- Click "Duplicate" to create a copy as a new draft
- Useful for recurring campaigns with slight modifications
12.5 Email Campaign Metrics
After sending, the campaign shows engagement metrics:
- Delivered: Emails successfully delivered
- Opened: Unique opens
- Clicked: Unique link clicks
- Bounced: Hard/soft bounces
- Unsubscribed: Recipients who unsubscribed
Metrics are updated via SendGrid/Mailchimp engagement webhooks in real-time.
12.6 Delete Email Campaign
- Only unsent campaigns (draft/scheduled) can be deleted
- Sent/sending campaigns cannot be deleted (for audit compliance)
13. Sub-Module: Newsletter Editions
Purpose
Author newsletter editions in Strapi CMS, manage their lifecycle, send them as email campaigns, and provide a reader-facing archive.
Architecture Note
Newsletter editions use a dual-storage approach:
- Strapi CMS: Source of truth for content authoring (rich text editing, cover images, SEO)
- Admin Console API: Acts as proxy to Strapi, bridges to Prisma
EmailCampaignfor email delivery
Access
Admin Console → Marketing → Newsletters
13.1 View Newsletter Editions
- List with status tabs: All / Draft / Published / Featured / Archived
- Search by title
- Columns: Title, Edition Number, Topic, Status, Publish Date
13.2 Create Newsletter Edition
- Click "New Newsletter"
- Fill in:
- Title (required)
- Edition Number (auto-incremented or manual)
- Topic (for archive filtering)
- Preview Text (teaser shown in archive cards)
- Publish Date
- Cover Image (via Media Picker)
- HTML Content (rich text editor)
- Plain Text Content (fallback for text-only email clients)
- External Archive URL (optional, for linking to external archive)
- Save (creates in Strapi as draft)
13.3 Publish Newsletter
- Open a draft edition
- Click "Publish"
- Uses Strapi v5 publish action API
- Edition becomes visible in the public newsletter archive at
{READER_URL}/newsletters
13.4 Send Newsletter as Email Campaign
- From the newsletters list, click "Send as Campaign" (or from edition detail page)
- A modal appears:
- Select Segment — choose target audience
- Subject Line — defaults to edition title
- Click Send
- System:
- Fetches full content from Strapi
- Creates a Prisma
EmailCampaignwithnewsletterEditionIdfor traceability - Dispatches emails to segment members
- The email campaign appears in the Email Campaigns list for tracking
13.5 Reader Portal Newsletter Archive
- Accessible at
{READER_URL}/newsletters - Topic filter pills: Filter editions by topic
- Edition cards: Show edition number, featured badge, topic tag, title, publish date, preview text
- Pagination: Navigate through archive
- Individual edition page:
{READER_URL}/newsletters/{slug}— renders full HTML content - ISR: Archive page uses Incremental Static Regeneration with 5-minute revalidation
14. Sub-Module: Promotional Codes

Purpose
Create and manage discount codes for subscription plans. Promo codes can be admin-created (DiscountCoupon) or auto-generated from campaign submissions (PromotionalCode).
Access
Admin Console → Marketing → Promo Codes
14.1 Two Types of Promo Codes
| Type | Model | Source | Purpose |
|---|---|---|---|
| Admin-Created Coupons | DiscountCoupon | Created manually by admins | Reusable discount codes (e.g., WELCOME20) |
| Campaign-Generated Codes | PromotionalCode | Auto-generated from approved campaign submissions | One-time codes tied to individual submissions |
14.2 Create Admin Coupon
- Click "New Promo Code"
- Configure:
- Code (unique, e.g.,
SUMMER2026) - Description
- Discount Type: Percentage or Fixed Amount
- Discount Value (e.g., 20% or $10)
- Linked Plan (optional, restrict to specific subscription plan)
- Billing Interval (optional)
- Max Uses (total redemption limit)
- Max Uses Per User
- Valid From / Valid To dates
- Options: New subscribers only, applies to (FIRST_PAYMENT / N_CYCLES / ALL_RENEWALS), discount cycles, allow gift/bulk/institutional
- Code (unique, e.g.,
- Save
14.3 Campaign Promo Code Auto-Generation
When a campaign submission is approved and the campaign has incentive settings:
- Admin approves submission → clicks "Generate Promo Code"
- System generates a unique code:
{promoCodePrefix}-{random} - Code is saved as
PromotionalCodewith:- Type: percentage, fixed, or trial_days (from campaign incentive settings)
- Value: from campaign
incentiveValue - Validity: from campaign
promoValidityDays - Max redemptions: 1 (single use)
- Code is linked to the submission record
14.4 Public Promo Code Validation & Redemption
Validate (POST /api/promo-codes/validate or POST /api/promo/validate):
- Checks both
PromotionalCodeandDiscountCoupontables - Returns discount type, value, validity
- Rate limited: 20 requests/minute/IP
Redeem (POST /api/promo/redeem):
- For
trial_days: Atomically creates reader account + subscription in a transaction - For
percentage/fixed: Atomically claims redemption slot - Rate limited: 5 requests/15 minutes/IP
15. Sub-Module: Conversion Surfaces

Purpose
Deploy promotional overlays (modals, banners, slide-ins, sticky notifications) on the Reader Portal with sophisticated targeting and trigger rules to drive reader actions.
Access
Admin Console → Marketing → Conversion Surfaces
15.1 Surface Types
| Type | Behavior |
|---|---|
modal | Full-screen overlay with backdrop |
sticky_notification | Persistent notification bar |
slide_in | Corner slide-in panel |
top_banner | Full-width banner at page top |
bottom_banner | Full-width banner at page bottom |
inline_embed | Embedded within page content |
15.2 Create Conversion Surface
- Click "New Surface"
- Configure:
Identity & Presentation:
- Name, Surface Type, Placement
- Title, Subtitle, Body Text
- Image URL
- Primary CTA (text + URL), Secondary CTA (text + URL)
- Background Color, Theme (light/dark/brand)
Conversion Goal:
- register, login, subscribe_paid, subscribe_newsletter, view_campaign, view_event, redeem_coupon, view_product, visit_pricing, custom_cta
Entity Links (optional):
- Linked Coupon (DiscountCoupon)
- Linked Plan (SubscriptionPlan)
- Linked Campaign (OutreachCampaign)
- Linked Event
- Linked Product
- Newsletter Slug
Targeting Rules (JSON):
audiences: anonymous, registered, subscriber, newsletter_subscribed, newsletter_unsubscribedpageTypes: homepage, article, section, etc.deviceTypes: mobile, tablet, desktopincludeUrls/excludeUrls: URL pattern matchingcontentTypes,sections,tags: Content-based targetingutmSource,utmMedium,utmCampaign: UTM parameter matching
Trigger Configuration:
page_load— Show on page load (with optional delay)time_on_page— Show after N secondsscroll_depth— Show after scrolling to N%exit_intent— Show when mouse leaves viewportpage_view_count— Show after N page viewsafter_article_read— Show after scrolling to 100% of articlemanual— Programmatic trigger only
Frequency & Suppression:
- Frequency Limit + Unit (per session/day/week/month)
- Suppress After Dismiss (boolean)
- Suppress After Conversion (boolean)
- Suppress If Already Converted (boolean)
Scheduling:
- Status: draft, scheduled, active, paused
- Starts At / Ends At dates
- Priority (lower number = higher priority)
- Save as draft
15.3 Activate Surface
- Change status from
draft→active(via PUT API) - Surface immediately begins displaying to eligible readers on the Reader Portal
15.4 Reader Portal Rendering
The conversion surface system uses a layered rendering architecture:
- GlobalConversionSurfaces — Wraps the entire Reader Portal root layout
- ConversionSurfaceProvider — React context that:
- Detects device type (mobile/tablet/desktop)
- Counts page views (stored in state, incremented per mount)
- Fetches eligible surfaces from
GET /api/public/conversion-surfaces/eligible - Filters through local suppression checks (localStorage for anonymous, server-side for authenticated)
- Sorts by priority
- Enforces slot-based conflict resolution: one modal at a time, one sticky at a time, banners can coexist
- ConversionSurfaceRenderer — Evaluates trigger conditions and renders the appropriate surface component
Trigger Evaluation:
page_load: Fires after optionaltriggerDelaySecondstime_on_page: Timer starts on mount, fires attriggerDelaySecondsscroll_depth: Monitors scroll position, fires whentriggerScrollPercentis reachedexit_intent: Listens formouseoutevent leaving the viewport (desktop only)page_view_count: Compares provider's page view count againsttriggerPageViewsafter_article_read: Monitors scroll to 100% of contentmanual: Not automatically triggered
Frequency Enforcement:
- Anonymous readers: Client-side via localStorage (per-surface tracking)
- Authenticated readers: Server-side via ConversionEvent records
15.5 Analytics
Dual tracking approach:
- Internal (ConversionEvent table): Stores every interaction with reader context (page, device, UTM, session)
- GA4: Fires custom events for cross-platform attribution
Stats API (GET /api/marketing/conversion-surfaces/{id}/stats):
- Date range filtering
- Time series data
- Breakdowns by device, page, UTM
- Computed rates: CTR, conversion rate, dismiss rate, engagement rate
Denormalized counters on ConversionSurface model:
impressionCount,clickCount,dismissCount,conversionCount- Updated atomically on each event
- Used for fast list view display
15.6 Coupon Integration
Surfaces can link to DiscountCoupon records:
- Eligibility API validates coupon status (active, not expired, usage limit not reached)
- Reader Portal shows copyable coupon code with "Copy Code" button
coupon_copiedevent tracked to both GA4 and internal analytics- Coupon code passed to checkout URL as parameter
16. Sub-Module: Campaign Types
Purpose
Define and manage reusable campaign type categories that appear in the campaign creation dropdown.
Access
Admin Console → Marketing → Campaign Types
Features
- CRUD: Create, read, update, delete campaign types
- Fields: Name (unique), Slug (unique, auto-generated), Description, Icon (Lucide icon name), Active flag, Sort Order
- Seeded Defaults: 5 types seeded from the system enum: Student Outreach, Event Registration, Lead Generation, Institutional, General
- Usage: Campaign types appear in the campaign creation wizard and campaign edit form as a dropdown
- Deletion Protection: Cannot delete a type that is linked to existing campaigns
17. Cron Jobs & Automation
Overview
| Cron Endpoint | Purpose | Auth | Recommended Schedule |
|---|---|---|---|
GET /api/cron/send-scheduled-emails | Send due email campaigns | CRON_SECRET | Every 1–5 min |
GET /api/cron/campaign-status | Auto-complete expired campaigns; send notify emails | CRON_SECRET | Every 15–30 min |
GET /api/cron/send-invitations | Send scheduled campaign invitations | CRON_SECRET | Every 5 min |
GET /api/cron/expire-invitations | Expire overdue invitations | CRON_SECRET | Every hour |
GET /api/cron/invitation-reminders | Weekly reminders for pending invitations | CRON_SECRET | Weekly |
send-scheduled-emails
- Finds all email campaigns with
status = 'scheduled'andscheduledAt <= now - Atomically transitions status to
'sending'(prevents double-send on concurrent cron runs) - Resolves segment members
- Sends emails via SendGrid/SMTP
- Updates status to
'sent'or'failed'
campaign-status
- Finds active campaigns where
endDate < now→ transitions to'completed' - Finds draft campaigns where
endDate < now→ transitions to'archived' - Looks up
CampaignNotifyRequestrecords for campaigns that just became active → sends notification emails usingcampaign_notify_new_campaigntemplate
send-invitations
- Finds invitations with
status = 'scheduled'andscheduledAt <= now - Resolves segment members for each invitation
- Sends invitation emails
- Updates status to
'sent'or'failed'
18. Webhook Integrations
SendGrid Engagement Webhook
Endpoint: POST /api/webhooks/sendgrid
Events Processed:
| Event | Action |
|---|---|
delivered | Updates EmailLog status, increments campaign delivered count |
open | Increments campaign opened count |
click | Increments campaign clicked count |
bounce | Updates EmailLog status to bounced, increments bounced count |
dropped | Logs delivery failure |
spamreport | Logs spam report |
unsubscribe | Increments unsubscribed count |
Authentication: ECDSA P-256 signature verification (optional, controlled by SENDGRID_WEBHOOK_VERIFICATION_KEY)
Mailchimp/Mandrill Engagement Webhook
Endpoint: POST /api/webhooks/mailchimp
Events Processed: send, open, click, hard_bounce, soft_bounce, spam, unsub
Authentication: HMAC-SHA1 signature verification (optional, controlled by MAILCHIMP_WEBHOOK_KEY)
URL Verification: GET /api/webhooks/mailchimp returns 200 OK for Mandrill webhook registration.
19. Reader Portal Rendering
Campaign Landing Pages
URL: {READER_URL}/campaigns/{slug}
Technology: Next.js App Router, Server Component with ISR (60-second revalidation)
Components (24 campaign section components):
| Component | Purpose |
|---|---|
| CampaignHero | Hero image with title/subtitle, status badges, date range |
| CampaignInfo | Rich content rendering (HTML/description) |
| CampaignForm | Dynamic form builder with 12 field types (text, email, phone, select, radio, checkbox, textarea, date, institution_picker, photo_selfie, photo_id_card, file_upload), validation, file upload |
| CampaignCta | Call-to-action with incentive info (state-aware: hidden for expired/paused/completed) |
| CampaignSuccess | Post-submission confirmation with share/copy link |
| CampaignExpired | Expired state with messaging and notify form |
| CampaignUpcoming | Pre-start date state with countdown and notify form |
| CampaignPaused | Paused state with messaging and notify form |
| CampaignCountdown | Real-time countdown timer to start/end |
| CampaignSpeakers | Speaker grid with photos, bios, social links (Twitter/LinkedIn/Website) |
| CampaignSchedule | Multi-day agenda with session types, "Happening Now" indicator, ICS calendar download |
| CampaignVenue | Venue details with Google Maps embed iframe, address, city, directions |
| CampaignWhyAttend | Feature/benefit highlights grid with emoji icons |
| CampaignPartners | Partner logos grouped by tier (Title Sponsor, Partners, Supporters, Media Partners) |
| CampaignLiveStream | Live stream embed (YouTube/Vimeo/custom) with "LIVE NOW" / "Recording" badge |
| CampaignGallery | Post-event photo gallery with lightbox viewer and keyboard navigation |
| CampaignWinners | Awards display (ranked podium / flat grid / categorized groups) |
| CampaignResources | Downloadable resources with type badges (video/article/document) |
| CampaignTestimonial | Testimonial cards with quotes, ratings, and avatars |
| CampaignNotifyForm | Reusable "Get Notified" form for expired/upcoming/paused campaigns |
| CampaignNotifyFormSection | Wrapper for notify form as standalone section |
| CampaignProgramCalendar | Multi-track or timeline calendar view of event schedule |
| CampaignListingGrid | Grid of campaign cards with status badges for campaign listing pages |
| CampaignListingHero | Hero section for campaign listing pages |
Newsletter Archive
URL: {READER_URL}/newsletters
Components:
- NewsletterArchive — Topic filter pills, paginated edition grid
- NewsletterCard — Edition card with number badge, topic, date, preview
- NewsletterArchiveHeader — Page header
- Individual edition:
{READER_URL}/newsletters/{slug}
Programs Discovery Page
URL: {READER_URL}/programs
Features:
- Browse past/ongoing/upcoming campaigns
- Filter by campaign type, status, institute, city, country
- Card grid with status badges, date ranges, type badges
- Links to campaign landing pages
Conversion Surfaces
Renders globally across all Reader Portal pages via GlobalConversionSurfaces in the root layout.
See Section 15.4 for detailed rendering behavior.
20. Business Logic & Rules
Campaign Validation Rules
- Campaign slug must be unique across all campaigns
- Start date must be before end date
- At least one form field required before publishing
- Status transitions follow a strict state machine (see Section 4)
- Cannot delete a campaign that has submissions (use archive instead)
Segment Resolution Rules
- Filters use AND logic between groups, OR logic within a group
- When source includes "audience contacts", queries both Reader and AudienceContact tables
- Members are resolved dynamically at query time (not cached lists)
- Segment cannot be deleted if used by active campaigns or invitations
Email Campaign Rules
- Cannot update or delete sent/sending campaigns
- Scheduled campaigns are atomically claimed by cron to prevent double-send
- Newsletter opt-in is enforced by default (unless
forceSend=true) - Template variables use Handlebars syntax:
{{variable_name}}
Conversion Surface Rules
- Slot-based conflict resolution: Maximum one modal, one sticky notification active simultaneously; banners can coexist
- Priority: Lower number = higher priority; when multiple surfaces compete for a slot, highest priority wins
- Frequency capping: Configurable per session/day/week/month
- Suppression: Can suppress after dismiss, after conversion, or if already converted
- Targeting: AND between dimension types, OR within a dimension
- Soft delete: Archive instead of hard delete (preserves analytics history)
Promotional Code Rules
- Campaign-generated codes are single-use (maxRedemptions = 1)
- Admin coupons can have configurable max uses and per-user limits
- Validation checks: code exists, is active, not expired, usage limit not reached
trial_daysredemption atomically creates reader account + subscription- Rate limiting on public validation (20/min) and redemption (5/15min)
Contact/Audience Rules
- Email must be unique across audience contacts
- Inactive contacts (isActive = false) are excluded from segments
- Unsubscribed contacts are excluded from non-forced email sends
- Auto-upsert from campaign submissions and newsletter signups is non-blocking (failures don't break primary flow)
21. End-to-End User Flows
Flow 1: Create and Launch an Outreach Campaign
1. Create Campaign Type (if needed)
Marketing → Campaign Types → Add → "Conference"
2. Build Audience Segment
Marketing → Segments → New Segment
→ Filter: Account Status = Active, Subscription = None
→ Save as "Non-Subscribers"
3. Create Campaign
Marketing → Campaigns → New Campaign
→ Step 1: Name, Slug, Type, Dates
→ Step 2: Form Fields (Name, Email, Phone, Institution)
→ Step 3: Landing Page (Hero, Content, Success/Expired messages)
→ Step 4: Event Details (Speakers, Schedule, Venue) — if event
→ Step 5: Post-Event Content — skip for now
→ Step 6: Incentive (Free Trial, 30 days)
→ Step 7: Review → Create
4. Configure Landing Page Template
Campaign Detail → Layout & Style tab
→ Select template → Save
5. Publish Campaign
Campaign Detail → Click "Publish"
→ Validates → Status = Active
→ Copy landing page URL
6. Send Invitations
Campaign Detail → Invitations tab → New Invitation
→ Select "Non-Subscribers" segment
→ Compose email → Send
7. Share on Social
Campaign Detail → Social tab
→ "Create Social Campaign" → "Compose Post"
→ Pre-filled with campaign name/URL → Publish
8. Monitor
Campaign Detail → Statistics tab → View submissions count
→ Submissions tab → Review & approve submissions
→ Generate promo codes for approved submissions
9. Complete Campaign
After end date: Click "Complete"
Add post-event content (gallery, winners, resources)
Campaign page shows recap stateFlow 2: Newsletter Edition → Email Campaign
1. Create Newsletter Edition
Marketing → Newsletters → New Newsletter
→ Author content in form
→ Save as draft
2. Publish to Archive
Newsletter detail → Click "Publish"
→ Visible at `/newsletters/{slug}`
3. Send as Email Campaign
Newsletters list → "Send as Campaign" button
→ Select target segment
→ Confirm subject line
→ Send
4. Track Engagement
Marketing → Email Campaigns
→ Find campaign → View open/click/bounce metricsFlow 3: Deploy a Conversion Surface
1. Create Coupon (optional)
Marketing → Promo Codes → New Code
→ Code: "SAVE20", 20% off
2. Create Conversion Surface
Marketing → Conversion Surfaces → New Surface
→ Type: Modal, Theme: Brand
→ Title: "Subscribe & Save 20%"
→ Link coupon: "SAVE20"
→ Goal: subscribe_paid
→ Targeting: anonymous + homepage + desktop
→ Trigger: exit_intent
→ Frequency: 1 per week
→ Suppress after conversion
3. Activate
Set status to "active"
→ Surface appears to matching readers on Reader Portal
4. Monitor
Conversion Surfaces list → View impression/click/conversion metrics
→ Detail → Stats → Time series, breakdowns- Email provider configured and verified (SMTP status = configured)
- Email templates seeded (4 campaign templates exist)
- Email preferences/branding configured
- RBAC permissions assigned to test role
- Cron secret configured
- Reader Portal running and accessible
- S3 storage configured (for file upload tests)
- At least one subscription plan exists (for promo code tests)
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| A1 | Add single contact | Marketing → Segments → Contacts → Add Contact → Fill email + name → Save | Contact appears in table |
| A2 | CSV upload — happy path | Upload valid CSV with email, name, phone columns | Created count matches, contacts in table |
| A3 | CSV upload — duplicate emails | Upload CSV with existing emails | Updated count > 0, no duplicates |
| A4 | CSV upload — missing email column | Upload CSV without email column | Error: email column required |
| A5 | Edit contact | Click contact → Edit fields → Save | Fields updated |
| A6 | Delete contact | Select contact → Delete | Contact removed from table (soft delete) |
| A7 | Export contacts | Click Export | CSV downloads with all active contacts |
| A8 | Auto-create from campaign submission | Submit campaign form with email | New contact appears with tag campaign_submission |
| A9 | Auto-create from newsletter signup | Subscribe to newsletter | New contact appears with tag newsletter_signup |
| A10 | Tag filtering | Add tags to contacts → Filter by tag | Only matching contacts shown |
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| B1 | Create segment — readers only | New Segment → Source: Readers → Subscription: Active → Save | Member count shows active subscribers |
| B2 | Create segment — audience only | New Segment → Source: Audience Contacts → Tags: "campaign" → Save | Member count shows tagged contacts |
| B3 | Create segment — mixed source | Source: All → Save | Count includes both readers and contacts |
| B4 | Live member count preview | Change filters → Watch count update | Count updates within 500ms debounce |
| B5 | Preview members | Click Preview Members | Paginated list with source labels |
| B6 | Export segment | Click Export | CSV with source column |
| B7 | Delete used segment | Try deleting segment used by active invitation | Error: segment in use |
| B8 | Delete unused segment | Delete segment with no references | Segment removed |
| B9 | Empty segment | Create segment with no matching filters | Count = 0, preview shows empty state |
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| C1 | Create campaign — full wizard | Complete all 7 steps → Create | Campaign saved as draft |
| C2 | Create campaign — minimal | Only required fields (name, slug, dates, 1 form field) → Create | Campaign saved |
| C3 | Duplicate slug | Create campaign with existing slug | Error: slug must be unique |
| C4 | Edit campaign | Edit → Change fields → Save | Fields updated |
| C5 | Publish campaign | Draft → Publish | Status = active, landing page accessible |
| C6 | Publish incomplete | Draft without form fields → Publish | Validation error |
| C7 | Unpublish campaign | Active → Unpublish | Status = paused, landing page shows paused |
| C8 | Complete campaign | Active → Complete | Status = completed |
| C9 | Archive campaign | Completed → Archive | Status = archived, landing page 404 |
| C10 | Delete with submissions | Try deleting campaign with submissions | Error: use archive instead |
| C11 | Delete without submissions | Delete draft campaign with no submissions | Campaign removed |
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| D1 | Active campaign page | Visit /campaigns/{slug} for active campaign | Full landing page renders |
| D2 | Expired campaign page | Visit page for completed campaign | Expired state with recap content |
| D3 | Upcoming campaign page | Visit page for campaign with future start date | Upcoming state with countdown |
| D4 | Paused campaign page | Visit page for paused campaign | Paused message shown |
| D5 | Draft/archived page | Visit page for draft/archived campaign | 404 not found |
| D6 | Form submission — happy path | Fill all required fields → Submit | Success message, submission created |
| D7 | Form submission — validation | Submit with empty required fields | Validation errors shown per field |
| D8 | Form — file upload | Submit form with file_upload field | File uploaded to S3, shown in submission |
| D9 | Form — photo selfie | Use selfie field on mobile | Camera option available, photo uploaded |
| D10 | Form — institution picker | Type institution name | Autocomplete suggestions appear |
| D11 | Notify form (expired) | Submit notify form on expired campaign | NotifyRequest created |
| D12 | Event page — speakers | Visit event campaign with speakers data | Speaker grid with photos and links |
| D13 | Event page — schedule | Visit event with schedule | Multi-day agenda renders |
| D14 | Event page — countdown | Visit upcoming event | Real-time countdown timer ticks |
| D15 | Event page — live stream | Visit active event with live stream URL | Stream embed renders |
| D16 | Event page — post-event | Visit completed event with gallery/winners | Gallery and winners render |
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| E1 | View submissions | Campaign Detail → Submissions tab | List of submissions with status |
| E2 | Approve submission | Select submission → Approve | Status = approved, approval email sent |
| E3 | Reject submission | Select → Reject → Enter reason | Status = rejected, rejection email sent |
| E4 | Request info | Select → Request Info | Status = needs_info |
| E5 | Generate promo code | Approve → Generate Promo Code | Unique code generated, linked to submission |
| E6 | Bulk approve | Select multiple → Bulk Approve | All selected approved |
| E7 | Bulk reject | Select multiple → Bulk Reject | All selected rejected |
| E8 | Permission check | User without REVIEW permission → Try approve | Action blocked / hidden |
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| F1 | Create email campaign | New → Name, Subject, Template, Segment → Save | Draft campaign created |
| F2 | Send now | Draft → Send Now | Status: draft → sending → sent |
| F3 | Schedule send | Draft → Schedule → Set future time | Status = scheduled |
| F4 | Scheduled delivery | Wait for cron trigger after scheduled time | Status: scheduled → sending → sent |
| F5 | Duplicate campaign | Click Duplicate | New draft created with same content |
| F6 | Delete draft | Delete draft campaign | Campaign removed |
| F7 | Delete sent campaign | Try deleting sent campaign | Error: cannot delete sent campaigns |
| F8 | Engagement tracking | Send campaign → Open email → Click link | Metrics update (delivered, opened, clicked) |
| F9 | Newsletter opt-in enforcement | Send to segment with non-opted-in members | Non-opted-in members skipped (unless forceSend) |
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| G1 | Create modal surface | New → Type: Modal → Configure → Save | Surface saved as draft |
| G2 | Activate surface | Set status to active | Surface visible to matching readers |
| G3 | Exit intent trigger | Set trigger: exit_intent → Move mouse out of viewport | Modal appears |
| G4 | Scroll depth trigger | Set trigger: scroll_depth 50% → Scroll to 50% | Surface appears |
| G5 | Time on page trigger | Set trigger: time_on_page 10s → Wait 10s | Surface appears |
| G6 | Frequency capping | Set frequency: 1/week → Trigger → Dismiss → Revisit | Surface does not reappear |
| G7 | Targeting — anonymous only | Target: anonymous → Visit as logged-in user | Surface does NOT appear |
| G8 | Targeting — homepage only | Target: pageType: homepage → Visit article page | Surface does NOT appear |
| G9 | Coupon copy | Surface with linked coupon → Click "Copy Code" | Code copied to clipboard, event tracked |
| G10 | Conflict resolution | Create 2 active modals → Visit matching page | Only higher-priority modal shows |
| G11 | Suppress after dismiss | Enable suppress → Dismiss surface → Revisit | Surface does not reappear |
| G12 | Analytics tracking | Trigger surface → Interact | Events recorded in ConversionEvent + GA4 |
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| H1 | Cron auth | Call cron endpoint without CRON_SECRET | 401 Unauthorized |
| H2 | Campaign auto-complete | Active campaign past end date → Trigger cron | Status → completed |
| H3 | Scheduled email delivery | Schedule email for past time → Trigger cron | Email sent, status → sent |
| H4 | Double-send prevention | Trigger send-scheduled-emails twice concurrently | Only one run processes the campaign |
| H5 | Invitation send via cron | Schedule invitation → Trigger cron | Invitation emails sent |
| # | Scenario | Steps | Expected Result |
|---|---|---|---|
| I1 | No marketing read | User without MARKETING_CAMPAIGNS_READ → Visit /marketing | Campaigns card hidden |
| I2 | Read only | User with READ but not CREATE → Try creating campaign | Create button hidden / action blocked |
| I3 | No send permission | User without EMAIL_CAMPAIGNS_SEND → Try sending | Send button hidden / action blocked |
| I4 | Full marketing admin | User with all marketing permissions | Full access to all features |
Symptoms: Email campaigns stuck in "sending", invitation emails not delivered
Check:
- SMTP/SendGrid configuration: Admin → Settings → Email → SMTP Status
- Verify
SENDGRID_API_KEYor SMTP credentials in environment - Check email logs for delivery errors: Query
EmailLogfor status = 'failed' - Verify cron jobs are running (for scheduled sends)
Symptoms: Published campaign not accessible at /campaigns/{slug}
Check:
- Campaign status is
active(not draft or archived) - Campaign dates: Start date must be in the past for active state
- Reader Portal is running
NEXT_PUBLIC_ADMIN_API_URLis correctly configured in Reader Portal- Campaign slug matches the URL exactly
Symptoms: Active surface not showing on Reader Portal
Check:
- Surface status is
active - Targeting rules match the current page/device/user state
- Frequency cap not exceeded (clear localStorage for anonymous testing)
- Not suppressed by dismiss/conversion rules
- Not blocked by conflict resolution (another higher-priority surface using the slot)
- Trigger conditions met (e.g., scroll depth reached, time elapsed)
Symptoms: Layout & Style tab shows iframe error
Check:
- Reader Portal is running on the expected port
NEXT_PUBLIC_READER_URLis correctly set in Admin Console environment- No module boundary errors in Reader Portal (check console logs)
Symptoms: Segment with filters shows no matching members
Check:
- Contact Source filter: Ensure correct source is selected (Readers vs Audience vs All)
- Filter combination may be too restrictive (AND logic between groups)
- For audience contacts: Ensure contacts are active (
isActive = true) - For readers: Ensure matching subscription/status criteria exist
Symptoms: Newsletters page shows empty or errors
Check:
- Strapi is running and accessible
STRAPI_URLandSTRAPI_API_TOKENare configured- Newsletter edition content type exists in Strapi
- Editions have been published in Strapi (draft editions only show in admin)
Symptoms: Email campaign shows 0 opens/clicks after sending
Check:
- Webhook URL registered in SendGrid/Mailchimp dashboard
- Webhook endpoint is publicly accessible
- Check webhook signature verification (disable temporarily for debugging)
- Verify
X-SMTPAPIunique_args are being set at send time
Symptoms: Valid-looking code returns error on redemption
Check:
- Code exists and
isActive = true - Code not expired (
validUntil> now) - Usage limit not reached (
currentRedemptions < maxRedemptions) - For campaign codes: Submission must be approved first
- Rate limiting: Max 5 redemptions per 15 minutes per IP
| Feature | Status | Notes |
|---|---|---|
| A/B Testing for Conversion Surfaces | Planned | Run multiple variants with statistical significance tracking |
| Geographic Targeting | Planned | Target conversion surfaces by reader location |
| Webhook Events for Campaign Lifecycle | Planned | Events: created, activated, conversion, milestone |
| Automated Conversion Surface Expiration Cron | Planned | Auto-transition scheduled → active → expired |
| Admin Preview for Conversion Surfaces | Planned | Preview surface rendering before activation |
| Bulk Operations for Conversion Surfaces | Planned | Bulk pause, activate, archive |
| Template Library for Surfaces | Planned | Pre-built templates for common patterns |
| Content-Aware Inline Embed Positioning | Planned | Auto-position inline embeds based on article structure |
- No real-time websocket notifications — Notification bell uses polling, not push
- Newsletter content authoring requires Strapi — No built-in rich text editor for newsletters in Admin Console
- Conversion surface preview — Cannot preview a surface's appearance in Admin Console before activating
- Campaign form field types — No conditional logic (show/hide fields based on other field values)
- Segment caching — Segments resolve dynamically on every query; large segments may have latency
- Email campaign retry — Failed campaigns must be manually retried (no automatic retry)
- Invitation tracking — Individual recipient open/click tracking depends on email provider webhook reliability
- Conversion surface inline embed — Requires manual template configuration; no automatic content-aware positioning
/api/marketing)| Endpoint | Methods | Auth | Section |
|---|---|---|---|
/campaigns | GET, POST | Session + RBAC | Campaigns |
/campaigns/[id] | GET, PUT, DELETE | Session + RBAC | Campaigns |
/campaigns/[id]/publish | POST | Session + RBAC | Publishing |
/campaigns/[id]/stats | GET | Session + RBAC | Statistics |
/campaigns/[id]/qr-code | GET | Session + RBAC | QR Code |
/campaigns/[id]/submissions | GET, POST | Session + RBAC | Submissions |
/campaigns/[id]/submissions/[submissionId] | GET, PUT | Session + RBAC | Submissions |
/campaigns/[id]/submissions/[submissionId]/generate-promo | POST | Session + RBAC | Promo Codes |
/campaigns/[id]/submissions/bulk | POST | Session + RBAC | Submissions |
/campaigns/[id]/invitations | GET, POST | Session + RBAC | Invitations |
/campaigns/[id]/invitations/[invitationId] | GET, DELETE | Session + RBAC | Invitations |
/campaigns/[id]/invitations/[invitationId]/send | POST | Session + RBAC | Invitations |
/campaigns/[id]/institutes | GET, POST | Session + RBAC | Institutes |
/campaigns/[id]/social-campaign | GET, POST | Session + RBAC | Social |
/campaigns/public/[slug] | GET | None | Public Landing Page |
/campaigns/public/[slug]/submit | POST | None | Public Form Submit |
/campaigns/public/[slug]/notify | POST | None | Public Notify |
/campaigns/public/[slug]/upload | POST | None | Public File Upload |
/campaigns/public/reference-data | GET | None | Public Reference Data |
/campaigns/public/institutions | GET | None | Public Institution Search |
/campaigns/public/programs | GET | None | Public Programs Listing |
/email-campaigns | GET, POST | Session + RBAC | Email Campaigns |
/email-campaigns/[id] | GET, PUT, DELETE | Session + RBAC | Email Campaigns |
/email-campaigns/[id]/send | POST | Session + RBAC | Email Send |
/email-campaigns/[id]/preview | POST | Session + RBAC | Email Preview |
/email-campaigns/[id]/duplicate | POST | Session + RBAC | Email Duplicate |
/segments | GET, POST | Session + RBAC | Segments |
/segments/[id] | GET, PUT, DELETE | Session + RBAC | Segments |
/segments/[id]/preview | GET | Session + RBAC | Segment Preview |
/segments/[id]/export | GET | Session + RBAC | Segment Export |
/segments/preview | POST | Session + RBAC | Preview w/o Save |
/audience | GET, POST | Session + RBAC | Audience Contacts |
/audience/[id] | GET, PUT, DELETE | Session + RBAC | Audience Contacts |
/audience/upload | POST | Session + RBAC | CSV Upload |
/audience/export | GET | Session + RBAC | CSV Export |
/promo-codes | GET, POST | Session + RBAC | Promo Codes |
/promo-codes/[id] | GET, PUT, DELETE | Session + RBAC | Promo Codes |
/promo-codes/validate | POST | Session + RBAC | Validate Code |
/newsletters | GET, POST | Session + RBAC | Newsletters |
/newsletters/[documentId] | GET, PUT, DELETE | Session + RBAC | Newsletters |
/newsletters/[documentId]/publish | POST | Session + RBAC | Newsletter Publish |
/newsletters/[documentId]/send | POST | Session + RBAC | Newsletter Send |
/newsletters/public | GET | None | Public Archive |
/newsletters/public/[slug] | GET | None | Public Edition |
/conversion-surfaces | GET, POST | Session + RBAC | Conversion Surfaces |
/conversion-surfaces/[id] | GET, PUT, DELETE | Session + RBAC | Conversion Surfaces |
/conversion-surfaces/[id]/stats | GET | Session + RBAC | Surface Analytics |
/conversion-surfaces/[id]/duplicate | POST | Session + RBAC | Surface Duplicate |
/campaign-types | GET, POST | Session + RBAC | Campaign Types |
/campaign-types/[id] | GET, PUT, DELETE | Session + RBAC | Campaign Types |
| Endpoint | Methods | Purpose |
|---|---|---|
/api/public/conversion-surfaces/eligible | GET | Fetch eligible surfaces for reader context |
/api/public/conversion-surfaces/event | POST | Track conversion surface events |
/api/promo/validate | POST | Validate promo code (rate limited) |
/api/promo/redeem | POST | Redeem promo code (rate limited) |
| Enum | Values |
|---|---|
| OutreachCampaignStatus | draft, active, paused, completed, archived |
| OutreachCampaignType | student_outreach, event, lead_gen, institutional, general |
| CampaignSubmissionStatus | pending_review, approved, rejected, needs_info |
| CampaignInvitationStatus | draft, scheduled, sending, sent, failed |
| EmailCampaignStatus | draft, scheduled, sending, sent, failed |
| PromoCodeType | percentage, fixed, trial_days |
| ConversionSurfaceType | modal, sticky_notification, slide_in, top_banner, bottom_banner, inline_embed |
| ConversionSurfaceStatus | draft, scheduled, active, paused, expired, archived |
| ConversionGoal | register, login, subscribe_paid, subscribe_newsletter, view_campaign, view_event, redeem_coupon, view_product, visit_pricing, custom_cta |
| ConversionTrigger | page_load, time_on_page, scroll_depth, exit_intent, page_view_count, after_article_read, manual |
| ConversionEventType | impression, click, dismiss, conversion, coupon_copied |
| FrequencyUnit | session, day, week, month |
| NewsletterEditionStatus | draft, published, featured, archived |
| Template Key | Variables | Used By |
|---|---|---|
campaign_submission_approved | user_name, campaign_name, promo_code, incentive_description | Submission approval |
campaign_submission_rejected | user_name, campaign_name, rejection_reason | Submission rejection |
campaign_invitation | user_name, campaign_name, campaign_url, incentive_description | Campaign invitations |
campaign_notify_new_campaign | user_name, campaign_name, campaign_url | Notify requests |
| Variable | Required For | Used By |
|---|---|---|
SENDGRID_API_KEY | Email delivery (SendGrid) | Admin Console |
SMTP_HOST/PORT/USER/PASS | Email delivery (SMTP) | Admin Console |
EMAIL_FROM_ADDRESS | All outgoing emails | Admin Console |
EMAIL_FROM_NAME | All outgoing emails | Admin Console |
STRAPI_URL | Newsletter editions | Admin Console |
STRAPI_API_TOKEN | Newsletter editions | Admin Console |
AWS_S3_BUCKET | File uploads | Admin Console |
AWS_ACCESS_KEY_ID | File uploads | Admin Console |
AWS_SECRET_ACCESS_KEY | File uploads | Admin Console |
AWS_REGION | File uploads | Admin Console |
CRON_SECRET | Cron job auth | Admin Console |
NEXT_PUBLIC_READER_URL | Campaign preview links | Admin Console |
NEXT_PUBLIC_ADMIN_API_URL | Campaign data fetch | Reader Portal |
NEXT_PUBLIC_GA_MEASUREMENT_ID | GA4 analytics | Reader Portal |
SENDGRID_WEBHOOK_VERIFICATION_KEY | Webhook signature verification | Admin Console |
MAILCHIMP_WEBHOOK_KEY | Webhook signature verification | Admin Console |