# Mailbazooka Documentation

> The safety-first, agent-native newsletter and recurring email engine.

## Overview

Mailbazooka is engineered for founders, developers, and autonomous AI agents who require strict delivery safety, hard daily send caps, and direct Gmail OAuth or BYO-SMTP integration.

## Introduction

*The safety-first, agent-native newsletter and recurring email engine.*

mailbazooka (the recurring engine) is an engineered email distribution platform built for founders, developers, and autonomous AI agents who require strict delivery safety, hard daily send caps, and direct Gmail OAuth integration without third-party email blast compromises.

### What this engine is / is not

mailbazooka is deliberately designed around hardware-inspired safety switches and transactional integrity:

- Is: A direct-to-Gmail sending engine with AES-256-GCM encrypted OAuth token persistence.
- Is: An agent-native platform exposing a Model Context Protocol (MCP) server for Claude, Cursor, and Antigravity.
- Is: A dual-gated security system with mechanical-style arming switches and recipient count verification.
- Is not: An unmonitored mass-mailer or spam blaster. Sends without confirmed counts and authorized switches refuse execution.
- Is not: A heavy SaaS with dozens of unused drag-and-drop templates. It renders clean plain-text/markdown with high deliverability.

### Quick Start in 4 Steps

Get your broadcast engine running locally or in production:

> **Local Development Mode**: When DEV_SIGNIN=1 is set in your .env, visiting /auth/google/start signs you into a local sandbox identity automatically without needing live Google OAuth credentials.

```bash
# 1. Clone & install dependencies
git clone https://github.com/0xm0w/automailer.git
cd automailer && bun install

# 2. Configure environment (.env)
cp .env.example .env
# Edit DATABASE_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, TOKEN_KEY, SESSION_SECRET

# 3. Apply PostgreSQL migrations
bun run apps/api/src/migrate.ts

# 4. Start local development
bun run dev
```

### Documentation Overview

Jump directly into the area relevant to your current workflow:

## Heartbeat Sequences

*Automated onboarding drip sequences with intelligent slot rendering.*

Every account features an automated 5-step Heartbeat Sequence designed to nurture new subscribers from day 1 to day 30 with personal, high-conviction check-ins.

### The 5 Sequential Steps

Each step sends from your authenticated Gmail address at carefully calculated intervals:

- Step 1 (Day 0 - Immediate): Welcome & founder greeting with your core product vision.
- Step 2 (Day 3): The high-leverage value drop or tutorial for getting started.
- Step 3 (Day 7): Usage check-in to identify setup roadblocks or missing capabilities.
- Step 4 (Day 14): Deep-dive feature spotlight or workflow power tip.
- Step 5 (Day 30): Personal founder check-in requesting feedback or offering 1-on-1 assistance.

### Slot Substitution Engine

Heartbeat templates automatically populate dynamic slots from your profile and enrollment records:

> **Idempotent Enrollments**: Enrolling the same email address twice is strictly idempotent. Duplicate enrollments are ignored at the database boundary.

```markdown
Hi {{displayName}},

Thanks for checking out {{productName}}.

The fastest way to get value is to {{actionName}}.
If anything blocks you, reply directly to this email or ping {{supportEmail}}.

Best,
The Founder
```

## Broadcasts & Safety Interlock

*Mass dispatch safety mechanics and the typed-count confirmation interlock.*

Accidental blast emails are catastrophic. mailbazooka prevents misfires using physical-safety-inspired software interlocks on every broadcast draft.

### The Typed-Count Safety Interlock

Before any broadcast can be saved or transitioned to the armed state, you must manually type the exact count of pasted recipients into the safety interlock input.

> **Count Mismatch Refusal**: If you paste 42 recipient addresses but type '40', the API immediately throws CountMismatch and refuses to insert or arm the broadcast. Automation cannot bypass this check without explicitly stating the recipient total.

### Scheduling & Timezones

Broadcasts can be scheduled for one-time dispatch or recurring cadences (daily, weekly, monthly). All dispatch timestamps are normalized to UTC in the database, with timezone offsets respected during execution.

```json
POST /api/schedules
Content-Type: application/json

{
  "name": "Q4 Release Notes",
  "subject": "mailbazooka v1.0 is live",
  "body": "Here is what we shipped this month...",
  "timeZone": "America/New_York",
  "repeat": "once",
  "nextSendAt": "2026-11-01T14:00:00Z",
  "emails": ["user1@acme.com", "user2@acme.com"],
  "confirmCount": 2
}
```

## Gmail Connect & Quotas

*Direct Gmail API integration, OAuth token encryption, and daily stop limits.*

Unlike third-party SMTP relays that trigger spam filters, mailbazooka sends through your personal or Google Workspace Gmail address via the official Gmail REST API.

### OAuth Scopes & Token Lifecycle

We request only the necessary scopes for sending mail and identifying the account:

- openid email profile: Identifies the founder session.
- https://www.googleapis.com/auth/gmail.send: Authorizes sending RFC 2822 MIME formatted messages.
- Token Key Encryption: Refresh tokens are encrypted with AES-256-GCM before entering the PostgreSQL database.

### Daily Stop Limits by Tier

To protect your sender's reputation, the worker stops each sender for the day at a limit set by your tier and sender type. Every sender (Gmail/Workspace, Grok Bot inbox, SMTP, SES, Brevo, Resend) is free on every tier:

- Free: 100 sends per day per sender (50 on a shared Grok Bot domain); 50 subscribers; 50 recipients per schedule; 1 armed schedule.
- Premium ($5/mo or $48/yr): 450 per day on a personal Gmail, 1,800 on Workspace, SMTP or an API sender (50 on a shared Grok Bot domain); no schedule size limit.
- New senders warm up over about two weeks (20 → 50 → 100 → 200 per day, then the full stop).

## System Architecture

*Monorepo topology, long-running daemons, and PostgreSQL persistence.*

The recurring platform is structured as an npm/bun monorepo designed for high throughput, sub-second API latencies, and persistent background task execution.

### Monorepo Topology

Four specialized packages work in unison:

- apps/api: Pure Node/Bun HTTP server (node:http) with zero-framework overhead and background worker loops.
- apps/web: React 19 + Vite Progressive Web App (PWA) with sub-second production builds.
- apps/mcp: Model Context Protocol (MCP) server exposing tools to AI assistants via stdio.
- apps/cli: the mailbazooka CLI, public at github.com/0xm0w/mailbazooka-cli. Run it with npx github:0xm0w/mailbazooka-cli connect <key> (requires Node 22+ / npm 10+).

### The Worker Send Loop

The worker process runs a continuous non-blocking poll loop querying due send jobs in PostgreSQL. When jobs are due, it classifies errors (e.g. invalid_grant, daily_cap_reached) and dispatches RFC 2822 MIME payloads via Gmail.

```typescript
// apps/api/src/worker.ts
export function startWorker(sql: postgres.Sql, gmailSend?: GmailSendFn) {
  const interval = setInterval(async () => {
    try {
      await runOnce(sql, { gmailSend, now: new Date() });
    } catch (err) {
      console.error("Worker tick error:", err);
    }
  }, 10_000);
  return () => clearInterval(interval);
}
```

## Tech Stack & Validation

*React 19, Vite, Bun, Node 24, Zod schema validation, and strict ESLint.*

Every layer of the repository enforces strict type safety and schema validation at runtime and build time.

### Zod Schema Validation

All external HTTP inputs and environment variables are strictly parsed with Zod schemas in apps/api/src/schemas.ts before touching business logic or the database:

```typescript
import { z } from "zod";

export const SaveDraftSchema = z.object({
  name: z.string().optional().default(""),
  subject: z.string().optional().default(""),
  body: z.string().optional().default(""),
  timeZone: z.string().optional().default("UTC"),
  repeat: z.enum(["once", "daily", "weekly", "monthly"]).default("once"),
  nextSendAt: z.string().optional().default(""),
  emails: z.array(z.string()).optional().default([]),
  confirmCount: z.coerce.number().optional().default(0),
});
```

### Strict Linting Standard

The repository utilizes ESLint 9 Flat Config with typescript-eslint strict mode, forbidding untyped any, enforcing consistent type imports, and validating React 19 hook purity.

## API Reference

*HTTP endpoints for browser sessions, API keys, and public webhooks.*

The API accepts session cookie authentication (sid) from browser dashboards, or Authorization: Bearer <API_KEY> headers from agent scripts.

### Core Endpoints

Primary HTTP endpoints exposed by apps/api/src/server.ts:

- GET /health: Health check, returns 200 { ok: true } with zero send side-effects.
- GET /api/account: Current account profile, tier status, active daily counts, and limits.
- POST /api/tools: Universal JSON RPC tool dispatcher for AI agents.
- GET /api/keys: Lists all API keys associated with the account.
- POST /api/keys: Creates a new API key (initially with read and draft scopes).
- PATCH /api/keys/:id: Toggles physical safety switches (arm and enroll).
- POST /api/schedules: Creates or updates a broadcast draft.
- POST /api/schedules/:id/arm: Transitions an armed schedule to active status.
- POST /api/subscribe/:slug: Public enrollment webhook for waitlists and landing forms.
- POST /api/events: Ingests user signup/auth events to trigger immediate welcome sequences.
- POST /api/webhooks/:provider/:slug: Native inbound webhooks for Clerk, Supabase, and Stripe. Rejects deliveries until WEBHOOK_SECRET_<PROVIDER> is configured.

### Automated Sign-In Welcome via @mailbazooka/sdk

Trigger immediate welcome emails on user sign-up or first login:

```typescript
import { Mailbazooka } from "@mailbazooka/sdk";
const mb = new Mailbazooka(process.env.MAILBAZOOKA_API_KEY!);

// Inside your sign-in / registration route or NextAuth callback:
await mb.track("user.signup", {
  email: user.email,
  displayName: user.name,
});
```

### Inbound Auth Webhooks (Clerk & Supabase)

Connect your managed auth provider with zero application code by pointing webhooks to Mailbazooka:

```bash
# Clerk Webhook Endpoint:
POST https://justfuckinguseburnaddress.com/api/webhooks/clerk/<slug>
# Subscribed Event: user.created

# Supabase Webhook Endpoint:
POST https://justfuckinguseburnaddress.com/api/webhooks/supabase/<slug>
# Table: auth.users, Event: INSERT
```

### Public Subscribe Webhook Example

Embed public newsletter signups into any website or landing page:

```bash
curl -X POST https://justfuckinguseburnaddress.com/api/subscribe/waitlist \
  -H "Content-Type: application/json" \
  -d '{"email": "founder@acme.com", "name": "Alex"}'
```

## Integrations

*Grok Bot 4.7, Generic SMTP Relay, Remote MCP Gateway, and Heartbeat Cadence setup.*

Choose how your agent controls Mailbazooka and how your own sender delivers email. These integrations use the same workspace safety checks and sending limits.

### Grok Bot 4.7

Connect your Grok Bot through AI Agent Setup in the cockpit. The wizard provides an MCP configuration for managing schedules, subscriber enrollment, and lifecycle pacing. Keep your API key private and complete any owner-only steps shown by the readiness checks.

### Generic SMTP Relay

Bring your own SMTP sender through Settings or the wizard's Dispatch Pipe step. Supply your provider's host, port, username, password, from address, and TLS setting. Mailbazooka verifies the connection before saving it, encrypts the password, and enforces one sender per account. Agents can also use connect_sender; see AI & MCP Integration for the tool details.

### Remote MCP Gateway

Remote agents can call Mailbazooka tools without running a local stdio process. Use the MCP configuration from AI Agent Setup, with the /mcp (or /api/mcp) endpoint on your Mailbazooka host and your workspace API key. See AI & MCP Integration for available tools and safety checks.

### Heartbeat Cadence

The default lifecycle sequence sends on Day 0, 3, 7, 14, and 30. Configure the content, connect your sender, test delivery, and arm the sequence before enrolling subscribers. See Heartbeat Sequences for the full workflow.

## AI & MCP Integration

*Model Context Protocol server for Claude Desktop, Cursor, and Antigravity.*

apps/mcp exposes the full mailbazooka control plane to autonomous AI agents through the Model Context Protocol (MCP) using standard I/O (stdio). Remote agents connect via the HTTP gateway at /mcp or /api/mcp.

### Configuring Claude Desktop or Cursor

Add mailbazooka to your MCP client configuration file (e.g. ~/Library/Application Support/Claude/claude_desktop_config.json):

```json
{
  "mcpServers": {
    "mailbazooka": {
      "command": "bun",
      "args": ["run", "apps/mcp/src/server.ts"],
      "env": {
        "RECURRING_API_KEY": "rc_live_your_api_key_here",
        "API_ORIGIN": "http://localhost:3000"
      }
    }
  }
}
```

### Exposed Agent Tools

The MCP server exposes these agent tools (among others), each with strict input validation:

- track_event: Ingests an event (e.g. user.signup, auth.signin) to automatically trigger immediate welcome sequences.
- list_events: Real-time event activity stream received via webhooks, SDK, or agent tool calls.
- list_triggers: Inspects configured event-to-sequence automation triggers.
- create_trigger: Maps an event name (e.g. user.signup) to an email sequence with optional delay seconds.
- install_heartbeat: Sets up the 5-step customer retention sequence draft.
- enroll_customer: Enrolls one subscriber with email and display name into the armed sequence.
- suppress_step: Skips one future sequence step for an active customer.
- create_schedule: Creates a one-off plain text broadcast draft.
- update_schedule: Edits a draft broadcast. An armed schedule is refused (409 schedule_not_draft); pause it first.
- arm_schedule: Arms a draft broadcast or sequence for automated delivery.
- pause_schedule: Pauses an active sequence or broadcast and removes pending jobs.
- list_schedules: Inspects active and draft broadcasts and sequences.
- get_schedule: Retrieves schedule metadata, body, and recipient list.
- list_sends: Inspects dispatch history, execution status, and provider message IDs.
- connect_sender: Connects this account's own SES, Brevo, Resend or SMTP sender (one per account; a second is refused 409). SMTP takes host, port, username, password, from_address and optional tls, is verified with a real login before saving (a failed verify stores nothing), and the password is encrypted; responses show only a masked hint. CLI: npx github:0xm0w/mailbazooka-cli sender connect --provider smtp --host <h> --port <n> --user <u> --from <addr> [--tls], password via --password or MAILBAZOOKA_SMTP_PASSWORD.
- set_sender_daily_stop: Lowers this account's sender daily stop (1-1,800); the enforced stop is the lowest of it, the plan, warm-up and the shared-domain cap.

### Agent Prompts & Workflows

Mailbazooka exposes specialized MCP prompts that lead agents through safe lifecycle automation:

- event_welcome_automation: Guides the AI agent through inspecting active sequences, configuring triggers, and surfacing webhook endpoints for sign-ins.
- customer_heartbeat: Leads the agent through install_heartbeat and stops before any arming.

## Security & Safety

*Dual-switch authorization, AES-256-GCM encryption, and token hashing.*

Security is implemented at the core architecture level with strict defense-in-depth principles.

### AES-256-GCM Token Encryption

OAuth refresh tokens from Google are never saved in plaintext. Each token is encrypted using AES-256-GCM with a unique 12-byte initialization vector (IV) and verified using a 16-byte authentication tag before writing to PostgreSQL.

```typescript
// apps/api/src/crypto.ts
export function encryptToken(token: string, key: Buffer): string {
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const enc = Buffer.concat([cipher.update(token, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return Buffer.concat([iv, tag, enc]).toString("base64");
}
```

### The Dual-Switch Authorization Model

API keys are segregated into scopes ('read', 'draft', 'arm', 'enroll') combined with two explicit toggle switches:

- ARM Switch: Even if a key has the 'arm' scope, arm_schedule fails if the arm_switch is toggled off in the dashboard.
- ENROLL Switch: Lets a key enroll subscribers and track real events. Available on every plan; the free plan caps subscribers at 50. ARM is premium-only.
- Sandbox: track_event and POST /api/events accept sandbox: true with any key that has read access. Nothing is enrolled and no email is sent. Limit: 30 sandbox calls per hour per account.
- Bearer keys work on POST /api/events (Authorization: Bearer <key>). A real event needs the ENROLL switch on; a refusal returns 403 with the missing scope.
- Hashed at Rest: API keys are prefix-masked (rc_live_...) and stored as SHA-256 hashes. Full key credentials can only be read once upon creation.
