Add initial documentation for Warmbox, including product brief, current state analysis, glossary, domain model, warmup recipes, primary-satellite topology, scheduling, reply rate engine, spam rescue, AI content policy, statistics, ESP compliance, system architecture, API evolution, and frontend UX specification.

This commit is contained in:
Jonny Nguyen 2026-06-24 22:40:25 -07:00
parent 3b540371cf
commit 4eedbbfb1e
21 changed files with 2253 additions and 0 deletions

84
docs/00-product-brief.md Normal file
View file

@ -0,0 +1,84 @@
# Product Brief — Warmbox
## One-liner
Warmbox is a self-hosted (today) / SaaS (tomorrow) cold email warmup engine that safely builds sender reputation for **primary outbound mailboxes** using a controlled network of **satellite inboxes**, realistic AI-generated conversations, and human-like engagement signals — without triggering ESP anti-abuse systems.
## Problem
New or cold mailboxes used for B2B outreach land in spam. ESPs (Google, Microsoft, Yahoo) evaluate:
- Sending volume patterns (ramps, spikes, consistency)
- Authentication (SPF, DKIM, DMARC)
- Engagement (replies, not-spam actions, spam complaints)
- Content signals (spam triggers, templated/robotic text)
Commercial warmup tools exist but carry risks: toxic peer pools, detectable patterns, placeholder-filled AI content, and ESP policies against artificial warmup networks.
## Solution
Warmbox gives operators **full control** over:
- Which mailboxes are warming (primaries) vs supporting (satellites)
- Warmup **recipe** (progressive ramp, flat, randomized, custom)
- Volume bounds (150/day), duration (3045 days), reply rate (3045%)
- Spam rescue and placement visibility per primary
- AI content quality guardrails (no placeholders, no spam triggers)
## Target users (personas)
### P1 — Cold email operator (FillCare / agency owner)
- Warms `hello@fillcareapp.com` and future sales domains
- Owns 410 personal Gmail/Yahoo/Live satellites
- Needs confidence before connecting Instantly/Smartlead
- Wants dashboard: sent, replied, spam rate, ramp progress
### P2 — Deliverability-focused growth lead
- Manages multiple primaries across brands
- Needs recipes, custom schedules, alerts on spam spikes
- Cares about Google Postmaster / Microsoft SNDS integration (future)
### P3 — SaaS customer (future)
- Signs up, connects mailboxes via OAuth or app passwords
- Pays per primary mailbox warmed
- Never sees other customers' satellites (tenant isolation)
## Goals
| Goal | Measure |
|------|---------|
| Inbox placement | ≥95% warmup emails in inbox (not spam) after week 3 |
| Spam rescue | 100% of detected warmup-in-spam rescued within rescuer cycle |
| Reply realism | Reply rate within configured ±5% of target |
| Content quality | 0% emails with bracket placeholders; <1% AI validation failures |
| ESP safety | No account suspensions attributable to Warmbox patterns |
| Operator trust | Per-primary daily stats accurate within 1 message |
## Non-goals (MVP SaaS phase)
- Sending live cold campaigns (delegate to Instantly, Smartlead, Apollo)
- Shared public peer network at launch (optional later)
- Mobile native apps
- White-label reseller portal (future)
## Competitive positioning
| Capability | Warmbox | Typical warmup SaaS |
|------------|---------|---------------------|
| Self-hosted option | Yes | Rare |
| Own satellite pool | Yes | Often forced shared pool |
| Recipe control | Full (4 types) | Often progressive only |
| Open source core | Possible | No |
| ESP-aware behavior | Designed-in | Varies |
## Success criteria for "production ready"
1. Multiple primaries warming concurrently with independent recipes
2. Satellites only engage assigned primaries (no random cross-primary noise)
3. Statistics dashboard matches raw logs
4. Auto-pause on auth errors, bounce spikes, or spam-rate thresholds
5. Web UI for all operator workflows
6. Documented ESP compliance checklist passed before go-live

View file

@ -0,0 +1,79 @@
# Current State & Gap Analysis
## What exists today (MVP)
### Data layer
| Model | Purpose |
|-------|---------|
| `Inbox` | Flat mailbox record (no role) |
| `WarmupLog` | Message lifecycle: sent → rescued_from_spam → replied → complete |
| `Setting` | Global key-value config |
### Jobs
| Job | Behavior |
|-----|----------|
| Dispatcher | Random sender/receiver from all active inboxes; AI email; SMTP send |
| Rescuer | Spam folder scan; move to INBOX; flag; AI reply (~100% reply when found) |
| Ramp bump | Midnight `currentRamp += 2` up to `dailyLimit` |
### API
- Inbox CRUD + connection test
- Paginated logs
- Global settings (cron, industries)
### Strengths
- Working SMTP/IMAP connector with encrypted credentials
- Spam rescue on Gmail/Yahoo/Outlook folder names
- AI via OpenRouter (free models)
- Error inbox marking (`status: error`)
- Prisma 7 + SQLite portability
## Gap matrix
| Feature | Current | Target | Priority |
|---------|---------|--------|----------|
| Primary vs satellite roles | All equal | Role-based pairing | P0 |
| Multiple primaries | Implicit | Explicit campaigns per primary | P0 |
| Grow recipe | Partial ramp | Named recipe engine | P0 |
| Flat recipe | Missing | Constant daily volume | P0 |
| Randomized recipe | Missing | Daily variance algorithm | P1 |
| Custom recipe | Missing | Per-day schedule editor | P1 |
| Warmup duration 3045d | Missing | Campaign start/end | P0 |
| Min emails/day (1) | Missing | Enforced floor | P0 |
| Max emails/day (4050) | Partial | Validated cap | P0 |
| Reply rate 30% | ~100% | Probabilistic sampling | P0 |
| Per-primary statistics | Missing | Dashboard API | P0 |
| Spam count per primary | Partial (per log) | Aggregated metrics | P0 |
| AI placeholder blocking | Prompt only | Post-generation validation | P0 |
| Send time jitter | Missing | Human-like windows | P1 |
| Web UI | Missing | Full operator console | P1 |
| Multi-tenant | Missing | Org/workspace model | P2 |
| Peer network | Missing | Optional phase 3 | P3 |
| DNS/auth checks | Missing | SPF/DKIM/DMARC validation | P1 |
| Auto-pause on bad signals | Partial (auth only) | Spam/bounce thresholds | P1 |
| Tests | Missing | Unit + integration | P1 |
## Architectural debt
1. **Flat inbox pool** — Dispatcher treats every inbox as equal; primaries can email each other.
2. **Global ramp** — Not tied to campaign start date or recipe.
3. **SQLite** — Fine for self-hosted; need Postgres path for SaaS.
4. **No schedule entity** — "Planned sends today" is implicit, not queryable.
5. **Spam rescue granularity** — Marks all pending logs from sender, not per UID.
6. **No idempotency** — Cron overlap could double-send without locks.
## Recommended migration path
```
Phase A: Schema + roles + recipes (backend only)
Phase B: Statistics API + reply rate engine
Phase C: Web UI (self-hosted)
Phase D: Hardening (jitter, validation, auto-pause)
Phase E: Multi-tenant + optional peer network
```
See [16-roadmap.md](./16-roadmap.md) for sprint breakdown.

20
docs/02-glossary.md Normal file
View file

@ -0,0 +1,20 @@
# Glossary
| Term | Definition |
|------|------------|
| **Primary mailbox** | The cold outbound address being warmed (e.g. `hello@fillcareapp.com`). Receives engagement signals; volume ramps per recipe. |
| **Satellite mailbox** | Supporting inbox (Gmail, Yahoo, Live) that sends to and replies from primaries. Not the main outreach address. |
| **Warmup campaign** | Configuration binding one primary to a recipe, duration, volume bounds, reply rate, and assigned satellites. |
| **Recipe** | Algorithm that determines how many emails to send per day over the campaign timeline. |
| **Ramp** | Progressive increase in daily send volume (Grow recipe). |
| **Reply rate** | Percentage of received warmup emails that get an AI-generated reply (not 100%). |
| **Spam rescue** | IMAP action: find warmup email in Spam/Junk, move to INBOX, mark read/flagged/important. |
| **Placement** | Where a message landed: inbox, spam, or unknown. |
| **ESP** | Email Service Provider (Google, Microsoft, Yahoo). |
| **Engagement signal** | Reply, not-spam move, flag, open (future) — positive reputation input. |
| **Peer network** | Shared pool of third-party mailboxes (optional future feature). |
| **Send window** | Time-of-day range when sends are allowed (e.g. 08:0018:00 local). |
| **Jitter** | Random delay between sends to avoid machine-like cadence. |
| **Warmup log** | Single message transaction with lifecycle status. |
| **Daily stat** | Aggregated per-primary metrics for a calendar day. |
| **Tenant** | Future SaaS customer organization with isolated data. |

123
docs/03-domain-model.md Normal file
View file

@ -0,0 +1,123 @@
# 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)

126
docs/04-warmup-recipes.md Normal file
View file

@ -0,0 +1,126 @@
# Warmup Recipes
Four recipes control **how many emails the primary receives/sends per day** over the campaign duration.
## Global constraints (all recipes)
| Parameter | Default | Min | Max |
|-----------|---------|-----|-----|
| Duration | 30 days | 14 | 45 |
| Min emails/day | 1 | 1 | — |
| Max emails/day | 40 | — | 50 |
| Reply rate | 30% | 10% | 45% |
| Volume change cap | +20% / 48h | — | Industry best practice |
## 1. Grow — Progressive (recommended)
**Use when:** New domain or cold mailbox. Safest default.
### Behavior
- Start at `minEmailsPerDay` (default 1) on day 1
- Increase daily toward `maxEmailsPerDay` over `durationDays`
- Never increase more than 20% vs prior 48h average
- After reaching max, hold flat until campaign end
- Optionally continue background warmup post-campaign at 20% of max
### Algorithm (pseudocode)
```
totalGrowthDays = durationDays * 0.7 // reach max by ~70% of campaign
dailyIncrement = (maxEmails - minEmails) / totalGrowthDays
for day in 1..durationDays:
target = minEmails + floor(dailyIncrement * min(day, totalGrowthDays))
target = min(target, maxEmails)
target = cap48hIncrease(previousTargets, target, 0.20)
schedule[day] = target
```
### Example (30 days, min=1, max=40)
| Day | Planned sends |
|-----|---------------|
| 1 | 1 |
| 7 | ~8 |
| 14 | ~18 |
| 21 | ~30 |
| 24+ | 40 |
## 2. Flat
**Use when:** Mailbox already warm; maintain reputation during live outreach.
### Behavior
- Constant `maxEmailsPerDay` every day
- No ramp
- Same reply rate and rescue rules
```
schedule[day] = maxEmailsPerDay // for all days
```
## 3. Randomized
**Use when:** Experimentation; avoid perfectly predictable patterns.
### Behavior
- Daily volume random within `[minEmailsPerDay, maxEmailsPerDay]`
- Weighted toward middle of range (normal distribution)
- 48h spike cap still applies
- Seed per campaign for reproducibility (debugging)
```
schedule[day] = randomInt(min, max, seed=campaignId+day)
schedule[day] = cap48hIncrease(...)
```
## 4. Custom
**Use when:** Expert operators want per-day control.
### Behavior
- User defines array: `[{ day: 1, sends: 2 }, { day: 2, sends: 5 }, ...]`
- Validate: each value within [min, max], length ≤ durationDays
- Missing days: hold last defined value or interpolate (configurable)
- UI: spreadsheet-style editor + import CSV
## Recipe selection API (future)
```json
{
"recipe": "grow",
"durationDays": 30,
"minEmailsPerDay": 1,
"maxEmailsPerDay": 40,
"replyRatePercent": 30
}
```
## Dispatcher integration
1. Load active campaigns
2. For each campaign, read `DailySchedule` for today
3. If `actualSends < plannedSends`, schedule next send slot with jitter
4. Pick satellite from `CampaignSatellite` (weighted random)
5. Primary is always receiver for satellite→primary sends; primary may send replies back
## Post-campaign
| Mode | Behavior |
|------|----------|
| Complete | Stop sends; mark campaign `completed` |
| Maintenance | Switch to Flat at 20% of max (recommended) |
| Pause | Operator pause; resume later |
## UI copy (recommended labels)
| Recipe | Label | Subtitle |
|--------|-------|----------|
| grow | Grow | Progressive ramp — recommended for new mailboxes |
| flat | Flat | Steady volume — for already-warm inboxes |
| randomized | Randomized | Variable daily volume — for testing |
| custom | Custom | Define your own day-by-day schedule |

View file

@ -0,0 +1,120 @@
# Primary & Satellite Topology
## Core principle
**Primaries** are the mailboxes whose reputation matters for cold outreach.
**Satellites** exist only to generate realistic, controlled engagement with primaries.
```
┌─────────────────┐
│ Primary │
│ hello@fillcare │
└────────┬────────┘
┌───────────────────┼───────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Satellite│ │Satellite│ │Satellite│
│ Gmail 1 │ │ Yahoo │ │ Live │
└─────────┘ └─────────┘ └─────────┘
```
## Traffic rules
| Direction | Allowed | Notes |
|-----------|---------|-------|
| Satellite → Primary | Yes | Main warmup send |
| Primary → Satellite | Yes | Reply to satellite (completes thread) |
| Satellite → Satellite | No | Wastes quota; looks unnatural |
| Primary → Primary | No | Never warm primaries against each other |
| Satellite → unrelated external | No | Only campaign-assigned primaries |
## Multi-primary support
One operator can run **N campaigns** concurrently:
```
Campaign A: primary=hello@fillcareapp.com ← satellites [gmail1, yahoo1, live1]
Campaign B: primary=sales@otherdomain.com ← satellites [gmail2, yahoo2]
```
Satellites may be **shared** across campaigns only if explicitly enabled (default: dedicated per primary to avoid cross-brand leakage).
## FillCare example setup
| Mailbox | Role | Provider |
|---------|------|----------|
| hello@fillcareapp.com | Primary | Google Workspace |
| personal@gmail.com | Satellite | Gmail |
| personal@yahoo.com | Satellite | Yahoo |
| personal@live.com | Satellite | Outlook |
| (+ 12 more) | Satellite | Mixed |
### Minimum viable pool
- 1 primary + **2 satellites** (technical minimum)
- Recommended: 1 primary + **46 satellites** for natural variety and reply distribution
## Satellite lifecycle
```mermaid
stateDiagram-v2
[*] --> Unassigned: Add mailbox
Unassigned --> Active: Assign to campaign
Active --> Paused: Operator pause
Paused --> Active: Resume
Active --> Error: Auth/IMAP failure
Error --> Active: Fix + test
Active --> Unassigned: Remove from campaign
```
## Add / remove operations
### Add satellite
1. `POST /mailboxes` with `role: satellite`
2. Test SMTP/IMAP
3. `POST /campaigns/:id/satellites` to assign
### Remove satellite
1. `DELETE /campaigns/:id/satellites/:mailboxId` — stops new sends
2. In-flight threads complete or timeout
3. Optional: `DELETE /mailboxes/:id` if fully removed
### Add primary
1. `POST /mailboxes` with `role: primary`
2. `POST /campaigns` with recipe + bounds
3. Assign satellites
4. Start campaign (`status: active`)
## Peer network (phase 3 — optional)
When enabled, additional engagement from vetted peer mailboxes:
| Aspect | Own satellites | Peer network |
|--------|----------------|--------------|
| Control | Full | Limited |
| ESP risk | Lower | Higher (pool quality) |
| Cost | User supplies accounts | Platform fee |
| Detection | User responsible | Platform must maintain pool health |
**Recommendation:** Ship own-satellite-only first. Peer network requires bounce monitoring, DNS validation, and pool rotation — significant ops burden.
## Dispatcher pairing (target logic)
```
for each campaign where status=active:
if today.sends < schedule.plannedSends:
satellite = pickWeighted(campaign.satellites)
primary = campaign.primaryMailbox
queueSend(from=satellite, to=primary)
```
Reply path (rescuer):
```
if shouldReply(campaign.replyRatePercent):
reply(from=primary, to=satellite, inReplyTo=original)
```

View file

@ -0,0 +1,91 @@
# Scheduling & Timing
## Campaign duration
| Setting | Value |
|---------|-------|
| Recommended | 3045 days |
| Minimum | 14 days (not recommended for new domains) |
| Maximum | 45 days (MVP cap) |
`endsAt = startedAt + durationDays`
## Daily send distribution
### Send windows
Avoid off-hours sends that signal automation.
| Parameter | Default |
|-----------|---------|
| Timezone | Campaign or mailbox TZ |
| Window start | 08:00 |
| Window end | 18:00 |
| Weekends | Optional: reduce volume 50% or pause |
### Jitter between sends
| Parameter | Default | Rationale |
|-----------|---------|-----------|
| Min gap | 45 minutes | Avoid burst patterns |
| Max gap | 180 minutes | Stay within daily quota |
| Distribution | Uniform random | Human-like variance |
```
nextSendAt = lastSendAt + random(45min, 180min)
if nextSendAt outside window: roll to next window start
```
## 48-hour volume cap
Industry guidance: never increase average daily volume more than **20% in 48 hours**.
```typescript
function cap48hIncrease(history: number[], proposed: number): number {
const avg48h = average(last48h(history));
return Math.min(proposed, Math.ceil(avg48h * 1.20));
}
```
Applies to all recipes except explicit expert override in Custom (with warning).
## Cron vs queue architecture
### Current (MVP)
- `node-cron` fires dispatcher every 15 min
- All eligible sends in one batch
### Target
| Component | Role |
|-----------|------|
| Scheduler cron | Every 5 min: enqueue due sends |
| Send queue | BullMQ / pg-boss / SQLite queue |
| Worker | One send per job with jitter |
| Rescuer cron | Every 10 min: IMAP scan per mailbox |
Benefits: precise timing, backoff, idempotency keys, pause per campaign.
## Campaign day rollover
At midnight (campaign TZ):
1. Increment `currentDay`
2. Load `DailySchedule` for new day
3. Reset `actualSends`, `actualReplies` counters
4. If `currentDay > durationDays` → complete or maintenance mode
## Maintenance mode (post-campaign)
After primary campaign completes:
- Continue Flat recipe at **20% of maxEmailsPerDay**
- Keeps engagement signals during live cold outreach
- Operator can disable
## Timezone handling
- Store all timestamps UTC in DB
- Display in campaign timezone in UI
- Send windows evaluated in campaign timezone

View file

@ -0,0 +1,94 @@
# Reply Rate Engine
## Problem
Current MVP replies to **every** matched warmup email (~100%). Real humans and reputable warmup tools reply to a **fraction** of mail (typically 30%, max recommended 45%).
## Configuration
| Parameter | Default | Min | Max recommended |
|-----------|---------|-----|-----------------|
| `replyRatePercent` | 30 | 10 | 45 |
Set per `WarmupCampaign`. Hard cap at 45% in validation.
## Semantics
**Reply rate** = of warmup emails received by the primary (or sent to primary from satellites), the percentage that receive an AI-generated reply that day.
Not the same as cold-email response rate — this is warmup engagement simulation only.
## Algorithm
### On rescuer cycle (per received warmup message)
```
function shouldReply(campaign, message, statsToday):
target = campaign.replyRatePercent
actual = statsToday.replies / max(statsToday.received, 1) * 100
if actual >= target:
return false // quota met for today
// Probabilistic: bias toward hitting target by end of day
remaining = statsToday.received - statsToday.replies
needed = ceil(statsToday.received * target/100) - statsToday.replies
probability = needed / max(remaining, 1)
return random() < probability
```
### Properties
- Deterministic enough to converge on daily target
- Stochastic enough to avoid fixed patterns
- Never exceeds 45% even if configured wrong (clamp)
## What still happens at 0% reply (for a given message)
Even when not replying:
1. Spam rescue still runs (move to inbox, flag)
2. Mark as read
3. Log status may be `complete` without `replied` — new status: `engaged_no_reply`
### Extended WarmupStatus enum
| Status | Meaning |
|--------|---------|
| sent | Delivered from satellite |
| delivered | Confirmed in primary inbox (optional) |
| rescued_from_spam | Moved from spam folder |
| engaged_no_reply | Read/flagged, no reply sent |
| replied | Reply sent |
| complete | Thread closed |
## Reply direction
| Step | From | To |
|------|------|-----|
| 1. Warmup send | Satellite | Primary |
| 2. Reply (if sampled) | Primary | Satellite |
Maintains realistic thread: primary appears engaged with incoming mail.
## Daily reply budget
```
plannedReplies = ceil(plannedSends * replyRatePercent / 100)
```
Shown on dashboard: `replies today: 8/12 planned`
## Edge cases
| Case | Handling |
|------|----------|
| All messages arrive at 23:59 | Spread replies across rescuer cycles |
| Reply SMTP fails | Retry once; log error; don't count toward rate |
| Primary in error | Pause replies; satellites may still send |
| Satellite in error | Exclude from pool; rebalance |
## ESP rationale
100% reply rate on automated mail is a detection signal. Variable reply rate + rescue without reply on some messages mimics real mailbox behavior more closely.

View file

@ -0,0 +1,129 @@
# Spam Rescue & Placement Tracking
## Objectives
1. Detect when warmup emails land in spam/junk
2. Rescue them (move to inbox, positive signals)
3. Track spam rate per primary for dashboard and auto-pause
4. Never leave warmup mail unhandled in spam folders
## Folder discovery
### Default folder list (try in order)
| Provider | Folders |
|----------|---------|
| Gmail | `[Gmail]/Spam` |
| Outlook | `Junk` |
| Yahoo | `Bulk` or `Spam` |
| Generic | `Spam`, `Junk` |
### Target enhancement
1. `LIST` IMAP folders on connect; match known spam patterns
2. Store per-mailbox `spamFolderPath` after first discovery
3. Provider enum shortcuts connection presets
## Rescue workflow
```mermaid
sequenceDiagram
participant R as Rescuer
participant IMAP as Primary IMAP
participant DB as Database
R->>IMAP: Open spam folder
R->>IMAP: SEARCH unseen FROM satellite
IMAP-->>R: UIDs
loop Each UID
R->>IMAP: MOVE to INBOX
R->>DB: Log placement=spam, status=rescued
R->>IMAP: FLAGS Seen, Flagged
R->>IMAP: FLAGS Important (best effort)
R->>R: shouldReply?
alt Reply sampled
R->>SMTP: Send reply
else No reply
R->>DB: status=engaged_no_reply
end
end
```
## Per-message matching (fix MVP weakness)
Current MVP marks all pending logs from sender when any spam found. Target:
| Priority | Match key |
|----------|-----------|
| 1 | `Message-ID` header exact match |
| 2 | `In-Reply-To` chain |
| 3 | From + normalized subject + 24h window |
One UID → one WarmupLog row.
## Placement tracking
Add to `WarmupLog` or separate `PlacementEvent`:
| Field | Values |
|-------|--------|
| placement | `inbox`, `spam`, `unknown` |
| detectedAt | timestamp |
| rescuedAt | timestamp nullable |
| folder | string |
### Inbox placement rate
```
inboxRate = 1 - (spamDetected / totalReceived)
```
Target: ≥95% inbox after week 3.
## INBOX processing (non-spam path)
Scan primary INBOX for unread warmup from assigned satellites:
- Same matching rules
- If already in inbox: flag + optional reply (no move)
- Log `placement=inbox`
## Auto-pause triggers
| Condition | Action |
|-----------|--------|
| Spam rate >30% for 3 consecutive days | Pause campaign; alert operator |
| Spam rate >50% in 24h | Immediate pause |
| Rescue failures >5 in row | Mark primary `error` |
| Auth failure | Mark `error`; stop all jobs |
## Provider-specific notes
### Gmail / Google Workspace
- `\Important` via labels (`useLabels: true`)
- Postmaster Tools integration (future): pull domain reputation
- App passwords required; OAuth preferred for SaaS
### Microsoft 365 / Outlook
- Junk Email folder naming varies
- Enable IMAP in admin settings
- SNDS integration (future)
### Yahoo
- App password required
- Bulk folder common
## Compliance considerations
Rescue actions simulate **user marking not spam** — legitimate when acting on mail the user owns. Warmbox must only access mailboxes the operator controls.
**Do not** rescue mail the operator does not own (peer network phase requires explicit consent).
## Future: seed placement tests
Optional inbox placement probes (like GlockApps) — out of MVP scope but document hook:
`POST /campaigns/:id/placement-test` → send test to probe addresses → report inbox/spam

View file

@ -0,0 +1,111 @@
# AI Content Policy
## Requirements
All warmup email subjects and bodies must be:
- Professional B2B tone
- Contextually appropriate to `industry` (Childcare, SaaS, Finance, etc.)
- Free of spam trigger words (see blocklist)
- **Free of placeholders** — no `[Name]`, `[Your company]`, `{{variable}}`, etc.
- Plain text only (MVP) — no HTML, images, tracking pixels, or links in warmup mail
- Unique enough across sends — no identical consecutive bodies
## Forbidden patterns (auto-reject)
Regex / validation pass after generation:
```
\[.*?\] # bracket placeholders
\{\{.*?\}\} # mustache
<.*?@.*?> # angle email placeholders in body
\[Your .*?\]
\[Director .*?\]
\[Company .*?\]
INSERT .* HERE
TODO
lorem ipsum
click here
act now
limited time
free money
guarantee
```
## Spam trigger word tiers
### Tier 1 — hard block (regenerate)
`free`, `winner`, `cash`, `viagra`, `casino`, `lottery`, `act now`, `100% free`, `risk-free`
### Tier 2 — soft warning (regenerate once, then flag for review)
`urgent`, `limited`, `offer`, `discount`, `sale`, `promotion`, `buy now`
## Generation pipeline
```mermaid
flowchart LR
A[AI Generate] --> B[JSON Parse]
B --> C[Placeholder scan]
C --> D[Spam word scan]
D --> E[Length check]
E --> F{Pass?}
F -->|No| G[Regenerate max 3]
F -->|Yes| H[Store + Send]
G --> A
```
## Prompt guidelines (system)
### Initial email
- 80150 words
- Specific but generic business topic (scheduling, follow-up, intro)
- Realistic subject ≤60 chars
- No signatures with fake phone numbers
- Sign off with first name only if name provided in mailbox profile (future)
### Reply
- 40100 words
- Reference specific points from incoming body (quote or paraphrase)
- Natural continuation, not sales pitch
- `Re:` subject handling
## Content diversity
| Technique | Purpose |
|-----------|---------|
| Temperature 0.80.9 | Variation |
| Industry rotation | Context diversity |
| Thread memory (last 3 bodies) | Avoid repetition in replies |
| Optional spintax-free rewriting | Structural variation (phase 2) |
## Mailbox profile (future)
```json
{
"senderName": "Jon",
"companyName": "FillCare",
"title": "Co-founder"
}
```
AI uses real values instead of inventing or placeholdering.
## Failure handling
| Failure | Action |
|---------|--------|
| 3 validation failures | Skip send slot; log `ai_validation_failed` |
| API down | Retry with backoff; don't send template fallback |
| JSON parse error | Retry once with stricter prompt |
## Audit
Store `contentVersion` and `validationResult` on `WarmupLog` for debugging disputes.
## Why no links in warmup
Tracking links and URLs are strong spam signals during reputation building. Warmup content should be link-free until placement stabilizes (phase 2: optional benign links).

View file

@ -0,0 +1,115 @@
# Statistics & Reporting
## Operator questions the dashboard must answer
1. How many emails are **scheduled** today for each primary?
2. How many were **sent** today?
3. How many were **replied** today?
4. How many landed in **spam** vs inbox?
5. Is warmup **on track** for the recipe?
6. Are any mailboxes in **error**?
## Per-primary campaign dashboard
### Summary cards
| Card | Source |
|------|--------|
| Campaign day | `currentDay / durationDays` |
| Today's sends | `actualSends / plannedSends` |
| Today's replies | `actualReplies / plannedReplies` |
| Inbox rate (7d) | `DailyStat` rollup |
| Spam rescues (7d) | Sum `spamRescued` |
| Status | active / paused / error |
### Time series charts (3045 day window)
| Chart | Series |
|-------|--------|
| Volume | planned vs actual sends |
| Engagement | replies, rescues |
| Placement | inbox % vs spam % |
| Ramp progress | cumulative sends vs recipe curve |
### Tables
| Table | Columns |
|-------|---------|
| Recent activity | time, satellite, subject, status, placement |
| Satellite health | email, status, last send, last error |
| Daily breakdown | date, sent, replied, spam, inbox rate |
## API endpoints (target)
```
GET /api/campaigns/:id/stats?range=7d|30d|all
GET /api/campaigns/:id/stats/today
GET /api/campaigns/:id/stats/daily?from=&to=
GET /api/mailboxes/:id/stats
GET /api/workspace/overview
```
### Response example (`/stats/today`)
```json
{
"campaignId": "...",
"date": "2026-06-20",
"dayIndex": 12,
"planned": { "sends": 18, "replies": 6 },
"actual": { "sends": 15, "replies": 5, "spamDetected": 2, "spamRescued": 2 },
"rates": { "reply": 33.3, "inbox": 86.7 },
"recipe": "grow",
"onTrack": true
}
```
## Aggregation strategy
### Real-time counters
Update `DailySchedule.actualSends` on each successful send (transactional).
### Nightly rollup
Job at 00:05 campaign TZ:
- Finalize yesterday's `DailyStat`
- Compute 7d rolling averages
- Evaluate auto-pause rules
### Materialized vs computed
| Metric | Strategy |
|--------|----------|
| Today's counts | Real-time counters |
| Historical | `DailyStat` table |
| Inbox rate | `PlacementEvent` aggregation |
| Recipe curve overlay | Computed from `DailySchedule` |
## Export
- CSV export of daily stats
- JSON export for BI (future)
## Alerts (phase 2)
| Alert | Channel |
|-------|---------|
| Spam rate threshold | Email, webhook |
| Mailbox error | Email, in-app |
| Campaign completed | In-app |
| Ramp behind schedule | In-app |
## FillCare example view
**hello@fillcareapp.com — Day 12/30 — Grow**
| Metric | Value |
|--------|-------|
| Scheduled today | 18 |
| Sent | 15 |
| Replied | 5 (33%) |
| Spam detected | 2 |
| Spam rescued | 2 |
| Inbox rate (7d) | 94% |

View file

@ -0,0 +1,143 @@
# ESP Compliance Playbook
How to avoid Google, Microsoft, and Yahoo flagging Warmbox as an abusive or detectable warmup service.
## Threat model
ESPs detect warmup services via:
| Signal | Detection vector |
|--------|------------------|
| Volume spikes | Sudden send increases |
| Machine cadence | Sends every N minutes exactly |
| Identical content | Same body hash across mailboxes |
| Artificial pools | Known warmup IP/domain clusters |
| 100% engagement | Every message opened/replied |
| Bad pool hygiene | High bounce rate in network |
| Missing auth | SPF/DKIM/DMARC failures |
| Spam complaints | User marks as spam |
## Warmbox mitigations
### 1. Own satellite pool (phase 1)
User controls all mailboxes. No unknown third-party addresses. Lower pool-toxicity risk than public warmup networks.
### 2. Human-like scheduling
- Send windows (business hours)
- Random jitter 45180 min between sends
- 48h volume cap +20%
- Weekend volume reduction (optional)
### 3. Realistic engagement
- Reply rate 3045%, not 100%
- Some messages: rescue + read, no reply
- Varied AI content with validation
- Threaded replies with proper `In-Reply-To`
### 4. Content hygiene
- Plain text, no links/pixels in warmup
- No placeholder templates
- Industry-appropriate language
- No sales spam triggers
### 5. Authentication
Pre-flight checks before campaign start:
| Check | Required |
|-------|----------|
| SPF record | Pass or warn |
| DKIM signing | Pass or warn |
| DMARC policy | Present (p=none minimum) |
| Reverse DNS | Warn if missing |
`POST /mailboxes/:id/dns-check` (future)
### 6. Volume discipline
| Rule | Value |
|------|-------|
| Max warmup sends/day/mailbox | 50 hard cap |
| Recommended max | 40 |
| New domain age before warmup | 14 days recommended |
| Never stop warmup during cold sends | Maintenance mode 20% |
### 7. Auto-pause
| Trigger | Action |
|---------|--------|
| Bounce rate >5% | Pause campaign |
| Spam rate >30% (3 days) | Pause + alert |
| Auth errors | Mark error, stop |
| Complaint signal (future) | Immediate pause |
### 8. Peer network (phase 3 only)
If added:
- Verify DNS on every peer mailbox
- Remove bouncing addresses within 24h
- No cross-tenant data leakage
- Rate limit peer assignments
- Monitor pool spam rate aggregate
## Provider-specific guidance
### Google Workspace / Gmail
- Use app passwords or OAuth
- Enable [Postmaster Tools](https://postmaster.google.com)
- Target spam rate <0.1% (alert at 0.3%)
- Avoid connecting consumer Gmail accounts with suspicious bulk patterns
- `[Gmail]/Spam` rescue is user-owned mail — acceptable
### Microsoft 365 / Outlook
- Enable IMAP/SMTP auth
- Respect sending limits (~30/min, ~10k/day)
- Monitor SNDS (future)
- Junk folder rescue
### Yahoo
- App password required
- Lower daily limits than Google
- Watch Bulk folder
## What Warmbox is NOT
- Not a cold email sender — don't blast prospects through warmup
- Not a link tracker — no pixels in warmup phase
- Not a shared bot network — own satellites first
## Legal & ToS
Operators must:
- Own or control all connected mailboxes
- Comply with provider Terms of Service
- Not use warmup to deceive recipients (warmup is internal engagement only)
Document in Terms of Service before SaaS launch.
## Compliance checklist (pre go-live)
- [ ] SPF/DKIM/DMARC verified on primary domain
- [ ] Domain age ≥14 days
- [ ] Recipe = Grow, duration ≥30 days
- [ ] Reply rate ≤45%
- [ ] ≥4 satellites assigned
- [ ] AI validation enabled
- [ ] Send window configured
- [ ] Postmaster Tools connected (Gmail)
- [ ] Test campaign 48h with 2 sends/day before full ramp
## Rumor: "ESPs detect warmup services"
**Partially true.** ESPs detect **patterns**, not brand names. Mitigation is behavioral realism + own pool + auth + volume discipline — not hiding that warmup exists.
Warmbox should be undetectable as a **low-quality bot network**, not invisible as a product.

158
docs/12-architecture.md Normal file
View file

@ -0,0 +1,158 @@
# System Architecture
## Deployment model (hybrid)
**Phase 12:** Self-hosted single-tenant (FillCare) with web UI
**Phase 3+:** Multi-tenant SaaS on shared infrastructure
Design all entities with optional `workspaceId` from day one.
## High-level architecture
```mermaid
flowchart TB
subgraph client [Client Layer]
WebUI[Web Dashboard]
API_Consumers[API / CLI]
end
subgraph app [Application Layer]
API[Express API]
Auth[Auth Middleware]
CampaignSvc[Campaign Service]
RecipeSvc[Recipe Engine]
StatsSvc[Statistics Service]
ContentSvc[AI Content Service]
end
subgraph workers [Worker Layer]
Scheduler[Cron Scheduler]
SendWorker[Send Queue Worker]
RescueWorker[Rescuer Worker]
StatsWorker[Stats Rollup Worker]
end
subgraph connectors [Connectors]
SMTP[nodemailer]
IMAP[imapflow]
AI[OpenRouter]
end
subgraph data [Data Layer]
DB[(SQLite → Postgres)]
Queue[(Job Queue)]
end
WebUI --> API
API_Consumers --> API
API --> Auth
API --> CampaignSvc
API --> StatsSvc
Scheduler --> SendWorker
Scheduler --> RescueWorker
SendWorker --> SMTP
SendWorker --> ContentSvc
ContentSvc --> AI
RescueWorker --> IMAP
RescueWorker --> ContentSvc
workers --> DB
API --> DB
SendWorker --> Queue
```
## Component responsibilities
| Component | Responsibility |
|-----------|----------------|
| **API** | CRUD mailboxes, campaigns, settings; stats queries |
| **Campaign Service** | Lifecycle: draft → active → paused → completed |
| **Recipe Engine** | Compute `DailySchedule` from recipe params |
| **Dispatcher / Send Worker** | Enqueue sends with jitter; role-aware pairing |
| **Rescuer Worker** | IMAP spam scan, rescue, reply sampling |
| **AI Content Service** | Generate + validate email bodies |
| **Stats Service** | Aggregations, rollups, on-track calculation |
| **Scheduler** | Cron triggers; reload on settings change |
## Data store evolution
| Phase | Database | Queue |
|-------|----------|-------|
| MVP (now) | SQLite | In-process cron |
| Phase 2 | SQLite | SQLite-backed queue or BullMQ |
| SaaS | PostgreSQL | Redis + BullMQ |
Use Prisma driver adapters; swap datasource URL for Postgres.
## Multi-tenant (future)
```mermaid
flowchart LR
subgraph tenantA [Workspace A]
A1[Primary]
A2[Satellites]
end
subgraph tenantB [Workspace B]
B1[Primary]
B2[Satellites]
end
API --> tenantA
API --> tenantB
```
- Row-level isolation via `workspaceId`
- JWT auth with workspace scope
- Encrypted credentials per workspace key (KMS in SaaS)
## Frontend architecture (recommended)
| Layer | Tech options |
|-------|----------------|
| Framework | Next.js or Vite + React |
| UI | shadcn/ui + Tailwind |
| Data | TanStack Query |
| Charts | Recharts |
| Auth | Clerk / Auth.js (SaaS) |
Serve as separate app or static build proxied by Express.
## Observability
| Concern | Tool |
|---------|------|
| Logs | Pino (existing) |
| Errors | Sentry |
| Metrics | Prometheus / Grafana (SaaS) |
| Job tracing | Correlation ID per send |
## Security boundaries
- Credentials encrypted at rest (AES-256-GCM)
- API key (self-hosted) → JWT (SaaS)
- No plaintext passwords in logs
- IMAP/SMTP connections TLS-only
## Integration points (external)
| System | Integration | Phase |
|--------|-------------|-------|
| Instantly / Smartlead | Read-only: pause warmup when they send | 3 |
| Google Postmaster | Pull domain stats | 3 |
| Webhooks | Campaign events | 2 |
| Stripe | Billing per primary | SaaS |
## Directory structure (target)
```
warmbox/
├── apps/
│ ├── api/ # Express (current src/)
│ └── web/ # Dashboard
├── packages/
│ ├── database/ # Prisma schema
│ ├── recipe-engine/
│ └── content-validator/
├── docs/
└── prisma/
```
Monorepo optional; single package acceptable through phase 2.

122
docs/13-api-evolution.md Normal file
View file

@ -0,0 +1,122 @@
# API Evolution
## Current endpoints (v0)
| Method | Path |
|--------|------|
| GET | `/health` |
| POST/GET/PATCH/DELETE | `/api/inboxes` |
| POST | `/api/inboxes/:id/test` |
| GET | `/api/logs` |
| GET/PATCH | `/api/settings` |
## Target REST API (v1)
### Mailboxes
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/mailboxes` | Create (role: primary \| satellite) |
| GET | `/api/mailboxes` | List with filters |
| GET | `/api/mailboxes/:id` | Detail |
| PATCH | `/api/mailboxes/:id` | Update |
| DELETE | `/api/mailboxes/:id` | Remove |
| POST | `/api/mailboxes/:id/test` | SMTP + IMAP test |
| POST | `/api/mailboxes/:id/dns-check` | SPF/DKIM/DMARC (future) |
### Campaigns
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/campaigns` | Create warmup campaign |
| GET | `/api/campaigns` | List |
| GET | `/api/campaigns/:id` | Detail + stats summary |
| PATCH | `/api/campaigns/:id` | Update recipe/limits |
| POST | `/api/campaigns/:id/start` | Activate |
| POST | `/api/campaigns/:id/pause` | Pause |
| POST | `/api/campaigns/:id/complete` | End campaign |
| DELETE | `/api/campaigns/:id` | Delete draft only |
### Campaign satellites
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/campaigns/:id/satellites` | List assigned |
| POST | `/api/campaigns/:id/satellites` | Assign `{ mailboxId }` |
| DELETE | `/api/campaigns/:id/satellites/:mailboxId` | Unassign |
### Statistics
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/campaigns/:id/stats/today` | Today's metrics |
| GET | `/api/campaigns/:id/stats/daily` | Time series |
| GET | `/api/campaigns/:id/stats` | Summary + range |
| GET | `/api/workspace/overview` | All primaries at a glance |
### Logs
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/logs` | Paginated (existing, add campaignId filter) |
### Recipes
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/recipes/preview` | Preview schedule without saving |
| GET | `/api/recipes/templates` | Default params per recipe type |
### Jobs (operator)
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/jobs/dispatcher/run` | Manual trigger (dev) |
| POST | `/api/jobs/rescuer/run` | Manual trigger (dev) |
### Settings
| Method | Path | Description |
|--------|------|-------------|
| GET/PATCH | `/api/settings` | Global + AI config |
## Create campaign example
```json
POST /api/campaigns
{
"primaryMailboxId": "clx...",
"name": "FillCare hello@",
"recipe": "grow",
"durationDays": 30,
"minEmailsPerDay": 1,
"maxEmailsPerDay": 40,
"replyRatePercent": 30,
"satelliteMailboxIds": ["clx...", "clx...", "clx..."],
"timezone": "America/Vancouver",
"sendWindowStart": "08:00",
"sendWindowEnd": "18:00"
}
```
## Backward compatibility
- Keep `/api/inboxes` as alias to `/api/mailboxes` for one release
- Deprecation header on v0 routes
## Error format
```json
{
"error": "Human message",
"code": "CAMPAIGN_SATELLITE_MIN",
"details": { "minRequired": 2 }
}
```
## Auth evolution
| Phase | Auth |
|-------|------|
| Self-hosted | Optional `x-api-key` |
| SaaS | JWT + workspace header |

144
docs/14-frontend-ux-spec.md Normal file
View file

@ -0,0 +1,144 @@
# Frontend UX Specification
## Design principles
1. **Confidence-first** — show health, spam rate, and on-track status prominently
2. **Progressive disclosure** — simple defaults; expert options collapsed
3. **Safe defaults** — Grow, 30 days, 30% reply, max 40/day pre-selected
4. **Mobile-readable** — dashboard works on tablet; setup flows desktop-first
## Tech recommendation
- **Vite + React + TypeScript**
- **shadcn/ui** + Tailwind
- **TanStack Query** for API state
- **Recharts** for time series
## Information architecture
```
Dashboard (overview)
├── Campaigns
│ ├── [Campaign detail]
│ │ ├── Overview tab
│ │ ├── Activity log tab
│ │ ├── Satellites tab
│ │ └── Settings tab
│ └── New campaign wizard
├── Mailboxes
│ ├── All mailboxes
│ ├── Add primary
│ └── Add satellite
├── Activity (global log)
└── Settings
├── AI provider
├── Schedule defaults
└── API keys
```
## Key screens
### 1. Dashboard (home)
**Purpose:** At-a-glance health of all warming primaries.
| Section | Content |
|---------|---------|
| Hero stats | Active campaigns, total sends today, avg inbox rate |
| Campaign cards | One card per primary: day X/Y, sends, spam %, status badge |
| Alerts | Error mailboxes, high spam rate, paused campaigns |
| Quick actions | Add primary, Add satellite, New campaign |
### 2. New campaign wizard (4 steps)
**Step 1 — Primary**
Select existing primary mailbox or add new.
**Step 2 — Recipe**
Radio cards: Grow (recommended), Flat, Randomized, Custom.
Sliders: duration 3045, min/max sends, reply rate 30%.
**Step 3 — Satellites**
Multi-select from satellite pool; minimum 2 warning; recommend 4+.
**Step 4 — Review**
Preview ramp chart; confirm; Start campaign.
### 3. Campaign detail — Overview
| Widget | Data |
|--------|------|
| Progress ring | Day 12/30 |
| Sends today | 15/18 bar |
| Replies today | 5/6 bar |
| Inbox rate 7d | 94% with trend arrow |
| Ramp chart | Planned vs actual line chart |
| Placement | Inbox vs spam stacked bar |
Actions: Pause, Edit settings, Export CSV.
### 4. Campaign detail — Satellites
Table: email, provider, status, last activity, remove button.
Add satellite dropdown.
### 5. Campaign detail — Activity
Filterable log table (reuse `/api/logs`).
### 6. Add mailbox modal
Tabs: Primary | Satellite
Fields: email, provider preset (Gmail/Yahoo/Outlook/Custom), SMTP/IMAP auto-fill, app password, industry, test connection button.
Provider presets fill host/port from [05-primary-satellite-topology.md](./05-primary-satellite-topology.md).
### 7. Mailbox detail
Connection status, DNS check results (future), assigned campaigns, error history.
### 8. Settings
- AI: API key, model, test generation button
- Defaults: recipe, duration, reply rate
- Danger zone: reset campaign, delete mailbox
## UX patterns
| Pattern | Use |
|---------|-----|
| Status badges | active=green, paused=yellow, error=red, completed=blue |
| Empty states | Guided CTA to add first primary |
| Loading skeletons | Stats cards |
| Optimistic updates | Pause/resume |
| Toast notifications | Test connection result, save settings |
## Recipe selector UI
```
┌─────────────────────────────────────────┐
│ ● Grow (Recommended) │
│ Progressive ramp over 30 days │
├─────────────────────────────────────────┤
│ ○ Flat │
│ Steady volume every day │
├─────────────────────────────────────────┤
│ ○ Randomized │
│ Variable volume for testing │
├─────────────────────────────────────────┤
│ ○ Custom │
│ Expert day-by-day schedule │
└─────────────────────────────────────────┘
```
## Accessibility
- WCAG 2.1 AA contrast on charts
- Keyboard navigation for wizards
- Screen reader labels on status badges
## MVP frontend scope (phase 2)
**In scope:** Dashboard, campaign wizard, campaign detail, mailbox CRUD, connection test, activity log.
**Out of scope:** Billing, team members, OAuth connect, mobile app.

View file

@ -0,0 +1,73 @@
# Security & Compliance
## Credential handling
| Data | Storage | Access |
|------|---------|--------|
| Mailbox passwords | AES-256-GCM encrypted | Decrypt only in send/IMAP workers |
| AI API keys | Env or encrypted settings | Never returned in GET settings |
| API keys | Env | Header auth self-hosted |
### SaaS evolution
- Per-tenant encryption keys (KMS)
- Secrets rotation without re-entering passwords where OAuth applies
- Audit log for credential access
## Authentication roadmap
| Phase | Model |
|-------|-------|
| 1 | Optional single API key |
| 2 | Session auth for web UI |
| 3 | Multi-tenant JWT + RBAC |
### Roles (SaaS)
| Role | Permissions |
|------|-------------|
| Owner | All + billing |
| Admin | Campaigns, mailboxes |
| Viewer | Read-only stats |
## Data privacy
- Warmup email bodies may contain AI-generated business text — not PII by default
- Store minimum content needed for thread continuity
- Retention policy: 90 days for logs (configurable)
- GDPR: export + delete workspace data
## Transport security
- TLS required for SMTP (587/465) and IMAP (993)
- HTTPS for API and UI
- No credential logging in Pino output
## Abuse prevention (SaaS)
- Rate limit API
- Verify email domain ownership before warming (DNS TXT challenge)
- Block disposable email domains for signup
- Monitor outbound volume per tenant
## Legal
Before public SaaS:
- Terms of Service (user owns mailboxes, compliant use)
- Privacy Policy
- Acceptable Use Policy (no spamming prospects via warmup)
- ESP ToS disclaimer
## Incident response
| Event | Response |
|-------|----------|
| Credential leak | Rotate ENCRYPTION_KEY, force password re-entry |
| ESP suspension | Pause all campaigns for affected mailbox |
| AI generating harmful content | Block send, alert operator |
## Compliance frameworks (future)
- SOC 2 Type II (SaaS scale)
- Not required for self-hosted FillCare deployment

172
docs/16-roadmap.md Normal file
View file

@ -0,0 +1,172 @@
# Product Roadmap
## Phase 0 — Foundation (complete)
**Status:** Done
- Barebone MVP: flat inbox pool, dispatcher, rescuer, AI, REST API
- Prisma 7 + SQLite
- OpenRouter AI integration
- Documentation scaffold (this folder)
---
## Phase 1 — Production warmup core (810 weeks)
**Goal:** Reliable multi-primary warmup with recipes, roles, and stats. Self-hosted backend only.
### Sprint 1.1 — Domain model & roles (2 weeks)
- Add `role` to mailbox (primary / satellite)
- Add `WarmupCampaign` entity
- Campaign ↔ satellite assignments
- Migrate existing inboxes
- Deprecate flat random pairing
**Deliverable:** API to create campaign with satellites; no sends yet.
### Sprint 1.2 — Recipe engine (2 weeks)
- Implement Grow, Flat, Randomized, Custom
- `DailySchedule` generation
- Duration 3045 days, min/max validation
- 48h volume cap
- `POST /recipes/preview`
**Deliverable:** Unit-tested schedule for all 4 recipes.
### Sprint 1.3 — Campaign dispatcher (2 weeks)
- Role-aware send: satellite → primary only
- Reply path: primary → satellite
- Jitter + send windows
- Campaign day rollover
- Idempotency keys
**Deliverable:** End-to-end warmup with one primary + 4 satellites.
### Sprint 1.4 — Reply rate engine (1 week)
- Probabilistic 30% default replies
- `engaged_no_reply` status
- Daily reply budget tracking
**Deliverable:** Reply rate within ±5% of target over 7 days.
### Sprint 1.5 — Stats API (1 week)
- `DailyStat` rollups
- `/campaigns/:id/stats/today` and `/stats/daily`
- On-track calculation
**Deliverable:** JSON stats matching manual log counts.
### Sprint 1.6 — AI content validator (1 week)
- Placeholder regex rejection
- Spam word tiers
- Regenerate loop (max 3)
- Mailbox profile (name, company)
**Deliverable:** Zero placeholder emails in 100-test run.
---
## Phase 2 — Operator experience (68 weeks)
**Goal:** Web UI + hardening for FillCare daily use.
### Sprint 2.1 — Web dashboard MVP (3 weeks)
- Vite + React + shadcn
- Dashboard, campaign wizard, campaign detail
- Mailbox add/test flows
### Sprint 2.2 — Spam rescue hardening (2 weeks)
- Per-UID log matching
- Placement tracking
- Folder auto-discovery
- Auto-pause on spam thresholds
### Sprint 2.3 — DNS & health checks (1 week)
- SPF/DKIM/DMARC preflight
- Provider presets (Gmail/Yahoo/Outlook)
### Sprint 2.4 — Job queue (2 weeks)
- Replace batch cron with queued sends
- Manual job triggers for dev
- Better overlap protection
---
## Phase 3 — SaaS readiness (812 weeks)
**Goal:** Multi-tenant, billing, optional integrations.
- Postgres migration path
- Workspace + JWT auth
- Stripe billing per primary
- Webhooks (campaign events)
- Google Postmaster integration
- Session-based web auth
- Optional peer network (beta)
---
## Phase 4 — Scale & ecosystem (ongoing)
- Instantly / Smartlead sync
- Inbox placement tests
- Team roles
- White-label
- Mobile notifications
---
## Milestone timeline (visual)
```
2026 Q2 [Phase 1: Core backend ████████░░]
2026 Q3 [Phase 2: Web UI + hardening ░░░░████████]
2026 Q4 [Phase 3: SaaS beta ░░░░░░░░████]
```
---
## FillCare-specific milestone
| Week | Milestone |
|------|-----------|
| 1 | hello@fillcareapp.com campaign created, 4 satellites assigned |
| 2 | Grow recipe running, stats visible via API |
| 4 | Web dashboard live |
| 6 | 30-day warmup complete, inbox rate ≥95% |
| 8 | Maintenance mode + cold email tool connected |
---
## Definition of done (per phase)
### Phase 1 done when
- [ ] 2+ primaries warming concurrently
- [ ] All 4 recipes pass integration tests
- [ ] Stats API accurate
- [ ] Reply rate configurable and honored
- [ ] AI validator blocks placeholders
- [ ] ESP compliance checklist passable
### Phase 2 done when
- [ ] Operator never needs curl for daily use
- [ ] Auto-pause tested on simulated spam spike
- [ ] FillCare running 30-day campaign via UI
### Phase 3 done when
- [ ] Second customer onboarded in isolated workspace
- [ ] Billing active
- [ ] Security review complete

View file

@ -0,0 +1,181 @@
# Agile Epics & Backlog
## Epic map
| Epic ID | Name | Phase | Priority |
|---------|------|-------|----------|
| E1 | Campaign & role model | 1 | P0 |
| E2 | Recipe engine | 1 | P0 |
| E3 | Campaign dispatcher | 1 | P0 |
| E4 | Reply rate engine | 1 | P0 |
| E5 | Statistics & reporting | 1 | P0 |
| E6 | AI content safety | 1 | P0 |
| E7 | Spam rescue v2 | 2 | P0 |
| E8 | Web dashboard | 2 | P1 |
| E9 | DNS & deliverability checks | 2 | P1 |
| E10 | Job queue | 2 | P1 |
| E11 | Multi-tenant SaaS | 3 | P2 |
| E12 | Peer network (optional) | 3 | P3 |
| E13 | External integrations | 34 | P3 |
---
## E1 — Campaign & role model
### Stories
| ID | Story | Points |
|----|-------|--------|
| E1-1 | As an operator, I can mark a mailbox as primary or satellite | 3 |
| E1-2 | As an operator, I can create a warmup campaign for a primary | 5 |
| E1-3 | As an operator, I can assign/remove satellites from a campaign | 3 |
| E1-4 | As an operator, I can run multiple campaigns for different primaries | 5 |
| E1-5 | As a developer, migrate existing Inbox rows to new schema | 3 |
**Integration:** Prisma migration + `/api/campaigns` routes
---
## E2 — Recipe engine
### Stories
| ID | Story | Points |
|----|-------|--------|
| E2-1 | As an operator, I can select Grow recipe with 30-day duration | 5 |
| E2-2 | As an operator, I can select Flat recipe | 3 |
| E2-3 | As an operator, I can select Randomized recipe | 5 |
| E2-4 | As an expert, I can define Custom per-day schedule | 8 |
| E2-5 | As an operator, I can preview ramp chart before starting | 3 |
| E2-6 | System enforces min 1, max 50 emails/day | 2 |
| E2-7 | System enforces 48h +20% volume cap | 5 |
**Integration:** `packages/recipe-engine` or `src/services/recipe.service.ts`
---
## E3 — Campaign dispatcher
### Stories
| ID | Story | Points |
|----|-------|--------|
| E3-1 | Satellites send only to assigned primary | 5 |
| E3-2 | Primary replies only to assigned satellites | 5 |
| E3-3 | Sends respect daily schedule from recipe | 5 |
| E3-4 | Sends distributed with jitter in send window | 5 |
| E3-5 | Campaign auto-completes after durationDays | 3 |
| E3-6 | Maintenance mode at 20% volume post-campaign | 5 |
**Integration:** Replace `dispatcher.job.ts` with campaign-aware version
---
## E4 — Reply rate engine
### Stories
| ID | Story | Points |
|----|-------|--------|
| E4-1 | Operator sets reply rate 30% (default) | 2 |
| E4-2 | System caps reply rate at 45% | 1 |
| E4-3 | Rescuer samples replies probabilistically | 8 |
| E4-4 | Non-replied messages still rescued and flagged | 3 |
| E4-5 | Dashboard shows reply rate actual vs target | 3 |
---
## E5 — Statistics & reporting
### Stories
| ID | Story | Points |
|----|-------|--------|
| E5-1 | API returns today's planned vs actual sends | 5 |
| E5-2 | API returns daily time series for 30 days | 5 |
| E5-3 | API returns spam detected/rescued counts | 3 |
| E5-4 | API returns inbox placement rate | 5 |
| E5-5 | Nightly rollup job materializes DailyStat | 5 |
| E5-6 | Workspace overview endpoint for all primaries | 5 |
---
## E6 — AI content safety
### Stories
| ID | Story | Points |
|----|-------|--------|
| E6-1 | Validator rejects bracket placeholders | 3 |
| E6-2 | Validator rejects tier-1 spam words | 3 |
| E6-3 | Auto-regenerate up to 3 times on failure | 3 |
| E6-4 | Mailbox profile supplies real name/company | 3 |
| E6-5 | Plain text only, no links in warmup body | 2 |
---
## E7 — Spam rescue v2
### Stories
| ID | Story | Points |
|----|-------|--------|
| E7-1 | Match spam UID to single WarmupLog | 5 |
| E7-2 | Track placement inbox vs spam | 3 |
| E7-3 | Auto-discover spam folder per provider | 5 |
| E7-4 | Auto-pause campaign on high spam rate | 5 |
---
## E8 — Web dashboard
### Stories
| ID | Story | Points |
|----|-------|--------|
| E8-1 | Dashboard shows all active campaigns | 5 |
| E8-2 | 4-step campaign wizard | 8 |
| E8-3 | Campaign detail with charts | 8 |
| E8-4 | Add mailbox with provider presets | 5 |
| E8-5 | Connection test from UI | 3 |
| E8-6 | Global activity log view | 3 |
---
## Integration breakdown (build order)
```mermaid
flowchart TD
E1[E1 Campaign model] --> E2[E2 Recipe engine]
E2 --> E3[E3 Dispatcher]
E1 --> E4[E4 Reply rate]
E3 --> E5[E5 Statistics]
E3 --> E6[E6 AI validator]
E4 --> E7[E7 Spam rescue v2]
E5 --> E8[E8 Web UI]
E6 --> E8
E7 --> E8
```
## Suggested first sprint (2 weeks)
1. E1-1, E1-2, E1-3 (campaign CRUD + roles)
2. E2-1, E2-6 (Grow recipe + validation)
3. E6-1 (placeholder validator)
**Sprint goal:** Create FillCare campaign via API; preview Grow schedule; no sends yet.
## Story point scale
| Points | Meaning |
|--------|---------|
| 12 | Few hours |
| 3 | Half day |
| 5 | 1 day |
| 8 | 23 days |
## Backlog hygiene
- Every story links to spec doc section
- Acceptance criteria required before sprint commit
- Demo: FillCare primary visible in stats after each epic

61
docs/18-open-questions.md Normal file
View file

@ -0,0 +1,61 @@
# Open Questions
Decisions needed before or during implementation. Update this doc as answers are confirmed.
## Confirmed
| Question | Answer | Date |
|----------|--------|------|
| Deployment model | Hybrid: self-hosted first, SaaS-ready | 2026-06 |
| Satellite pool | Own satellites first; peer network optional later | 2026-06 |
| Warmup duration | 3045 days recommended | 2026-06 |
| Reply rate default | 30%, max 45% | 2026-06 |
| Daily volume | Min 1, default max 40, cap 50 | 2026-06 |
## Product
| # | Question | Options | Impact |
|---|----------|---------|--------|
| Q1 | Can one satellite serve multiple primaries? | A) No (dedicated) B) Yes with toggle | Dispatcher pairing |
| Q2 | Should primary ever send unsolicited to satellites (not replies)? | A) No B) Yes for variety | Traffic rules |
| Q3 | Post-campaign behavior default? | A) Stop B) Maintenance 20% C) Ask user | Campaign lifecycle |
| Q4 | Weekend sending? | A) Same volume B) 50% C) Pause | Scheduling |
| Q5 | FillCare: is hello@ on Google Workspace? | Confirm SMTP/IMAP hosts | Onboarding docs |
## Technical
| # | Question | Options | Impact |
|---|----------|---------|--------|
| Q6 | Job queue tech for phase 2? | A) BullMQ+Redis B) pg-boss C) SQLite queue | Infra |
| Q7 | When to migrate SQLite → Postgres? | A) Phase 3 B) Phase 2 if UI heavy | Schema hosting |
| Q8 | OAuth for Gmail/Outlook in SaaS? | A) App passwords only B) OAuth phase 3 | Auth complexity |
| Q9 | Monorepo split api/web? | A) Yes phase 2 B) Stay single package | Repo structure |
| Q10 | Warmup content: store full body forever? | A) 90d retention B) Forever | Storage/privacy |
## Business (SaaS phase)
| # | Question | Options | Impact |
|---|----------|---------|--------|
| Q11 | Pricing model? | Per primary / per satellite / flat | Billing |
| Q12 | Free tier? | Yes limited primaries / No | GTM |
| Q13 | Brand name: Warmbox or FillCare-internal? | | Marketing |
| Q14 | Open source core? | A) Private B) OSS + hosted | License |
## Compliance
| # | Question | Options | Impact |
|---|----------|---------|--------|
| Q15 | Require DNS verification before campaign start? | A) Warn B) Block | Onboarding friction |
| Q16 | Canada (PIPEDA) data residency for SaaS? | | Hosting region |
## How to resolve
1. Add answer to **Confirmed** table
2. Update affected spec docs
3. Create implementation story in [17-agile-epics-backlog.md](./17-agile-epics-backlog.md)
## Questions for you (next conversation)
1. **Q5** — Is `hello@fillcareapp.com` on Google Workspace, Microsoft 365, or cPanel?
2. **Q1** — Should satellites be dedicated per primary, or shared across your primaries?
3. **Q3** — After 30-day warmup completes, auto-switch to maintenance mode or stop?

55
docs/19-risk-register.md Normal file
View file

@ -0,0 +1,55 @@
# Risk Register
| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|----|------|------------|--------|------------|-------|
| R1 | ESP suspends satellite Gmail account | Medium | High | Own pool, volume caps, app passwords, monitor errors | Operator |
| R2 | ESP detects warmup patterns | Medium | High | Jitter, reply sampling, content diversity, no links | Engineering |
| R3 | AI generates placeholder/spam content | Medium | High | Post-validation, regen loop, blocklists | Engineering |
| R4 | Shared peer pool toxicity (phase 3) | High | Critical | Delay peer network; bounce monitoring | Product |
| R5 | SQLite corruption / lock under load | Low | Medium | Migrate to Postgres before SaaS; queue writes | Engineering |
| R6 | Credential breach | Low | Critical | Encryption, no log leaks, KMS for SaaS | Security |
| R7 | IMAP spam folder mismatch | Medium | Medium | Auto-discovery, provider presets | Engineering |
| R8 | 100% reply rate detection | High (now) | Medium | Reply rate engine (E4) | Engineering |
| R9 | Operator sets max 50 on day 1 | Medium | High | Grow recipe default; UI warnings | Product |
| R10 | Google Postmaster spam rate >0.3% | Medium | High | Auto-pause, reduce volume, rescue | Operations |
| R11 | Legal/ToS violation (warmup as deception) | Low | High | ToS: only owned mailboxes; internal engagement | Legal |
| R12 | OpenRouter free model quality | Medium | Low | Model fallback list; paid tier option | Engineering |
| R13 | Cron overlap double-sends | Medium | Medium | Idempotency keys, job queue | Engineering |
| R14 | Primary warms too fast vs domain age | Medium | High | Domain age check; min 14-day warning | Product |
| R15 | Competitor ESP crackdown on warmup industry | Low | High | Differentiate on own-pool + self-hosted control | Product |
## Risk heat map
```
Impact
High | R4 R6 R9 R10 R11 R15
| R1 R2 R3 R8 R14
Medium | R5 R7 R13
Low | R12
+------------------
Low Med High
Likelihood
```
## Top 5 actions (before scaling FillCare)
1. **Implement E4** — reply rate sampling (addresses R8, R2)
2. **Implement E6** — AI validator (addresses R3)
3. **Implement E2** — Grow recipe enforced (addresses R9, R14)
4. **Run DNS checklist** on fillcareapp.com (addresses R10, R14)
5. **Add auto-pause** on spam rate (addresses R10, R1)
## Monitoring triggers
| Metric | Warning | Critical |
|--------|---------|----------|
| Spam placement rate | >15% 7d avg | >30% 3d avg |
| Bounce rate | >2% | >5% |
| Auth failures | 1 per mailbox | 3 consecutive |
| AI validation fail rate | >5% | >15% |
| Reply rate deviation | >±10% from target | >±20% |
## Review cadence
- Weekly during active warmup campaigns
- Update register after each sprint retro

52
docs/README.md Normal file
View file

@ -0,0 +1,52 @@
# Warmbox Documentation
Planning artifacts for evolving Warmbox from a barebone MVP into a production-grade cold email warmup platform and eventual SaaS.
## Decisions (confirmed)
| Decision | Choice |
|----------|--------|
| Deployment | **Hybrid** — self-hosted + web UI first; architecture ready for multi-tenant SaaS later |
| Satellite pool | **Own satellites first**; optional shared peer network in a later phase |
| Warmup duration | **3045 days** recommended default |
| Reply rate default | **30%** (max recommended **45%**) |
| Daily volume | Min **1**, default max **40**, hard cap **50** |
## Document index
| # | Document | Purpose |
|---|----------|---------|
| 00 | [Product Brief](./00-product-brief.md) | Vision, personas, success metrics |
| 01 | [Current State & Gaps](./01-current-state-and-gaps.md) | MVP inventory vs target |
| 02 | [Glossary](./02-glossary.md) | Domain language |
| 03 | [Domain Model](./03-domain-model.md) | Future entities & relationships |
| 04 | [Warmup Recipes](./04-warmup-recipes.md) | Grow, Flat, Randomized, Custom specs |
| 05 | [Primary & Satellite Topology](./05-primary-satellite-topology.md) | Multi-primary warmup design |
| 06 | [Scheduling & Timing](./06-scheduling-and-timing.md) | Duration, windows, jitter |
| 07 | [Reply Rate Engine](./07-reply-rate-engine.md) | Probabilistic replies |
| 08 | [Spam Rescue](./08-spam-rescue-and-placement.md) | Rescue, placement tracking |
| 09 | [AI Content Policy](./09-ai-content-policy.md) | Safe B2B content rules |
| 10 | [Statistics & Reporting](./10-statistics-and-reporting.md) | Metrics & dashboards |
| 11 | [ESP Compliance Playbook](./11-esp-compliance-playbook.md) | Anti-detection, DNS, policies |
| 12 | [Architecture](./12-architecture.md) | System design (hybrid → SaaS) |
| 13 | [API Evolution](./13-api-evolution.md) | REST roadmap |
| 14 | [Frontend UX Spec](./14-frontend-ux-spec.md) | Web UI screens & flows |
| 15 | [Security & Compliance](./15-security-and-compliance.md) | Credentials, privacy, legal |
| 16 | [Roadmap](./16-roadmap.md) | Phased delivery plan |
| 17 | [Agile Epics & Backlog](./17-agile-epics-backlog.md) | Epics, stories, integrations |
| 18 | [Open Questions](./18-open-questions.md) | Unresolved decisions |
| 19 | [Risk Register](./19-risk-register.md) | Risks & mitigations |
## How to use these docs
1. **Product** — Start with `00`, `16`, `17` for prioritization.
2. **Engineering** — Use `03``13` as implementation specs per integration.
3. **Each sprint** — Pick one epic from `17`, cross-reference the relevant spec doc, update `18` when decisions are made.
## Agile workflow (recommended)
```
Epic (17) → Technical spec (0313) → Implementation PR → Stats/metrics validation (10)
```
Sprints should ship **vertical slices** (e.g. "Primary inbox + Grow recipe + daily stats") rather than horizontal layers only.