Overview
The Campaign system organizes marketing content around strategic initiatives. Each campaign links to multiple content types and inherits context from the Brand Profile.
Data Model
erDiagram
CompanyCampaign {
uuid id PK
string name
text brief
string goal
enum type "ONE_TIME|RECURRING|EVERGREEN"
enum status "DRAFT|ACTIVE|PAUSED|COMPLETED"
datetime startsAt
datetime endsAt
datetime pausedAt
datetime resumedAt
datetime completedAt
datetime lastStatusChangedAt
boolean isArchived
json context
json planningHistory
json recurrence
json performanceSummary
json generationDefaults
}
CompanyBrandProfile ||--o{ CompanyCampaign : "provides defaults"
CompanyCampaign ||--o{ CompanyPost : "contains"
CompanyCampaign ||--o{ CompanyBlogPost : "contains"
CompanyCampaign ||--o{ CompanyVideoScript : "contains"IMPORTANT
isArchived is a separate boolean field, not a status. This allows campaigns to retain their actual status (COMPLETED, ACTIVE, etc.) while being hidden from default views.
Lifecycle Fields
| Field | Type | Description |
|---|---|---|
pausedAt | DateTime | When campaign was last paused |
resumedAt | DateTime | When campaign was last resumed |
completedAt | DateTime | When campaign was completed |
lastStatusChangedAt | DateTime | Most recent status change |
isArchived | Boolean | Soft-archive flag |
Context Field Structure
The context JSON field stores campaign-specific overrides:
interface CampaignContext {
keyMessages?: string[]; // Always campaign-specific
targetAudience?: string; // Override for Brand Profile
tone?: string; // Override for Brand Voice
channels?: string[]; // Override for connected accounts
[key: string]: unknown; // Extensible
}Defaults + Overrides Pattern
When context fields are absent or null, the frontend displays Brand Profile defaults with badge indicators. This reduces data duplication and ensures consistency.
| Field | Default Source |
|---|---|
targetAudience | CompanyBrandProfile.targetAudience |
tone | CompanyBrandProfile.voiceTone |
channels | All connected UserSocialAccount platforms |
keyMessages | None (always campaign-specific) |
Frontend Components
CampaignList.tsx
Location: apps/frontend/src/components/campaigns/CampaignList.tsx
Renders the campaign grid with:
- Status/type filtering with debounced search
- Archive filter toggle (
includeArchivedparameter) - Pagination controls
- Auto-Plan integration (calls
/api/generate-content-calendar) - Context menu for delete and bulk operations
CampaignDetail.tsx
Location: apps/frontend/src/components/campaigns/CampaignDetail.tsx
NOTE
This component integrates ResumeStrategyModal for pause→active transitions and handles the lifecycle status changes.
Tabs:
- Details — Brief, Goal, Context & Constraints
- Planning Chat — AI conversation with history
- Content — Unified list from
/api/campaigns/:id/content - Performance — Aggregate metrics
ResumeStrategyModal.tsx
Location: apps/frontend/src/components/campaigns/ResumeStrategyModal.tsx
Modal for selecting resume strategy when transitioning from PAUSED to ACTIVE:
shift_weekday— Preserve weekday patternshift_calendar— Shift by paused durationkeep_original— No auto-publish past postscancel— Cancel remaining posts
DeleteCampaignModal.tsx
Location: apps/frontend/src/components/campaigns/DeleteCampaignModal.tsx
Smart delete modal:
- If published content exists → guides user to mark as Completed
- Otherwise → offers delete all or move content options
ArchiveWarningModal.tsx
Location: apps/frontend/src/components/campaigns/ArchiveWarningModal.tsx
Warning modal before archiving with "Don't show again" option (uses dismissedWarnings user preference).
Backend Routes
Location: apps/backend/src/routes/campaigns.ts
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/campaigns | List with filters (status, type, search, includeArchived) |
| POST | /api/campaigns | Create new campaign |
| GET | /api/campaigns/:id | Get single campaign |
| PUT | /api/campaigns/:id | Update campaign fields |
| DELETE | /api/campaigns/:id | Delete (with action options) |
| GET | /api/campaigns/:id/content | Unified content list |
| GET | /api/campaigns/:id/analytics | Aggregated metrics |
| POST | /api/campaigns/:id/resume | Resume with strategy |
| POST | /api/campaigns/:id/archive | Archive/unarchive |
| POST | /api/campaigns/chat | Planning chat endpoint |
| POST | /api/campaigns/:id/generate-plan | Extract plan from chat |
| POST | /api/campaigns/:id/generate-content | Generate content from plan |
Lifecycle Service
Location: apps/backend/src/services/campaign-lifecycle.service.ts
Handles status transitions with validation:
isTransitionAllowed(from, to)— Checks allowed transitionstransitionStatus()— Updates status with lifecycle trackingresumeCampaign()— Implements resume strategiescanDelete()— Checks for published content
AI Integration
Planning Chat Flow
sequenceDiagram
participant U as User
participant FE as Frontend
participant BE as Backend
participant AI as GenAI Service
U->>FE: Types message in Planning Chat
FE->>BE: POST /api/campaigns/chat
BE->>AI: chatCampaignPlanning()
AI-->>BE: Response text
BE-->>FE: { response, role }
FE->>FE: Append to history
U->>FE: Clicks "Generate Plan"
FE->>BE: POST /api/campaigns/:id/generate-plan
BE->>AI: extractCampaignPlan(history)
AI-->>BE: Structured plan
BE->>BE: Update campaign record
BE-->>FE: Updated campaignTesting
Unit Tests
pnpm --filter @tendsocial/backend test test/campaigns.test.tsIntegration Tests
Campaign integration is covered in:
test/integration/ai-generation.test.tstest/integration/campaign-lifecycle.test.ts
Common Patterns
Tenant Scoping
All campaign queries use getTenantPrisma(companyId):
const tenantPrisma = getTenantPrisma(request.user.companyId);
const campaigns = await tenantPrisma.companyCampaign.findMany({
where: { companyId },
orderBy: { updatedAt: 'desc' }
});Archive Filtering
By default, archived campaigns are excluded:
const where: any = { companyId };
if (!includeArchived) {
where.isArchived = false;
}Cascade Delete
When deleting a campaign with action=delete_all:
- All linked
CompanyPostrecords are deleted - All linked
CompanyBlogPostrecords are deleted - All linked
CompanyVideoScriptrecords are deleted
Implementation in campaignService.deleteWithOptions().