Campaign System
Quick reference for AI agents working on campaign features.
Key Files
| Purpose | Location |
|---|---|
| List Component | apps/frontend/src/components/campaigns/CampaignList.tsx |
| Detail Component | apps/frontend/src/components/campaigns/CampaignDetail.tsx |
| Planning Chat | apps/frontend/src/components/campaigns/CampaignChat.tsx |
| Selector Dropdown | apps/frontend/src/components/CampaignSelector.tsx |
| Backend Routes | apps/backend/src/routes/campaigns.ts |
| Service Layer | apps/backend/src/services/campaign.service.ts |
Data Model
typescript
// Prisma model: CompanyCampaign
interface Campaign {
id: string;
companyId: string;
userId: string;
name: string;
brief: string | null;
goal: string | null;
type: 'ONE_TIME' | 'RECURRING' | 'EVERGREEN';
status: 'DRAFT' | 'ACTIVE' | 'PAUSED' | 'COMPLETED';
startsAt: Date | null;
endsAt: Date | null;
// Lifecycle tracking
pausedAt: Date | null;
resumedAt: Date | null;
completedAt: Date | null;
lastStatusChangedAt: Date | null;
isArchived: boolean; // Separate from status!
context: CampaignContext | null; // JSON field
planningHistory: object[] | null; // Chat history
}IMPORTANT
isArchived is a SEPARATE BOOLEAN, not a status. Archived campaigns retain their actual status.
Context Pattern (Important!)
The context field uses Defaults + Overrides:
typescript
interface CampaignContext {
keyMessages?: string[]; // Campaign-specific only
targetAudience?: string; // Overrides BrandProfile.targetAudience
tone?: string; // Overrides BrandProfile.voiceTone
channels?: string[]; // Overrides connected account platforms
}Rule: Absent fields inherit from Brand Profile. UI shows "Brand Default" badges.
API Endpoints
typescript
// List campaigns (excludes archived by default)
GET /api/campaigns?status=ACTIVE&type=ONE_TIME&search=q&includeArchived=false
// CRUD
POST /api/campaigns // Create
GET /api/campaigns/:id // Read
PUT /api/campaigns/:id // Update
DELETE /api/campaigns/:id?action=delete_all // Delete
// Content
GET /api/campaigns/:id/content // All linked content
GET /api/campaigns/:id/analytics // Aggregated performance metrics
// Lifecycle
POST /api/campaigns/:id/resume // Resume with strategy
POST /api/campaigns/:id/archive // Archive/unarchive
// AI Features
POST /api/campaigns/chat // Planning conversation
POST /api/campaigns/:id/generate-plan // Extract plan from chat
POST /api/campaigns/:id/generate-content // Generate content drafts
// Bulk Operations
POST /api/campaigns/:id/duplicate // Deep copy campaign
POST /api/campaigns/:id/find-replace // Bulk text updateCommon Patterns
Tenant Scoping
typescript
const tenantPrisma = getTenantPrisma(request.user.companyId);
const campaigns = await tenantPrisma.companyCampaign.findMany({...});Archive Filtering
typescript
// Default behavior excludes archived
const where: any = { companyId };
if (!includeArchived) {
where.isArchived = false;
}Context Resolution (Frontend)
typescript
// Display effective value with fallback
const audience = campaign.context?.targetAudience
|| brandProfile?.targetAudience
|| 'General Audience';
// Show badge if using default
const isDefault = !campaign.context?.targetAudience;Key Components
| Purpose | Location |
|---|---|
| List Component | apps/frontend/src/components/campaigns/CampaignList.tsx |
| Detail Component | apps/frontend/src/components/campaigns/CampaignDetail.tsx |
| Resume Modal | apps/frontend/src/components/campaigns/ResumeStrategyModal.tsx |
| Duplicate Modal | apps/frontend/src/components/campaigns/DuplicateCampaignModal.tsx |
| Find & Replace Modal | apps/frontend/src/components/campaigns/FindReplaceModal.tsx |
| Backend Routes | apps/backend/src/routes/campaigns.ts |
| Ops Service | apps/backend/src/services/campaign-ops.service.ts |
| Lifecycle Service | apps/backend/src/services/campaign-lifecycle.service.ts |
Related Systems
- Brand Profile provides default audience/voice
- Connected Accounts provide default channels
- SchedulingConfig provides flood prevention settings
- CompanyPost / CompanyBlogPost / CompanyVideoScript link via
campaignId