Developer Documentation

Complete guide to the Bulk Mailer system architecture and API reference.

Getting Started

The Bulk Mailer is a scalable email distribution system built on Cloudflare Workers with Next.js 14 and TypeScript.

Installation

cd bulk-mailer
npm install
npm run dev

Environment Setup

Create a .env.local file with the following variables:

JWT_SECRET=your-secret-here
ENCRYPTION_KEY=your-encryption-key-here
CSRF_SECRET=your-csrf-secret-here
SMTP_SERVER=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USERNAME=apikey
SMTP_PASSWORD=your-sendgrid-api-key
FROM_EMAIL=noreply@yourdomain.com

Development Server

npm run dev

The application will start on http://localhost:3000

System Architecture

The system follows a three-phase architecture optimized for edge computing and scalability.

Phase 1: Foundation and Security

  • JWT-based authentication (stateless)
  • CSRF protection with token validation
  • Security headers via middleware
  • Password hashing with bcryptjs
  • Encryption for sensitive data (AES-256-GCM)
  • Rate limiting (in-memory)

Phase 2: Core Features

  • In-memory database with singleton pattern
  • Email queue processing
  • Campaign management
  • Contact management
  • SMTP provider configuration
  • Template variable substitution

Phase 3: Advanced Features (Planned)

  • Durable Objects for persistent storage
  • Cloudflare KV Store for distributed caching
  • R2 buckets for file storage
  • Bounce handling
  • Delivery tracking
  • Multi-tenant support

Technology Stack

ComponentTechnologyPurpose
RuntimeCloudflare WorkersEdge computing
FrameworkNext.js 14React framework
LanguageTypeScriptType safety
DatabaseDurable ObjectsPhase 3 storage
CacheKV StorePhase 3 caching
EmailNodemailerSMTP sending

Authentication

JWT Flow

  1. User registers with email and password
  2. Password is hashed with bcryptjs (10 rounds)
  3. JWT token is generated with 24-hour expiry
  4. CSRF token is generated for state-changing operations
  5. Tokens are stored in localStorage on client
  6. All requests send JWT in Authorization header

Token Structure

interface JWTPayload {
  userId: string
  email: string
  role: string
  iat: number
  exp: number
}

CSRF Protection

All state-changing operations (POST, PUT, DELETE, PATCH) require CSRF token validation:

  • CSRF tokens are JWT-based (stateless)
  • Generated upon login/register
  • Stored in localStorage as csrf_token
  • Sent in X-CSRF-Token header
  • Verified on backend before processing
  • No session storage required

API Endpoints

Authentication

POST/api/auth/register

Register new user

Request Body:

{
  name: string
  email: string
  password: string
}

Response:

{
  success: boolean
  data: {
    user: User
    token: string
    csrfToken: string
  }
}
POST/api/auth/login

Authenticate user

Request Body:

{
  email: string
  password: string
}

Response:

{
  success: boolean
  data: {
    user: User
    token: string
    csrfToken: string
  }
}

SMTP Providers

GET/api/smtp/providers

List SMTP providers

Authentication: Required

Response:

{
  success: boolean
  data: SMTPProvider[]
}
POST/api/smtp/providers

Create SMTP provider

Authentication: Required + CSRF

Request Body:

{
  name: string
  host: string
  port: number
  username: string
  password: string
  secure: 'tls' | 'ssl'
  dailyLimit: number
}

Response:

{
  success: boolean
  data: SMTPProvider
}

Contacts

GET/api/contacts

List contacts with pagination

Authentication: Required

Query Parameters: page, limit

POST/api/contacts

Create contact

Authentication: Required + CSRF

Request Body:

{
  email: string
  firstName?: string
  lastName?: string
  tags?: string[]
}

Campaigns

GET/api/campaigns

List campaigns with pagination

Authentication: Required

Query Parameters: page, limit

POST/api/campaigns

Create campaign

Authentication: Required + CSRF

Request Body:

{
  name: string
  subject: string
  template: string
  scheduledAt?: string
}

Database Design

Phase 2: In-Memory Database

Current implementation uses in-memory storage with singleton pattern for development and testing.

EntityFieldsIndexed By
Userid, email, passwordHash, createdAtemail
SMTPProviderid, userId, host, port, username, password (encrypted)userId
Contactid, userId, email, firstName, lastName, tags, statususerId, email
Campaignid, userId, name, subject, template, statususerId
EmailQueueid, campaignId, contactId, status, attemptscampaignId

Phase 3: Durable Objects

Persistent storage will use Cloudflare Durable Objects with the following structure:

  • UsersDurableObject: User data and authentication
  • SMTPProvidersDurableObject: SMTP configurations
  • ContactsDurableObject: Contact information
  • CampaignsDurableObject: Campaign definitions
  • EmailQueueDurableObject: Email sending queue

Encryption

Sensitive data (SMTP passwords) is encrypted using AES-256-GCM before storage:

import { encryptData, decryptData } from '@/lib/encryption'

const encrypted = await encryptData(smtpPassword)
const decrypted = await decryptData(encrypted)

Security

Security Headers

HeaderValuePurpose
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForce HTTPS
Content-Security-PolicyRestrictive policyPrevent XSS
X-Frame-OptionsDENYPrevent clickjacking
X-Content-Type-OptionsnosniffPrevent MIME sniffing

Input Validation

All endpoints validate input using Zod schemas:

import { z } from 'zod'

const contactSchema = z.object({
  email: z.string().email('Invalid email'),
  firstName: z.string().optional(),
  tags: z.array(z.string()).default([])
})

const data = contactSchema.parse(body)

Rate Limiting

  • Phase 2: In-memory rate limiting per IP
  • Phase 3: Distributed rate limiting with KV Store
  • Configurable per endpoint
  • Returns 429 Too Many Requests when exceeded

Password Security

  • Hashed with bcryptjs (10 rounds)
  • Never stored in plain text
  • Never transmitted in URLs
  • Always sent over HTTPS only

Testing

Unit Tests

npm test                  # Run all tests
npm run test:watch      # Watch mode
npm run test:coverage   # Coverage report

Test suites: Auth (8 tests), CSRF (7 tests), Utils (7 tests), Encryption (2 tests)

Integration Tests

Test API endpoints with request validation and response verification:

import request from 'supertest'
import { POST } from '@/app/api/contacts/route'

describe('Contacts API', () => {
  it('should create contact with CSRF token', async () => {
    const res = await request(app)
      .post('/api/contacts')
      .set('Authorization', `Bearer ${token}`)
      .set('X-CSRF-Token', csrfToken)
      .send({ email: 'test@example.com' })
    
    expect(res.status).toBe(200)
  })
})

Email Testing

  1. Setup SMTP credentials (Gmail, SendGrid, or Mailgun)
  2. Create test user and SMTP provider
  3. Create test contacts
  4. Create test campaign with template
  5. Trigger email send
  6. Verify email received with variables replaced

Deployment

Prerequisites

  • Cloudflare account
  • wrangler CLI installed
  • Environment variables configured
  • All tests passing

Deployment Steps

  1. Build project: npm run build
  2. Run tests: npm test
  3. Set environment variables in Cloudflare Pages
  4. Deploy: npm run deploy
  5. Verify endpoints accessible
  6. Monitor logs for errors

Environment Variables

Configure these in Cloudflare Pages dashboard, never commit to git:

  • JWT_SECRET: Long random string
  • ENCRYPTION_KEY: 32+ character string
  • CSRF_SECRET: Long random string
  • SMTP_SERVER: Your mail server
  • SMTP_PORT: Usually 587 or 465
  • SMTP_USERNAME: Authentication username
  • SMTP_PASSWORD: Authentication password
  • FROM_EMAIL: Sender email address

Troubleshooting

Common Issues

CSRF Token Missing Error

Ensure X-CSRF-Token header is included in state-changing requests:

fetch('/api/contacts', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify(data)
})

Invalid Email or Password

Check that credentials are correct and account exists. In Phase 2 (in-memory), data is lost on server restart.

Email Not Sending

Verify SMTP credentials, check server logs, ensure contact and campaign exist, and rate limits not exceeded.

TypeScript Errors

npm run type-check

Getting Help

Need help? Reach out through these channels:

  • Email: hello@nsisonglabs.com
  • Twitter: @1cbyc
  • GitHub Issues: github.com/1cbyc/mailer/issues