# Domain Model (Target) Evolution from flat `Inbox` to campaign-centric model. Implement incrementally; not all entities ship in phase 1. ## Entity relationship (target) ```mermaid erDiagram Workspace ||--o{ Mailbox : owns Workspace ||--o{ WarmupCampaign : has Mailbox ||--o{ WarmupCampaign : "primary" WarmupCampaign ||--o{ CampaignSatellite : assigns Mailbox ||--o{ CampaignSatellite : "satellite" WarmupCampaign ||--o{ DailySchedule : plans WarmupCampaign ||--o{ DailyStat : tracks WarmupCampaign ||--o{ WarmupLog : generates Mailbox ||--o{ WarmupLog : sends Mailbox ||--o{ WarmupLog : receives ``` ## Mailbox (evolved Inbox) | Field | Type | Notes | |-------|------|-------| | id | cuid | | | workspaceId | cuid | Future multi-tenant; nullable for self-hosted | | email | string | Unique per workspace | | role | enum | `primary` \| `satellite` \| `unassigned` | | smtpHost, smtpPort, imapHost, imapPort | | Connection | | username, password | | Encrypted | | provider | enum | `gmail` \| `yahoo` \| `outlook` \| `custom` | | status | enum | `active` \| `paused` \| `error` \| `warming` \| `completed` | | industry | string | AI context | | dnsStatus | json | SPF/DKIM/DMARC check results (future) | | lastError, errorAt | | | **Rule:** A mailbox can be primary in one campaign and satellite in another only if explicitly allowed (default: one role per campaign assignment). ## WarmupCampaign One campaign = one primary mailbox warming with one recipe. | Field | Type | Notes | |-------|------|-------| | id | cuid | | | workspaceId | cuid | | | primaryMailboxId | cuid | FK Mailbox | | name | string | e.g. "FillCare hello@" | | recipe | enum | `grow` \| `flat` \| `randomized` \| `custom` | | status | enum | `draft` \| `active` \| `paused` \| `completed` \| `failed` | | durationDays | int | Default 30, max 45 | | startedAt | datetime | | | endsAt | datetime | Computed | | minEmailsPerDay | int | Default 1 | | maxEmailsPerDay | int | Default 40, max 50 | | replyRatePercent | int | Default 30, max 45 | | customSchedule | json | For `custom` recipe: `[{ day, sends }]` | | timezone | string | Send window TZ | | sendWindowStart | time | e.g. 08:00 | | sendWindowEnd | time | e.g. 18:00 | | currentDay | int | Day index since start | | rampState | json | Recipe-specific state | ## CampaignSatellite (join) | Field | Type | |-------|------| | campaignId | cuid | | satelliteMailboxId | cuid | | weight | int | Optional load balancing (default 1) | | active | boolean | ## DailySchedule Pre-computed plan for a campaign day (generated at campaign start or nightly). | Field | Type | |-------|------| | campaignId | cuid | | date | date | | dayIndex | int | | plannedSends | int | | plannedReplies | int | | actualSends | int | | actualReplies | int | | spamDetected | int | | spamRescued | int | ## WarmupLog (evolved) Add `campaignId`, `placement` (`inbox` \| `spam` \| `unknown`), `scheduledAt`, `sentAt`, `repliedAt`. ## DailyStat (materialized) Rollup for fast dashboard queries. | Field | Type | |-------|------| | campaignId | cuid | | date | date | | sent, received, replied, spamDetected, spamRescued | int | | replyRateActual | float | | inboxPlacementRate | float | ## Setting (global / workspace) Move cron and AI config to workspace scope over time. ## Migration from current schema | Current | Target | |---------|--------| | `Inbox` | `Mailbox` + `role` | | Per-inbox `dailyLimit`, `currentRamp` | `WarmupCampaign` fields | | Flat dispatcher | Campaign-aware dispatcher | | `WarmupLog` | Add `campaignId` | ### Phase 1 migration (minimal) 1. Add `role` to `Inbox` (`primary` \| `satellite`) 2. Add `WarmupCampaign` table 3. Add `campaignId` to `WarmupLog` 4. Keep backward compat: inboxes without campaign use legacy flat mode (deprecated)