---
title: Sandbox
description: A fully working local demo environment, with live sending, replies, opens, clicks, warmup, and a full history and analytics dataset running end to end.
---

The sandbox turns a local stack into a living product demo. It seeds a showcase organization ("Sunrise Labs") whose mailboxes really send and really sync, backfills a complete history and analytics dataset so every dashboard surface is populated on first load, then runs a simulator that plays the internet: it delivers captured mail into recipient inboxes, opens tracking pixels, clicks tracked links, and replies as the seeded contacts. Campaign stats climb, the unibox fills with threaded replies, warmup interactions verify, and the dashboard updates in realtime, all through production code paths. The only simulated thing is the humans.

## Quick start

One command brings up the whole thing:

```bash
make sandbox
```

It starts the infra (postgres, redis, nats, mailpit, dovecot), runs migrations, seeds the showcase org, brings up realtime and tracking as containers (no host Elixir or cargo needed), then runs the backend, consumer, worker, dashboard, admin, and the simulator together in that terminal. Ctrl-C stops the app; the infra stays up for next time.

Then sign in at `http://localhost:5173`:

| Field | Value |
|-------|-------|
| Email | `sandbox@warmbly.test` |
| Password | `password123` |
| Organization | Sunrise Labs (Pro plan) |

The dashboard is already full from the seeded history. Live activity starts shortly after: watch the `make sandbox` output for a running feed of `delivered` / `opened` / `clicked` / `replied` lines, and Mailpit at `http://localhost:18025` for every message on the wire.

Two other entry points:

- `make sandbox SEED=false` runs the stack against existing data without re-seeding.
- `make sandbox-seed` seeds (or resets) the showcase org and exits, without the app or simulator.

## What gets seeded

`make sandbox-seed` is idempotent and safe to re-run. It provisions:

- the full `internal/seed` fixture first (plans, workers, the demo orgs), so it works on a fresh database
- the Sunrise Labs org on an active Pro subscription, owned by `sandbox@warmbly.test`, plus a second teammate (Marco Diaz, admin), one pending invitation, and an active developer API key
- AI credits: the plan's monthly allowance plus a 5,000-credit purchased top-up, so the assistant and every other AI surface work out of the box (re-seeding refills both pools)
- 20 sender mailboxes on `@sunrise.test` (a healthy warmup pool by the dashboard's own coverage bar), assigned to the worker `make sandbox` runs, spread across the whole warmup lifecycle: seven graduated senders that finished their ramp weeks ago and carry full (~50/day) cold volume, eight mid-ramp, three freshly connected, one with warmup paused, and one on reduced health with an open provider warning. Every account has matching warmup ramp stats and daily send counts backfilled, varied pool health scores, a personal signature, and a tag (VIP, Agency, or Cold); pacing is demo friendly (45s minimum gap, 100/day cap)
- campaigns in every list bucket: two active ("Sunrise Q3 launch outreach", 3 steps, 48 contacts; "Agency partnerships", 2 steps, 24 contacts), one paused, one completed, and one draft, all with tracking enabled, every step connected to the next (the scheduler only follows explicit step connections), all 20 senders in the rotation, and a realistic funnel of sent, opened, clicked, replied, and bounced timestamps spread over three weeks
- a live unified inbox: threaded inbound replies across several senders, including a meeting request, an out-of-office autoreply, a not-interested reply, and a bounce notice, with a mix of read and unread
- a CRM pipeline with five stages, five deals across them, tasks, contact notes, and per-contact activity timelines
- the analytics tables behind the deliverability and reporting views: deliverability events (opens, clicks, replies, bounces, complaints, unsubscribes), reply-intent classifications, a suppression list, and resolved and open mailbox errors
- an org audit trail (campaign created and started, mailbox connected, member invited, deal created, key created) so the audit log has depth
- reply templates, notifications (some unread), and labels everywhere they appear: folders (Outbound, Nurture), mailbox tags (VIP, Cold, Agency), and categories (Lead, Customer, Churn risk) bound to the campaigns, senders, several contacts, and inbox threads
- working credentials for every `smtp_imap` account in the database, including the older `make seed` fixtures, sealed with `CREDENTIALS_ENCRYPTION_KEY` so the worker can decrypt and use them
- an [Advisor](/guides/advisor/) showcase (see below), and one evaluation run at the end of seeding so the recommendations exist before you log in

### The Advisor showcase

The Advisor only has something to say about a workspace with problems, so the seeder gives Sunrise Labs a few deliberate ones. Everything else is normalized to a well-run baseline first (every sender at the 50/day safe cap, on its own verified tracking domain, with SPF, DKIM, and DMARC passing), so the findings that remain are specific rather than a wall of the same card repeated twenty times.

The seeded problems, all in `internal/sandbox/advisor.go`:

- `liam.walsh@sunrise.test` has a month of real sending behind it (1,600 completed tasks) with 72 bounces and one complaint, putting it at `4.5%` bounce and `0.06%` complaint. Both come from real `tasks` and `deliverability_events` rows, so the Advisor's numbers reconcile with the Deliverability page rather than contradicting it. Its cap is also raised to 120/day.
- `yuki.sato@sunrise.test` has SPF and DKIM but no DMARC, and a 90-second gap between sends.
- `erik.lund@sunrise.test` was connected six days ago, went straight to the full 50/day cap, and has warmup paused.
- "Q4 outbound (first draft)" is a paused campaign written the way first drafts get written: one email with no follow-up, one-click unsubscribe off, a shouting subject line that truncates, 246 words, four links, bulk-mail phrasing, and an `{{if}}` with no `{{end}}` (which does not fail a send, it silently degrades to literal text). Its 160-contact audience is a quarter shared inboxes with a third missing the first name the copy greets on.

Nothing here stops the sandbox sending: caps go up rather than down, gaps get shorter, the authentication columns are observation-only, and the deliberately-rough campaign stays paused so it never mails the synthetic list.

Seeding ends by running one evaluation and printing what it found, so `make sandbox-seed` on its own tells you the whole path works. Expect roughly twenty findings and a score in the thirties.

## How it works

Real mail servers stand in for the internet's mail infrastructure:

- **Mailpit** is the SMTP sink. Every mailbox's outbound server points at it, so all sends (campaign and warmup) are captured and visible in its UI.
- **Dovecot** hosts the mailboxes' inboxes. It accepts any username with the password `sandbox` and auto-creates the maildir on first use. The worker's real IMAP sync polls it every minute.

The simulator bridges the two and plays every human:

1. It polls Mailpit's API for new messages (the read flag is its cursor).
2. Mail addressed to a hosted mailbox (for example warmup mail between pool members) is appended into that user's Dovecot inbox. The recipient's worker sync picks it up, the warmup token verifies, and engagement actions run against the real IMAP folders.
3. Mail addressed to a seeded contact triggers a persona: after humanized delays, most contacts open the pixel against the local tracking service, some click the tracked link, and some reply. Replies are composed with correct `In-Reply-To` threading and appended into the sending mailbox's inbox, so reply attribution, the reply classifier, `EMAIL_REPLIED` realtime events, and the unibox all behave exactly as in production. A slice of replies are out-of-office autoresponders (with `Auto-Submitted` headers) so classifier gating is visible too.

Personas are derived from a hash of the contact's address, so behavior is stable across restarts without any extra state: roughly 92% open, 57% of openers click, 45% of openers reply, with reply tones split across interested, question, not-interested, and out-of-office. The rates are deliberately hotter than real-world outreach so the demo stays lively.

## Pieces involved

| Piece | Role |
|-------|------|
| `cmd/sandbox` | seeder + simulator binary (`-seed-only`, `-simulate-only`) |
| `internal/sandbox` | seeding, history and analytics fixtures, Mailpit client, personas, IMAP delivery |
| `dovecot` compose service | IMAP host for sandbox inboxes (ports 10143/10993), `sandbox` profile |
| `mailpit` compose service | SMTP sink + capture API (ports 11025/18025) |
| `CREDENTIALS_ENCRYPTION_KEY` | seals mailbox credentials at rest; fixed dev value in the Makefile |
| `MAIL_TLS_INSECURE=true` | worker-only dev knob: skip TLS verification and allow the TLS-less Mailpit; never set in production |

## Environment variables

`make sandbox` sets everything for the default stack. When running `cmd/sandbox` by hand:

| Variable | Default | Meaning |
|----------|---------|---------|
| `PRIMARY_DB` | dev postgres on `:15432` | database for seeding and directory lookups |
| `MAILPIT_URL` | `http://localhost:18025` | Mailpit API base |
| `TRACKING_URL` | `http://localhost:3000` | tracking service for pixel/click hits |
| `DOVECOT_IMAP_ADDR` | `localhost:10993` | where the simulator appends mail |
| `DOVECOT_PASSWORD` | `sandbox` | Dovecot's static password |
| `SANDBOX_SMTP_HOST` / `SANDBOX_SMTP_PORT` | `localhost` / `11025` | seeded as the mailboxes' outbound server (the worker dials it) |
| `SANDBOX_IMAP_HOST` / `SANDBOX_IMAP_PORT` | `localhost` / `10993` | seeded as the mailboxes' inbound server (the worker dials it) |
| `CREDENTIALS_ENCRYPTION_KEY` | set by the Makefile | must match the backend's key |

## Demo script

A tour that shows the platform working end to end:

1. **Dashboard**: the overview lands full. Overall stats, top campaigns, recent activity, account health, and the daily trend chart are all populated from the seeded history before a single live send.
2. **Campaigns**: all four buckets are filled (active, paused, completed, draft). Open the Sunrise Q3 launch campaign; sent counts tick up as the scheduler paces live sends across the twenty mailboxes, and opens, clicks, and replies accumulate on top of the seeded funnel.
3. **Unibox**: replies arrive threaded under the original message with full bodies, synced by the worker over IMAP. Out-of-office autoresponders appear but do not count as human replies.
4. **Deliverability and analytics**: the deliverability view shows bounces, complaints, and unsubscribes with rates; reply intents and the suppression list are populated; mailboxes surface one resolved and one open error.
5. **Mailboxes**: each sender shows warmup activity climbing its ramp; the warmup pool exchanges verified mail between accounts.
6. **Mailpit** (`http://localhost:18025`): the raw wire view; every message the platform sends, including warmup tokens and tracking rewrites.
7. **Advisor**: Accounts, Campaigns, Contacts, and Deliverability each badge the number of urgent findings behind them. Open Accounts to see the three problem senders, then open "Q4 outbound (first draft)" under Campaigns for the copy and list findings. Press **Fix** on one (for example, warmup paused on `erik.lund`) to see the before/after preview, apply it, and undo it from the card.
8. **Realtime**: open a second browser window and watch counts and inbox rows update live without refreshing. Applying an Advisor fix in one window clears the card in the other.

## Troubleshooting

**No sends shortly after startup.** Check the backend log for campaign reconciler lines and that the worker is running with the account loaded (`email account added to worker`). The worker learns about mailboxes from the backend's reconciler within a minute of both being up.

**Sends fail with TLS errors.** The worker must run with `MAIL_TLS_INSECURE=true` (set by `make worker`). Production configurations must never set it.

**Replies missing from the unibox.** Confirm the simulator is running and the consumer is up: delivery is simulator to Dovecot to worker IMAP sync to the event bus to consumer.

**Opens/clicks not counting.** The tracking service must be up (`make sandbox` runs it as a container; `make tracking` runs it natively if you have cargo). The consumer processes tracking events into campaign stats.

**Start over.** `make db-wipe && make sandbox` reseeds from scratch. Dovecot and Mailpit state can be cleared with `docker compose -p warmbly rm -sf dovecot mailpit` followed by `make sandbox`.
