Skip to main content

Newsletter Subscription Implementation Guide

Overview​

This guide documents the complete newsletter subscription system that allows users to subscribe to newsletters from the website footer (and potentially other sources). All subscriber emails are stored securely in Supabase with proper RLS policies to protect user privacy.

Features Implemented​

1. Database Schema​

  • Table: newsletter_subscribers
  • Location: supabase/schema.sql and supabase/migrations/20251103_newsletter_subscribers.sql
  • Fields:
    • id (UUID, primary key)
    • email (text, unique, required)
    • status (text, 'active' or 'unsubscribed')
    • source (text, defaults to 'footer')
    • subscribed_at (timestamptz)
    • unsubscribed_at (timestamptz, nullable)
    • created_at (timestamptz)
    • updated_at (timestamptz)

2. Security & Privacy (RLS Policies)​

  • Anonymous users: Can INSERT (subscribe) but CANNOT read entries
  • Service role: Full access (read, update, delete)
  • Email protection: User emails are never exposed to anonymous users
  • Duplicate prevention: Unique constraint on email field
  • Reactivation support: Can reactivate unsubscribed emails

3. Backend API Routes​

  • File: src/routes/newsletter.py
  • Endpoints:
    • POST /api/newsletter/subscribe - Subscribe to newsletter
    • POST /api/newsletter/unsubscribe - Unsubscribe from newsletter
    • GET /api/newsletter/check?email=<email> - Check subscription status

4. Frontend Service​

  • File: src/services/newsletter.js
  • Functions:
    • subscribeToNewsletter({ email, source }) - Subscribe to newsletter
    • unsubscribeFromNewsletter({ email }) - Unsubscribe from newsletter
    • checkNewsletterStatus({ email }) - Check if email is subscribed

5. UI Integration​

  • File: components/Footer.jsx
  • Features:
    • Real-time subscription form in footer
    • Loading states (idle, submitting, success, error)
    • Error message display
    • Success confirmation
    • Email validation
    • Disabled state during submission

Database Setup​

Step 1: Apply the Migration​

Run the migration to create the table:

# Using Supabase CLI
supabase db push

# Or manually in Supabase SQL Editor
# Execute: supabase/migrations/20251103_newsletter_subscribers.sql

Step 2: Verify Table Creation​

-- Check if table exists
SELECT * FROM public.newsletter_subscribers LIMIT 1;

-- Check RLS policies
SELECT * FROM pg_policies WHERE tablename = 'newsletter_subscribers';

API Usage Examples​

Subscribe to Newsletter​

curl -X POST https://your-domain.com/api/newsletter/subscribe \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"source": "footer"
}'

Response (201 Created):

{
"success": true,
"message": "Successfully subscribed to newsletter!",
"subscriber": {
"email": "user@example.com",
"subscribed_at": "2025-11-03T12:00:00Z"
}
}

Response (200 OK - Already Subscribed):

{
"success": true,
"message": "You are already subscribed to our newsletter!",
"already_subscribed": true
}

Check Subscription Status​

curl -X GET "https://your-domain.com/api/newsletter/check?email=user@example.com"

Response:

{
"subscribed": true,
"status": "active",
"subscribed_at": "2025-11-03T12:00:00Z"
}

Unsubscribe from Newsletter​

curl -X POST https://your-domain.com/api/newsletter/unsubscribe \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'

Response:

{
"success": true,
"message": "Successfully unsubscribed from newsletter"
}

Frontend Integration​

Basic Usage​

import { subscribeToNewsletter } from '../src/services/newsletter';

// Subscribe
try {
const response = await subscribeToNewsletter({
email: 'user@example.com',
source: 'footer'
});
console.log('Subscribed:', response);
} catch (error) {
console.error('Subscription failed:', error.message);
}

React Component Example​

import { useState } from 'react';
import { subscribeToNewsletter } from '../src/services/newsletter';

function NewsletterForm() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState('idle');
const [message, setMessage] = useState('');

const handleSubmit = async (e) => {
e.preventDefault();
setStatus('submitting');

try {
const response = await subscribeToNewsletter({ email, source: 'modal' });
setStatus('success');
setMessage(response.message);
setEmail('');
} catch (error) {
setStatus('error');
setMessage(error.message);
}
};

return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={status === 'submitting'}
required
/>
<button type="submit" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Subscribing...' : 'Subscribe'}
</button>
{message && <p>{message}</p>}
</form>
);
}

Testing Guide​

Manual Testing Steps​

  1. Test Subscription Flow:

    • Navigate to any page with the footer
    • Scroll to the newsletter subscription section
    • Enter a valid email address
    • Click "Subscribe"
    • Verify success message appears
    • Check Supabase dashboard for the new entry
  2. Test Duplicate Prevention:

    • Try subscribing with the same email again
    • Should show "already subscribed" message
  3. Test Validation:

    • Try submitting without an email (should be blocked by HTML5)
    • Try submitting an invalid email format
    • Should show error message
  4. Test Error Handling:

    • Temporarily disable backend/database
    • Try subscribing
    • Should show error message and not crash

Database Queries for Testing​

-- View all active subscribers
SELECT email, source, subscribed_at
FROM newsletter_subscribers
WHERE status = 'active'
ORDER BY subscribed_at DESC;

-- Count subscribers by source
SELECT source, COUNT(*) as count
FROM newsletter_subscribers
WHERE status = 'active'
GROUP BY source;

-- View recently subscribed
SELECT email, subscribed_at
FROM newsletter_subscribers
WHERE status = 'active'
AND subscribed_at > NOW() - INTERVAL '24 hours'
ORDER BY subscribed_at DESC;

-- Find specific email
SELECT * FROM newsletter_subscribers
WHERE email = 'user@example.com';

Exporting Subscriber Data​

For Email Marketing Campaigns​

-- Export all active subscribers
COPY (
SELECT email, subscribed_at, source
FROM newsletter_subscribers
WHERE status = 'active'
ORDER BY subscribed_at DESC
) TO '/tmp/newsletter_subscribers.csv' WITH CSV HEADER;

Via Supabase Dashboard​

  1. Go to Table Editor
  2. Select newsletter_subscribers table
  3. Use filters: status equals active
  4. Export as CSV

Integration with Email Marketing Tools​

The newsletter subscriber data can be exported and used with:

  • Mailchimp
  • SendGrid
  • ConvertKit
  • Customer.io
  • Brevo (formerly Sendinblue)
  • Any ESP supporting CSV imports

Monitoring & Analytics​

Key Metrics to Track​

-- Total active subscribers
SELECT COUNT(*) as total_subscribers
FROM newsletter_subscribers
WHERE status = 'active';

-- Subscription rate by source
SELECT
source,
COUNT(*) as subscriptions,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2) as percentage
FROM newsletter_subscribers
WHERE status = 'active'
GROUP BY source;

-- Unsubscribe rate
SELECT
COUNT(CASE WHEN status = 'unsubscribed' THEN 1 END) as unsubscribed,
COUNT(*) as total,
ROUND(COUNT(CASE WHEN status = 'unsubscribed' THEN 1 END) * 100.0 / COUNT(*), 2) as unsubscribe_rate
FROM newsletter_subscribers;

-- Daily subscription trend
SELECT
DATE(subscribed_at) as date,
COUNT(*) as new_subscribers
FROM newsletter_subscribers
WHERE subscribed_at > NOW() - INTERVAL '30 days'
GROUP BY DATE(subscribed_at)
ORDER BY date DESC;

Future Enhancements​

Potential Additions​

  1. Email Preferences: Let users choose topics/frequency
  2. Double Opt-In: Send confirmation email before activating
  3. Unsubscribe Link: Add unsubscribe functionality via email link
  4. Subscriber Segments: Tag subscribers by interests
  5. Welcome Email: Automatically send welcome email on subscription
  6. Admin Dashboard: View/manage subscribers in admin panel
  7. Export API: Build API endpoint for exporting subscriber lists
  8. Webhook Integration: Send subscriber data to external services

Troubleshooting​

Issue: Subscription fails silently​

  • Check browser console for errors
  • Verify API endpoint is accessible
  • Check Supabase connection
  • Verify RLS policies are set correctly

Issue: Duplicate email error​

  • This is expected behavior
  • The system handles it gracefully by showing "already subscribed"

Issue: Cannot read subscribers from admin​

  • Ensure you're using service_role key for admin access
  • Check RLS policies allow service_role to read

Issue: Migration fails​

  • Ensure set_updated_at() function exists
  • Run the base schema first
  • Check for naming conflicts

Security Considerations​

  1. Email Privacy: Emails are never exposed to anonymous users
  2. SQL Injection: All queries use parameterized statements
  3. Rate Limiting: Consider adding rate limiting on the API endpoint
  4. Email Validation: Server-side validation prevents invalid emails
  5. GDPR Compliance: Users can request data deletion via unsubscribe
  6. Data Export: Keep audit logs for compliance

Maintenance​

Regular Tasks​

  • Weekly: Review new subscribers, check for spam emails
  • Monthly: Export subscriber list for backup
  • Quarterly: Analyze subscription trends and sources
  • Annually: Clean up very old unsubscribed records (with proper retention policy)

Support​

For issues or questions:

  • Check Supabase logs for errors
  • Review Flask application logs
  • Test API endpoints directly with curl/Postman
  • Verify database schema and RLS policies

Implementation Date: November 3, 2025
Last Updated: November 3, 2025
Version: 1.0.0