Last Updated: 2026-03-15
This document defines the core entities, their attributes, relationships, and state machines for the Content Engine — independent of any database implementation.
ID Convention: All entity IDs are
stringtype (generated viacrypto.randomUUID()or similar), stored astextcolumns — consistent with theblog-serviceandmanagerschema patterns. The notationUUIDbelow refers to this string format, not a PostgreSQL UUID column type.
1. Entity Map
┌──────────────────────────────────────────────────────────────────┐
│ Tenant (from core-tenant) │
│ │
│ ┌─────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Campaign │ │ ContentSlot │ │SocialConnection │ │
│ │ │◀───│ │───▶│ │ │
│ └─────────────┘ │ ┌───────────┐ │ └──────────────────┘ │
│ │ │SlotTarget │ │ │
│ ┌─────────────┐ │ └───────────┘ │ ┌──────────────────┐ │
│ │ Label │◀───│ │ │RecurringSchedule │ │
│ └─────────────┘ └─────────────────┘ └──────────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ PublishLog │ │
│ └─────────────┘ │
└──────────────────────────────────────────────────────────────────┘2. Entities
2.1 SocialConnection
Represents a linked social media account for a tenant.
| Attribute | Type | Description |
|---|---|---|
id |
UUID | Primary key |
tenantId |
UUID | FK → Tenant |
platform |
Enum | twitter, instagram, linkedin, facebook |
platformAccountId |
String | The external account ID on the platform |
platformUsername |
String | Display name / handle (e.g., @clientbrand) |
accessToken |
EncryptedString | OAuth2 access token (AES-256-GCM) |
refreshToken |
EncryptedString | OAuth2 refresh token (AES-256-GCM) |
tokenExpiresAt |
DateTime | When the access token expires |
status |
Enum | active, expired, revoked, error |
connectedBy |
UUID | FK → User who connected it |
createdAt |
DateTime | — |
updatedAt |
DateTime | — |
Business Rules:
- A tenant can have multiple connections per platform (multi-brand agencies)
- If
tokenExpiresAt < NOW() + 48h, the proactive refresh job should refresh it - If refresh fails, status →
expiredand tenant is notified
2.2 Campaign
A logical grouping of content for planning purposes.
| Attribute | Type | Description |
|---|---|---|
id |
UUID | Primary key |
tenantId |
UUID | FK → Tenant |
name |
String | e.g., "Q1 Product Launch" |
description |
String | Optional notes |
color |
HexColor | Calendar display color |
startDate |
Date | Campaign start (for filtering) |
endDate |
Date | Campaign end |
status |
Enum | active, completed, archived |
createdAt |
DateTime | — |
updatedAt |
DateTime | — |
createdBy |
UUID | FK → User who created it |
Business Rules:
- Campaigns are optional — content slots can exist without one
- Archiving a campaign does not cancel its scheduled slots
- A campaign's
endDatemust be >=startDate
2.3 Label
Lightweight tags for categorizing content.
| Attribute | Type | Description |
|---|---|---|
id |
UUID | Primary key |
tenantId |
UUID | FK → Tenant |
name |
String | e.g., "ProductUpdate", "Motivational" |
color |
HexColor | Badge color |
createdAt |
DateTime | — |
Relationship: Many-to-Many with ContentSlot via content_slot_labels junction table.
2.4 ContentSlot
The core entity — a single piece of content to be published.
| Attribute | Type | Description |
|---|---|---|
id |
UUID | Primary key |
tenantId |
UUID | FK → Tenant |
campaignId |
UUID? | FK → Campaign (nullable) |
contentType |
Enum | text, image, video, carousel, story, link |
caption |
Text | The post text / copy |
mediaIds |
UUID[] | References to Media Service assets |
linkUrl |
String? | External URL to include (for link-type posts) |
scheduledAt |
DateTime (UTC) | When to publish |
timezone |
String | IANA timezone (e.g., Asia/Kolkata) for display |
status |
Enum | See state machine below |
approvalNote |
Text? | Reviewer's comment (on reject/approve) |
recurringScheduleId |
UUID? | FK → RecurringSchedule (if auto-generated) |
source |
Enum | manual, recurring, event_bus, mcp_agent |
sourceAgentId |
String? | If created by MCP agent, the agentId |
createdBy |
UUID | FK → User (or system user for auto-generated) |
updatedAt |
DateTime | — |
createdAt |
DateTime | — |
State Machine (see adr_architecture.md ADR-4 for full diagram):
draft → pending_review → approved → scheduled → publishing → published
↓ (reject) ↓ (fail)
draft failed → draft2.5 SlotTarget
A junction entity: one ContentSlot can publish to multiple platforms.
| Attribute | Type | Description |
|---|---|---|
id |
UUID | Primary key |
contentSlotId |
UUID | FK → ContentSlot |
socialConnectionId |
UUID | FK → SocialConnection |
platformCaption |
Text? | Per-platform override caption |
publishStatus |
Enum | pending, publishing, published, failed |
externalPostId |
String? | The ID returned by the platform after publishing |
externalPostUrl |
String? | Link to the published post |
errorMessage |
String? | Error details if failed |
retryCount |
Int | Number of publish attempts |
publishedAt |
DateTime? | When it was actually published |
Business Rules:
- A ContentSlot's overall
status=publishedonly when all SlotTargets arepublished - If any SlotTarget fails after all retries, the ContentSlot's status =
partially_failed(if some targets succeeded) orfailed(if all failed) - Maximum 3 retries per SlotTarget
- A SlotTarget's
platformCaptionoverrides the parent ContentSlot'scaptionfor that platform
2.6 RecurringSchedule
Defines a recurring content pattern.
| Attribute | Type | Description |
|---|---|---|
id |
UUID | Primary key |
tenantId |
UUID | FK → Tenant |
name |
String | e.g., "Motivation Monday" |
cronExpression |
String | Cron pattern: 0 10 * * 1 (Mon 10 AM) |
timezone |
String | IANA timezone for cron evaluation |
templateCaption |
Text | Template with {variables} |
templateMediaIds |
UUID[]? | Default media attachments |
targetConnectionIds |
UUID[] | Which social accounts to publish to |
autoSchedule |
Boolean | Skip approval? |
isActive |
Boolean | Can be paused |
nextGenerateAt |
DateTime | Next slot generation time |
createdAt |
DateTime | — |
updatedAt |
DateTime | — |
Business Rules:
- The generation job creates slots 7 days in advance
- Generated slots are editable — the template just provides defaults
- Deactivating a schedule does not cancel already-generated slots
2.7 PublishLog
Immutable audit trail of every publish attempt.
| Attribute | Type | Description |
|---|---|---|
id |
UUID | Primary key |
tenantId |
UUID | FK → Tenant |
slotTargetId |
UUID | FK → SlotTarget |
action |
Enum | publish_attempt, publish_success, publish_failed, retry |
platformResponse |
JSON? | Raw response from platform API |
errorCode |
String? | Normalized error code |
createdAt |
DateTime | — |
3. Aggregate Boundaries
| Aggregate Root | Contains |
|---|---|
| ContentSlot | SlotTargets (create/update/delete together) |
| SocialConnection | Standalone (referenced by SlotTarget) |
| Campaign | Standalone (referenced by ContentSlot) |
| RecurringSchedule | Standalone (generates ContentSlots) |
4. Key Invariants
- A ContentSlot cannot be scheduled without at least 1 SlotTarget
- A SlotTarget must reference an
activeSocialConnection - A ContentSlot's
scheduledAtmust be in the future at time of scheduling - A ContentSlot in
publishingstatus cannot be edited or deleted - A
publishedContentSlot cannot be re-published (immutable terminal state) - Deleting a SocialConnection does not cascade to ContentSlots — slots targeting a deleted connection are marked as
failedwith errorconnection_removed