CCA Campaign Manager is a production-oriented email campaign platform built for Brevo-backed delivery, typed template management, staged CSV/XLSX imports, queue-based sending, and a premium dashboard workflow.
- Why This Project Exists
- What You Get
- Architecture Overview
- Frontend and UX Principles
- Feature Breakdown
- Project Structure
- Environment Configuration
- Run with Docker Compose
- Run Locally Without Docker
- Brevo and Sender Configuration
- Database Migration
- Testing and Verification
- Security Notes
- Operational Notes
- Troubleshooting
- Recommended Commands
- License
This repository is built to solve the real operational problems around outbound email campaigns:
- template creation should be versioned and safe to edit
- imports should be validated against the selected template before launch
- sends should be deterministic and tied to the published template version used at launch time
- delivery credentials should support secure server-side environment configuration
- the dashboard should remain premium and user-friendly without becoming fragile
The result is not an MVP shell. It is a structured application with:
- a route-based web frontend
- secure cookie auth
- typed API contracts
- campaign batch staging
- queue-backed delivery
- contact unsubscribe handling
- Postgres persistence
- Redis-backed worker/runtime support
- Next.js + TypeScript frontend with route-based pages
- FastAPI API backend under
/api/v1 - secure cookie auth with CSRF protection and refresh-session rotation
- Postgres as the primary system of record
- Redis-backed queue/runtime path for asynchronous work
- Dramatiq workers for background delivery
- template draft/publish lifecycle
- builder mode and raw HTML/code mode template support
- template-aware CSV/XLSX/XLS import analysis and validation
- staged batches and explicit launch flow
- activity tracking with per-recipient state
- unsubscribe-safe delivery behavior
- environment-driven sender configuration
- support for server-side Brevo API key usage without exposing the secret in the browser UI
- local asset serving via
PUBLIC_BASE_URL - deterministic sending against the locked template version snapshot
- fallback inline scheduler support when queue mode is disabled
- SQLite-to-Postgres migration utility
- Alembic migrations for evolving the schema
Next.js Web App
├─ Login / Signup / Dashboard / Templates / Contacts / Unsubscribe
├─ TanStack Query for typed data fetching
└─ Cookie-authenticated API calls
FastAPI API
├─ /api/v1/auth/*
├─ /api/v1/settings
├─ /api/v1/templates/*
├─ /api/v1/imports/*
├─ /api/v1/batches/*
├─ /api/v1/activity
└─ /api/v1/contacts/*
Persistence and runtime
├─ Postgres: users, settings, templates, versions, contacts, batches, recipients, sessions
├─ Redis: queue transport, locks, counters, ephemeral coordination
└─ Dramatiq: background delivery and async jobs
The current UI look and feel is intentionally preserved. This repository is not trying to redesign the product into a generic admin panel.
Visual constraints carried forward into the new frontend:
- dark premium background palette
- purple-blue glow accents
- glassmorphism-inspired cards and panels
- rounded bento-style surfaces
- strong contrast for white-on-dark content
- intentional dashboard spacing and hierarchy
Technical improvements were made underneath the visual layer:
- typed components instead of raw page scripts
- route-based pages instead of a single monolithic dashboard file
- reusable primitives for auth, cards, tables, badges, and forms
- better state handling for async workflows
- login and signup flows use secure cookies
- refresh sessions rotate instead of relying on persistent browser tokens
- CSRF protection is enforced for mutating API requests
- session invalidation supports logout and logout-all patterns
- password hashing prefers Argon2id with legacy bcrypt fallback support
- create and manage templates from the dashboard
- maintain separate draft and published versions
- preview and test send before publishing
- preserve merge-field schema with each version
- support imported HTML templates without degrading them into broken builder output
- allow builder-style and raw code-mode editing paths
- upload CSV, XLSX, or XLS
- choose the target published template
- detect fields and map columns
- validate rows against required template fields
- surface row-level errors
- stage batches before launch
- launch only after validation passes
- recipients move through staged, queued, processing, sent, failed, and unsubscribed states
- queue workers process recipients asynchronously
- sends use the batch’s locked template version
- unsubscribe state is checked before sending
- contact delivery metadata is updated after send/failure
- store per-account limits and template defaults
- keep Brevo API keys in the database when desired
- optionally use
BREVO_SMTP_API_KEY,SENDER_EMAIL, andSENDER_NAMEdirectly from the server environment - prevent the dashboard from exposing the env Brevo API key value
cca-mail-sender/
├── alembic/ # Alembic migration environment and revisions
├── app/ # FastAPI app, models, auth, services, routers, queue integration
│ ├── routers/
│ ├── services/
│ ├── auth.py
│ ├── config.py
│ ├── database.py
│ ├── main.py
│ └── models.py
├── data/ # Local templates, imports, asset storage, error reports
├── scripts/ # Migration and maintenance scripts
├── tests/ # Backend tests
├── web/ # Next.js frontend
│ ├── app/
│ ├── components/
│ ├── lib/
│ └── public/
├── .env.example # Documented environment sample
├── Dockerfile # API/worker image
├── docker-compose.yml # Full local stack
├── requirements.txt # Python dependencies
└── LICENSE
Copy the sample file first:
cp .env.example .envThe sample file is documented with sections and comments. The most important variables are:
APP_NAME=CCA Campaign Manager
SECRET_KEY=replace-with-a-long-random-secret
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=30DATABASE_URL=postgresql+psycopg://cca:cca@127.0.0.1:5432/cca_mail_sender
REDIS_URL=redis://127.0.0.1:6379/0
QUEUE_BACKEND=dramatiqWEB_ORIGIN=http://127.0.0.1:3000
PUBLIC_BASE_URL=http://127.0.0.1:8000
NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8000
SECURE_COOKIES=falseBREVO_SMTP_API_KEY=
SENDER_EMAIL=
SENDER_NAME=When these three env values are present, the dashboard settings page can use them directly. The UI exposes a “use .env” mode for Brevo and sender identity, while keeping manual per-user settings available too.
This is the preferred full-stack local run:
docker compose up --buildThat starts:
postgresredisapiworkerweb
Expected local URLs:
- Web: http://127.0.0.1:3000
- API: http://127.0.0.1:8000
- Health check: http://127.0.0.1:8000/healthz
- the API and worker both read from environment values
docker-compose.ymlnow forwardsBREVO_SMTP_API_KEY,SENDER_EMAIL, andSENDER_NAME- the API and worker run
alembic upgrade headbefore starting NEXT_PUBLIC_API_BASE_URLis passed to the web container
python -m venv venv
source venv/bin/activatepip install -r requirements.txtIf you already use local services, point .env at them. Otherwise, you can still use Compose just for infrastructure:
docker compose up -d postgres redisalembic upgrade headuvicorn app.main:app --reloadIf your environment blocks file watcher hooks, use:
WATCHFILES_FORCE_POLLING=1 uvicorn app.main:app --reloaddramatiq app.taskscd web
npm install
npm run devThere are now two supported configuration paths.
Set these in .env:
BREVO_SMTP_API_KEY=your-real-brevo-key
SENDER_EMAIL=you@example.com
SENDER_NAME=Your CompanyThen in the dashboard settings page:
- enable the “use
.env” option for Brevo - enable the “use
.env” option for sender identity
Benefits:
- the Brevo key does not need to be typed into the UI
- server-managed credentials can be shared across operators
- the frontend never receives the raw env key
Keep the env fields empty and enter:
- Brevo API key
- sender email
- sender name
in the dashboard settings form for that account.
- if env defaults exist and no manual value is stored yet, the backend can automatically use the env value
- the batch snapshot stores sender identity at stage time
- test-send and delivery both resolve settings through the same runtime path
If you have older local SQLite data and want to move it into Postgres:
TARGET_DATABASE_URL=postgresql+psycopg:///cca_mail_sender \
venv/bin/python scripts/migrate_sqlite_to_postgres.pyThe migration utility:
- prepares the source SQLite schema if older columns are missing
- recreates the target schema from current SQLModel metadata
- copies users, settings, templates, versions, contacts, batches, recipients, sessions, and jobs
After migration, apply Alembic:
alembic upgrade headRecommended verification commands:
venv/bin/python -c "import app.main; print('import ok')"
venv/bin/python -m pytest
venv/bin/python -m compileall app scripts tests
cd web && npm run buildWhat these checks cover:
- API imports and startup wiring
- backend unit/integration coverage currently in
tests/ - Python syntax compilation across app code
- TypeScript/Next production build safety
This project includes several security-oriented defaults:
- secure cookie auth instead of browser-stored auth tokens
- CSRF protection on state-changing requests
- Argon2id password hashing when available
- session rotation and revocation support
- per-user data isolation across API routes
- environment-backed secret handling for Brevo credentials
- unsubscribe-aware delivery suppression
- no raw env Brevo key sent back to the frontend
Set:
QUEUE_BACKEND=dramatiqfor Redis-backed background delivery.
If you ever disable it, the inline scheduler path can still run inside the API process, but Redis + Dramatiq is the intended production setup.
Local asset upload is enabled when PUBLIC_BASE_URL is present. Uploaded files are served from:
/public-assets/<user-id>/<filename>
Each user account gets a default template bootstrap path if none exists. The dashboard also allows selecting a default template for convenience.
Check:
.envexists and is valid- Postgres is reachable
data/public_assetscan be created
If the reloader itself is blocked:
WATCHFILES_FORCE_POLLING=1 uvicorn app.main:app --reloadVerify at least one valid path exists:
- manual Brevo key + manual sender email
- or env-backed
BREVO_SMTP_API_KEY+SENDER_EMAIL
Confirm:
- API is running on port
8000 NEXT_PUBLIC_API_BASE_URLis correct for your modeWEB_ORIGINmatches the web origin
Check:
redisis healthyworkeris runningBREVO_SMTP_API_KEY,SENDER_EMAIL, andSENDER_NAMEare available in the API and worker environment
docker compose up --builduvicorn app.main:app --reloaddramatiq app.tasksvenv/bin/python -m pytestcd web && npm run buildThis project is licensed under the MIT License.
If you are using or extending this repository internally, keep the environment values in .env only and never commit real secrets to version control.