Developer Quick Reference & Handover Guide

A comprehensive reference for developers working with or taking over the GP3MixMedia TrailMix platform.


Project Overview

AttributeValue
TypePolitical Campaign Media Tracking Platform
FrameworkNext.js 15.5.9 (App Router)
RuntimeNode.js 22.14.0
DatabaseMySQL 8+ with Prisma 7.2.0
LanguageTypeScript 5.9 / React 19.2
DeploymentFly.io (Docker containers)
CacheUpstash Redis (REST API)
StorageAWS S3 / Tigris (S3-compatible)
EmailGmail API (OAuth 2.0)
MonitoringSentry + OpenTelemetry + Prometheus

Key Statistics

MetricCount
Database Models221 (Prisma schema)
API Endpoints286
Frontend Pages42+
External Integrations15+
Scheduled Jobs25
Feed Sources9
Test Files331+

Directory Structure

gp3mixmedia/
├── gp3mixmedia-web/              # Main application
│   ├── app/                      # Next.js App Router
│   │   ├── api/v1/              # 286 REST API endpoints
│   │   ├── (docs)/docs/         # In-app documentation portal
│   │   ├── campaigns/           # Campaign pages
│   │   ├── ingestion/           # Ingestion pages
│   │   ├── dedupe/              # Deduplication pages
│   │   └── admin/               # Admin pages
│   ├── src/
│   │   ├── workers/             # Background jobs & scheduler
│   │   │   ├── feeds/           # Feed implementations (FCC, AdImpact, Meta, etc.)
│   │   │   ├── mailbox/         # Gmail polling (rep sheets, newsletters)
│   │   │   ├── scheduler/       # Cron job configuration
│   │   │   └── index.ts         # Worker entry point
│   │   └── server/              # Server-side utilities
│   ├── lib/                     # Shared libraries & services
│   │   ├── services/            # Business logic (alerts, dedupe, reconciliation)
│   │   └── vendor/              # External API clients (FCC, OpenFEC)
│   ├── components/              # React UI components
│   ├── prisma/
│   │   ├── schema.prisma        # Database schema (221 models)
│   │   └── migrations/          # 153 schema migrations
│   ├── content/docs/            # In-app documentation (markdown files)
│   ├── tests/                   # Test files (331+ files, Vitest)
│   ├── .env.example             # Complete env var reference
│   ├── Dockerfile               # Multi-stage production build
│   └── fly.toml                 # Fly.io web app config
├── deploy/
│   ├── mysql/                   # MySQL + ProxySQL Docker setup
│   │   ├── Dockerfile           # MySQL 8.0.44 image
│   │   └── fly.toml             # Fly.io DB config
│   └── fly/                     # Deployment helpers
├── docs/                        # Extended documentation
│   ├── api/                     # API docs (auth, pagination, errors)
│   ├── adr/                     # Architecture Decision Records
│   ├── runbooks/                # Incident response procedures
│   └── email-parsing/           # Email parsing system docs
├── fly.toml                     # Fly.io database config
└── README.md                    # Project overview

External Dependencies

Infrastructure Services

ServicePurposeRequired?Notes
Fly.ioHosting (web + workers + DB)Yes3 process types: web, ingest, historical-ingest
MySQL 8+Primary databaseYesHosted on Fly.io with ProxySQL multiplexing
Upstash RedisRate limiting, cachingYes (prod)REST-based Redis; falls back to in-memory in dev
AWS S3 / TigrisFile storage (uploads, creatives, PDFs)YesS3-compatible; Tigris used on Fly.io
DockerContainerizationYes (deploy)Multi-stage Dockerfile for app; custom MySQL image
GitHubSource controlYesStandard git workflow

Data Source APIs

ServicePurposeRequired?API Key Variable
Meta Ad LibraryFacebook/Instagram political adsOptionalMETA_ADS_API_TOKEN
Google BigQueryGoogle/YouTube political adsOptionalGOOGLE_CLOUD_PROJECT_ID + credentials
AdImpactIndustry ad intelligenceOptionalADIMPACT_API_TOKEN
OpenFECFederal campaign finance dataOptionalOPENFEC_API_KEY
FEC 24-HourIndependent expenditure filingsOptionalFEC_API_KEY
FCCBroadcast ad disclosuresOptionalNo key needed (HTML scraping)
Google Civic APIBallot info, candidatesOptionalGOOGLE_CIVIC_API_KEY

Communication & Monitoring

ServicePurposeRequired?Config Variable
Gmail APIEmail sending + mailbox pollingYesGMAIL_OAUTH_* + INGESTION_GMAIL_OAUTH_*
SentryError trackingRecommendedSENTRY_DSN
OpenTelemetryDistributed tracingOptionalOTEL_EXPORTER_OTLP_ENDPOINT
SpeechmaticsVideo transcriptionOptionalSPEECHMATICS_API_KEY
DeepgramVideo transcription (alt)OptionalDEEPGRAM_API_KEY
Google Custom SearchWeb search enrichmentOptionalGOOGLE_CUSTOM_SEARCH_API_KEY

Fly.io Architecture

The platform runs as two separate Fly.io apps:

1. Database App (gp3mixmedia)

  • Image: Custom MySQL 8.0.44 + ProxySQL
  • Region: iad (US East)
  • Ports: 3306 (MySQL direct), 6033 (ProxySQL - app should connect here)
  • Storage: 8GB persistent volume at /var/lib/mysql
  • VM: 8GB RAM, 4 performance CPUs

2. Web App (gp3mixmedia-web)

  • Region: iad (US East)
  • Processes:
    • web — Next.js server (port 3000, HTTPS enforced)
    • ingest — Background ingestion workers (cron-based feeds, Gmail polling)
    • historical-ingest — Large backfill jobs (auto-suspends when idle)
  • VM per process: 8GB RAM, 1-2 performance CPUs
  • Deploy: fly deploy runs migrations via release_command
  • Health checks: /api/v1/health (45s timeout), /api/v1/ping (10s timeout)

Deployment Commands

# Deploy web app
cd gp3mixmedia-web && fly deploy

# Deploy database
cd deploy/mysql && fly deploy --app gp3mixmedia

# Set secrets (web app)
fly secrets set META_ADS_API_TOKEN=xxx --app gp3mixmedia-web

# SSH into web app
fly ssh console --app gp3mixmedia-web

# View logs
fly logs --app gp3mixmedia-web
fly logs --app gp3mixmedia-web --process ingest

# Scale machines
fly scale count web=1 ingest=1 --app gp3mixmedia-web

Environment Variables

Required Secrets (set via fly secrets set or .env)

These have no defaults and must be provided:

VariablePurpose
DATABASE_URLMySQL connection string (mysql://USER:PASS@HOST:PORT/DB)
NEXTAUTH_SECRETSession encryption key (generate: openssl rand -base64 32)
AUTH_SECRETAlternative auth secret (same purpose as NEXTAUTH_SECRET)
AUTH_GOOGLE_CLIENT_IDGoogle OAuth client ID for login
AUTH_GOOGLE_CLIENT_SECRETGoogle OAuth client secret
CSRF_SECRETCSRF token signing (generate: openssl rand -base64 32)
TWO_FA_ENCRYPTION_KEY2FA TOTP encryption (min 32 chars)
GMAIL_OAUTH_CLIENT_IDGmail API OAuth client ID (email sending)
GMAIL_OAUTH_CLIENT_SECRETGmail API OAuth client secret
GMAIL_OAUTH_REFRESH_TOKENGmail API refresh token
GMAIL_FROM_EMAILSender email address
INGESTION_GMAIL_OAUTH_CLIENT_IDGmail API OAuth for mailbox polling
INGESTION_GMAIL_OAUTH_CLIENT_SECRETGmail API OAuth secret for polling
INGESTION_GMAIL_OAUTH_REFRESH_TOKENGmail API refresh token for polling
AWS_ACCESS_KEY_IDS3 access key
AWS_SECRET_ACCESS_KEYS3 secret key
S3_BUCKETS3 bucket name
UPSTASH_REDIS_REST_URLUpstash Redis REST endpoint (HTTPS)
UPSTASH_REDIS_REST_TOKENUpstash Redis auth token
REP_LABEL_IDGmail Label ID for rep/buyline messages (use label-utils to find)
NEWSLETTER_LABEL_IDGmail Label ID for newsletter messages

Required for Specific Feeds

VariableFeedNotes
META_ADS_API_TOKENMeta Ads LibraryFacebook access token (see Meta Feed Setup below)
GOOGLE_CLOUD_PROJECT_IDGoogle Political AdsGCP project with BigQuery access
GOOGLE_CLOUD_CREDENTIALS_JSONGoogle Political AdsInline GCP service account JSON
OPENFEC_API_KEYOpenFECGet at api.open.fec.gov
FEC_API_KEYFEC 24-Hour ReportsSame API, separate key recommended
ADIMPACT_API_TOKENAdImpactProvided by AdImpact
ADIMPACT_ENCRYPTION_KEYAdImpactFor encrypting stored credentials
GOOGLE_CIVIC_API_KEYGoogle CivicFor ballot/candidate lookup
SPEECHMATICS_API_KEYTranscriptionFor video transcription
VAPID_PUBLIC_KEYPush NotificationsGenerate with web-push generate-vapid-keys
VAPID_PRIVATE_KEYPush NotificationsGenerate with web-push generate-vapid-keys

Variables with Defaults (override if needed)

VariableDefaultPurpose
NEXTAUTH_URLhttp://localhost:3000Base URL for auth
NEXT_PUBLIC_APP_URLhttp://localhost:3000Public-facing URL
APP_ENVdevelopmentApp environment
NODE_ENVdevelopmentNode environment
TZAmerica/New_YorkServer timezone
LOG_LEVELinfoLog verbosity (debug, info, warn, error)
EMAIL_FROM_NAMEGP3 Mix MediaEmail sender display name
EMAIL_DEFAULT_SEND_MODEqueuedEmail sending mode
GMAIL_DAILY_SEND_LIMIT2000Daily Gmail send cap
WORKER_POOL_SIZE4DB pool size for worker processes
POOL_CPU_MULTIPLIER2DB pool = CPUs x multiplier
VAPID_SUBJECTmailto:[email protected]Push notification identity
PROMETHEUS_PORT9090Metrics port
RATE_LIMIT_STRICTtrue (prod)Reject requests when Redis down
DLQ_ENABLEDtrueDead letter queue
DLQ_MAX_RETRIES3DLQ retry attempts
DLQ_RETENTION_DAYS30DLQ record retention

Variables Set in the UI (Campaign Settings)

These are configured per-campaign through the web interface, not env vars:

SettingLocationPurpose
FCC DMAs/MarketsCampaign Settings > FeedsWhich markets to monitor for FCC filings
FCC Advertiser KeywordsCampaign Settings > FeedsNames as they appear in FCC filings
FCC Regex PatternsCampaign Settings > FeedsCustom matching rules
AdImpact/AIDE Webhook URLCampaign Settings > FeedsAIDE integration endpoint
AdImpact/AIDE TokensCampaign Settings > FeedsWebhook authentication
OpenFEC Committee IDCampaign Settings > FeedsFEC Committee ID (format: C00XXXXXX)
Meta Search TermsCampaign Settings > FeedsKeywords to find relevant Meta ads
Meta Advertiser IDsCampaign Settings > FeedsSpecific Meta advertiser accounts
Google Ads ConfigCampaign Settings > FeedsAdvertiser discovery settings
Alert ThresholdsCampaign Settings > AlertsSpend change notification triggers
Email RecipientsCampaign Settings > AlertsAlert delivery addresses
Mailbox Routing RulesCampaign Settings > FeedsEmail pattern matching rules
EOD Report ScheduleCampaign Settings > ReportsEnd-of-day report timing

Scheduled Jobs (Crons)

Feed Ingestion Schedules

FeedDefault CronEnv OverrideDescription
AdImpact15 * * * *ADIMPACT_CRONHourly at :15
FCC Political Files45 */6 * * *FCC_CRONEvery 6 hours at :45
Google Political Ads0 6,14,22 * * *GOOGLE_POLITICAL_ADS_CRON3x daily (6 AM, 2 PM, 10 PM)
FEC 24-Hour Reports0 */4 * * *FEC_24H_CRONEvery 4 hours
Meta Ads Library0 */8 * * *META_ADS_CRONEvery 8 hours
State Press RSS0 */6 * * *STATE_PRESS_RSS_CRONEvery 6 hours
Feed Retry Queue*/10 * * * *FEED_RETRY_PROCESS_CRONEvery 10 minutes

Background Worker Schedules

JobDefault CronEnabled FlagDescription
Email Queue*/5 * * * *EMAIL_QUEUE_ENABLED (true)Process queued emails
Pre-Buy Alerts*/10 * * * *PRE_BUY_ALERTS_ENABLED (true)Process alert triggers
Social Creative Discovery5,35 * * * *SOCIAL_CREATIVE_DISCOVERY_ENABLED (true)Find social ad creatives
Creative Followup10,40 * * * *CREATIVE_FOLLOWUP_ENABLED (true)Follow up on creatives
Social Monitor (X/Twitter)15,45 * * * *SOCIAL_MONITOR_ENABLED (true)Monitor social feeds
Radio Creative Discovery20,50 * * * *RADIO_CREATIVE_DISCOVERY_ENABLED (true)Find radio creatives
AdImpact Creative Download25,55 * * * *ADIMPACT_CREATIVE_DOWNLOAD_ENABLED (true)Download ad creatives
AdImpact Transcription10 * * * *ADIMPACT_CREATIVE_TRANSCRIBE_ENABLED (true)Transcribe creatives
FCC Coverage Monitoring25 * * * *FCC_COVERAGE_MONITORING_ENABLED (true)Verify FCC coverage
Station Refresh30 7 * * *STATION_REFRESH_ENABLED (true)Refresh station rosters
EOD Reports0 * * * *EOD_REPORT_ENABLED (true)Process EOD reports
EOD Scheduling0 2 * * *EOD_SCHEDULING_ENABLED (true)Pre-generate EOD reports
Candidate Roster0 6 * * *CANDIDATE_ROSTER_MONITOR_ENABLED (true)Check Ballotpedia rosters
Dashboard Stats*/7 * * * *DASHBOARD_STATS_ENABLED (true)Aggregate dashboard metrics
Discovery Refresh0 2 * * 0DISCOVERY_REFRESH_ENABLED (true)Weekly discovery refresh
AdImpact Reconciliation0 2 * * 0ADIMPACT_FULL_RECONCILIATION_ENABLED (true)Weekly full resync
Dedupe Auto-Resolver0 */2 * * *DEDUPE_AUTO_RESOLVER_ENABLED (false)Auto-resolve high-confidence
Parser Success Monitoring15 8 * * *PARSER_SUCCESS_RATE_MONITORING_ENABLED (true)Daily parser health

Health & Monitoring Schedules

JobDefault CronEnabled FlagDescription
DLQ Auto-Dismiss30 */4 * * *DLQ_AUTO_DISMISS_ENABLED (true)Auto-dismiss stale DLQ
DLQ Cleanup0 3 * * *DLQ_CLEANUP_ENABLED (true)Clean old DLQ records
DLQ Monitor*/15 * * * *DLQ_MONITOR_ENABLED (true)Monitor DLQ health
Queue Health Check*/15 * * * *QUEUE_HEALTH_CHECK_ENABLED (true)Check queue health
Stale Watermark Monitor*/30 * * * *STALE_WATERMARK_MONITOR_ENABLED (true)Detect stale feeds
Email Ingestion Watchdog*/2 * * * *EMAIL_INGESTION_WATCHDOG_ENABLED (true)Email polling health
MySQL Connection Monitor* * * * *MYSQL_CONNECTION_MONITOR_ENABLED (true)DB connection limits

Polling Workers (non-cron)

WorkerDefault IntervalEnv OverrideDescription
Gmail Polling (reps)60sINGESTION_GMAIL_POLL_INTERVALPoll for rep emails
Newsletter Polling300s (5 min)NEWSLETTER_GMAIL_POLL_INTERVALPoll for newsletters
IMAP Polling60sINGESTION_MAIL_POLL_INTERVALPoll IMAP inbox
Input Parse15sINPUT_PARSE_POLL_INTERVALProcess uploaded files
RSS Feeds300sRSS_POLL_INTERVALPoll RSS feeds
S3 Recovery30sS3_RECOVERY_POLL_INTERVALRetry failed S3 uploads

Meta Ads Library Feed Setup

The Meta Ads Library feed pulls political/issue ads from Facebook and Instagram.

Prerequisites

  1. Facebook Developer Account with access to the Ad Library API
  2. Facebook Access Token with ads_read permission
    • Go to Meta for Developers
    • Create an app or use an existing one
    • Generate a long-lived access token with ads_read scope
    • The token must belong to an account approved for the Ad Library API

Environment Configuration

Set the access token as a secret:

# On Fly.io
fly secrets set META_ADS_API_TOKEN=your_facebook_access_token --app gp3mixmedia-web

# In .env for local development
META_ADS_API_TOKEN=your_facebook_access_token

The following env vars have sensible defaults but can be overridden:

VariableDefaultPurpose
META_ADS_CRON0 */8 * * *Feed schedule (every 8 hours)
META_ADS_API_VERSIONv20.0Graph API version
META_ADS_PER_PAGE100Results per API page (max 250)
META_ADS_MAX_PAGES10Max pages per search term
META_ADS_SEARCH_COUNTRIESUSCountry filter
META_ADS_AD_TYPEPOLITICAL_AND_ISSUE_ADSAd type filter
META_ADS_MAX_RETRIES3Retry attempts (max 10)
META_ADS_RETRY_DELAY_MS2000Base retry delay
META_ADS_REQUEST_TIMEOUT_MS60000Request timeout (max 120s)
META_ADS_INTER_REQUEST_DELAY_MS1000Delay between requests
META_ADS_LOOKBACK_DAYS21Minimum lookback window
META_ADS_ALLOW_KEYWORD_FALLBACKtrueFall back to keyword search
META_ADS_ALLOW_AUTO_DERIVE_TERMStrueAuto-derive search terms from campaign
META_ADS_REDUCED_FIELDSfalseUse lite field set (omits demographics)

Campaign-Level Configuration

After the env var is set, configure per-campaign in the UI:

  1. Navigate to Campaign Settings > Feeds > Meta Ads Library
  2. Configure:
    • Search Terms: Keywords to find relevant ads (leave empty for auto-derive from campaign name/candidates)
    • Advertiser IDs: Optional — specific Meta advertiser page IDs to monitor
    • Geographic Scope: Countries/regions to include

How It Works

  1. The feed runs on the META_ADS_CRON schedule (default: every 8 hours)
  2. For each campaign with Meta enabled:
    • Uses search terms (configured or auto-derived from campaign candidates)
    • Queries the Meta Ad Library API for political/issue ads matching those terms
    • Applies keyword fallback if primary search returns few results
    • Converts matching ads to buy line candidates
    • Runs deduplication against existing records
  3. Circuit breaker protects against API failures with exponential backoff
  4. Partial results are saved on timeouts (graceful degradation)

Key Files

  • Feed implementation: src/workers/feeds/meta-ads-library.ts
  • API client: lib/services/social-creative/meta-ads-library.client.ts

Local Development Setup

Prerequisites

  • Node.js 22.14.0+ (use nvm or fnm)
  • MySQL 8+ (local or Docker)
  • Redis (optional for dev — falls back to in-memory)

Quick Start

# 1. Clone and install
git clone <repo-url> && cd gp3mixmedia/gp3mixmedia-web
npm install

# 2. Set up environment
cp .env.example .env
# Edit .env with your DATABASE_URL, auth secrets, etc.

# 3. Set up database
npx prisma migrate deploy    # Apply all migrations
npx prisma generate          # Generate Prisma client

# 4. Start development
npm run dev                  # Next.js dev server on :3000

# 5. Start workers (separate terminal)
npm run ingest               # Background workers

Common Commands

# Development
npm run dev                     # Start dev server
npm run ingest                  # Start ingestion workers
npm run build                   # Production build
npm start                       # Production server

# Database
npx prisma migrate deploy      # Apply migrations
npx prisma migrate dev          # Create new migration
npx prisma studio               # Visual database browser
npx prisma generate             # Regenerate client after schema changes

# Testing
npm test                        # Run all tests (Vitest)
npm run test:watch              # Watch mode
npm run build                   # Verify no build errors (important for circular deps)

# Gmail Label Discovery
npx tsx src/workers/mailbox/label-utils.ts   # List Gmail label IDs

# Deployment
fly deploy --app gp3mixmedia-web             # Deploy web app
fly secrets set KEY=VALUE --app gp3mixmedia-web  # Set secrets

API Endpoints Quick Reference

Core

GET/POST   /api/v1/campaigns
GET/PATCH  /api/v1/campaigns/{id}
POST       /api/v1/campaigns/{id}/clone

Matrix

GET        /api/v1/matrix
POST       /api/v1/matrix/batch-update
GET        /api/v1/matrix/export

Deduplication

GET        /api/v1/dedupe/groups
POST       /api/v1/dedupe/groups/{id}/resolve
POST       /api/v1/dedupe/rows/combine

Ingestion

GET/POST   /api/v1/ingestion/control
POST       /api/v1/aide/inbound          # AIDE webhook endpoint

Email & Notifications

POST       /api/v1/email/send
GET        /api/v1/email/logs
POST       /api/v1/push/subscribe
POST       /api/v1/push/send

Health & Monitoring

GET        /api/v1/ping                  # Liveness check
GET        /api/v1/health                # Full health check
GET        /api/v1/observability/metrics  # Prometheus metrics
GET        /api/v1/circuit-breakers       # Circuit breaker status

Interactive API Docs

  • Swagger UI: /api/docs
  • OpenAPI Spec: /api/v1/openapi.json

Key Design Patterns

Circular Dependency Prevention

Next.js builds can fail with ReferenceError: Cannot access 'X' before initialization due to circular imports. Always use dynamic imports in API route files for service/worker modules:

// WRONG — static import causes TDZ errors
import { someService } from '@/lib/services/some-service';

// RIGHT — dynamic import breaks circular chain
async function getSomeService() {
  const { someService } = await import('@/lib/services/some-service');
  return someService;
}

Soft Deletes

High-value tables use soft deletes (archived, not deleted). Data can always be recovered from the archives.

Feed Distributed Locks

Redis-based locks prevent duplicate feed runs across processes. Lock TTL defaults to 2700s with 60s heartbeat.

Circuit Breaker

External API calls use circuit breakers for resilience. Status viewable at /api/v1/circuit-breakers.


Quick Troubleshooting

IssueSolution
Build fails with TDZ errorUse dynamic imports in API routes (see pattern above)
Tests failingRun npx prisma generate then npm test
Database connection errorCheck DATABASE_URL format; ensure MySQL is running
Missing Prisma clientRun npx prisma generate
S3 upload failsCheck AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET
Push notifications brokenCheck VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY
Rate limiting issuesVerify UPSTASH_REDIS_REST_URL and token
Gmail polling not startingCheck REP_LABEL_ID / NEWSLETTER_LABEL_ID are set
Meta feed not runningVerify META_ADS_API_TOKEN is set; check token expiry
Feed stuck / lockedCheck Redis lock status; feed locks expire after 45 min

Security Features

  • Google Workspace SSO (NextAuth.js)
  • Role-based access control (Viewer, Analyst, Admin) with per-campaign permissions
  • CSRF protection with signed tokens
  • Optional 2FA (TOTP authenticator apps)
  • API rate limiting via Upstash Redis (configurable per tier)
  • Request idempotency to prevent duplicate processing
  • Soft-delete with recovery on all high-value data
  • AES-256-GCM encryption for stored API tokens
  • Comprehensive audit trails
  • Sentry error tracking and alerting
  • Circuit breaker pattern for external API resilience

Monitoring Endpoints

EndpointPurpose
/api/v1/pingLiveness check (fast)
/api/v1/healthFull health check (DB, Redis, S3)
/api/v1/observability/metricsPrometheus metrics
/api/v1/circuit-breakersCircuit breaker status
/api/v1/service-healthExternal service status

Last Updated: March 2026

Was this helpful? If you have feedback or questions, please contact your administrator.