3.7 KiB
3.7 KiB
Domain Model (Target)
Evolution from flat Inbox to campaign-centric model. Implement incrementally; not all entities ship in phase 1.
Entity relationship (target)
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 |
| 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 |
| 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)
- Add
roletoInbox(primary|satellite) - Add
WarmupCampaigntable - Add
campaignIdtoWarmupLog - Keep backward compat: inboxes without campaign use legacy flat mode (deprecated)