Skip to content

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

mermaid
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

FieldTypeDescription
pausedAtDateTimeWhen campaign was last paused
resumedAtDateTimeWhen campaign was last resumed
completedAtDateTimeWhen campaign was completed
lastStatusChangedAtDateTimeMost recent status change
isArchivedBooleanSoft-archive flag

Context Field Structure

The context JSON field stores campaign-specific overrides:

typescript
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.

FieldDefault Source
targetAudienceCompanyBrandProfile.targetAudience
toneCompanyBrandProfile.voiceTone
channelsAll connected UserSocialAccount platforms
keyMessagesNone (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 (includeArchived parameter)
  • 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:

  1. Details — Brief, Goal, Context & Constraints
  2. Planning Chat — AI conversation with history
  3. Content — Unified list from /api/campaigns/:id/content
  4. 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 pattern
  • shift_calendar — Shift by paused duration
  • keep_original — No auto-publish past posts
  • cancel — 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

MethodPathDescription
GET/api/campaignsList with filters (status, type, search, includeArchived)
POST/api/campaignsCreate new campaign
GET/api/campaigns/:idGet single campaign
PUT/api/campaigns/:idUpdate campaign fields
DELETE/api/campaigns/:idDelete (with action options)
GET/api/campaigns/:id/contentUnified content list
GET/api/campaigns/:id/analyticsAggregated metrics
POST/api/campaigns/:id/resumeResume with strategy
POST/api/campaigns/:id/archiveArchive/unarchive
POST/api/campaigns/chatPlanning chat endpoint
POST/api/campaigns/:id/generate-planExtract plan from chat
POST/api/campaigns/:id/generate-contentGenerate content from plan

Lifecycle Service

Location: apps/backend/src/services/campaign-lifecycle.service.ts

Handles status transitions with validation:

  • isTransitionAllowed(from, to) — Checks allowed transitions
  • transitionStatus() — Updates status with lifecycle tracking
  • resumeCampaign() — Implements resume strategies
  • canDelete() — Checks for published content

AI Integration

Planning Chat Flow

mermaid
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 campaign

Testing

Unit Tests

bash
pnpm --filter @tendsocial/backend test test/campaigns.test.ts

Integration Tests

Campaign integration is covered in:

  • test/integration/ai-generation.test.ts
  • test/integration/campaign-lifecycle.test.ts

Common Patterns

Tenant Scoping

All campaign queries use getTenantPrisma(companyId):

typescript
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:

typescript
const where: any = { companyId };
if (!includeArchived) {
    where.isArchived = false;
}

Cascade Delete

When deleting a campaign with action=delete_all:

  • All linked CompanyPost records are deleted
  • All linked CompanyBlogPost records are deleted
  • All linked CompanyVideoScript records are deleted

Implementation in campaignService.deleteWithOptions().

TendSocial Documentation