Gmail-first sync infrastructure for correct mailbox state, durable events, and replayable webhooks.
A reliable event system on top of Gmail.
Problem
·
Quickstart
·
GitHub
Most Gmail integrations fail in production because:
- Duplicate push notifications cause replayed state
- Missed changes from bad cursor handling
- Worker crashes between fetch and commit
- Invalid or expired Gmail history cursors
- Revoked OAuth tokens
- Webhook endpoints going down
- No replay path for historical changes
Mailmon treats email sync as a distributed systems problem, not a fetch loop.
The key question is not "can we read email?" — it's:
Can we maintain correct mailbox state over time under retries, failures, and scale?
Mailmon is alpha stage software. The core email sync workloads are production-ready. It implements:
- Gmail OAuth, credential encryption, and secure token storage
- Initial and incremental sync with durable history cursors
- Canonical message/thread state with lease-based concurrency control
- Durable event logs with at-least-once webhook delivery
- Local development mode and GCP cloud runtime
- TypeScript SDK for programmatic access
- CLI tools for operators: sync, credential audit, event replay, and control jobs
-
Mailbox is the unit of work. Each mailbox owns its cursor, operational state, and active sync lease. No account-scoped state.
-
Push is a wake-up, not truth. Gmail push notifications trigger work. Gmail history remains the source of truth.
-
State first, cursor second. The cursor advances only after mailbox state and events are durably committed.
-
Correctness over speed. Durable event logs, transactional state commits, and mailbox leases enforce one sync per mailbox—queue ordering is not trusted.
| Area | Current state |
|---|---|
| Provider | Gmail |
| Sync model | Canonical state + durable event log with at-least-once webhooks |
| API | HTTP with workspace-scoped API keys |
| SDK | TypeScript |
| Persistence | PostgreSQL (Drizzle ORM) |
| Async transport | Pub/Sub (sync dispatch) + Cloud Tasks (webhook delivery) |
| Local dev | Full feature parity via local adapters, no emulators required |
- Node.js 22+
- pnpm 10.32.1
- Docker
- Docker Compose
- Gmail OAuth credentials, if testing real Gmail connectivity
pnpm installCreate a .env file:
NODE_ENV=development
DATABASE_URL=postgres://mailmon:mailmon@127.0.0.1:5432/mailmon
MAILMON_ASYNC_TRANSPORT_MODE=local
MAILMON_WORKER_BASE_URL=http://127.0.0.1:3001
# Generate your own 32-byte base64 key.
MAILMON_GMAIL_REFRESH_TOKEN_ENCRYPTION_KEY=replace_me
MAILMON_GMAIL_REFRESH_TOKEN_ENCRYPTION_KEY_ID=primary
# Required for real Gmail OAuth.
MAILMON_GMAIL_OAUTH_CLIENT_ID=replace_me
MAILMON_GMAIL_OAUTH_CLIENT_SECRET=replace_meGenerate a local encryption key:
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"Important
The encryption key must be exactly 32 bytes base64-encoded. Generate with:
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"pnpm docker:up
pnpm db:migrate
pnpm devDefault local URLs:
- API:
http://127.0.0.1:3000 - Worker:
http://127.0.0.1:3001
Tip
Local mode does not require local Pub/Sub, Cloud Tasks, or Gmail watch infrastructure. It uses local adapters so the core sync and webhook flows can be developed without cloud emulators.
Create a workspace:
pnpm --filter @mailmon/cli dev -- admin workspace createCreate an API key:
pnpm --filter @mailmon/cli dev -- admin keys create --workspace-id <workspace-id>Run a mailbox sync:
pnpm --filter @mailmon/cli dev -- sync-mailbox <mailbox-id>Run a control job:
pnpm --filter @mailmon/cli dev -- control-job recover_stuck_syncs
pnpm --filter @mailmon/cli dev -- control-job recover_webhook_deliveriesAudit or rewrap persisted Gmail credential envelopes:
pnpm --filter @mailmon/cli dev -- gmail-credentials audit
pnpm --filter @mailmon/cli dev -- gmail-credentials rewrapForward webhook deliveries to a local app:
pnpm --filter @mailmon/cli dev -- listen \
--forward-to http://localhost:4000/webhooks/mailmonReplay stored events into a local endpoint:
pnpm --filter @mailmon/cli dev -- replay \
--mailbox <mailbox-id> \
--last 1h \
--forward-to http://localhost:4000/webhooks/mailmonBuilt on Effect service interfaces (transport-neutral), PostgreSQL persistence, and GCP-native async transport: Pub/Sub for mailbox sync dispatch and Cloud Tasks for webhook delivery.
Key guarantee: One sync per mailbox. Sync runs hold a database-backed lease. State and events commit atomically with cursor advancement.
flowchart TD
App[Developer Application] -->|API key| API[Mailmon API]
API --> DB[(PostgreSQL)]
API -->|create connect session| GmailOAuth[Gmail OAuth]
GmailOAuth --> API
GmailPush[Gmail Push] --> GmailPubSub[Pub/Sub]
GmailPubSub --> Worker[Mailmon Worker]
API -->|sync request| SyncPubSub[Pub/Sub Sync Dispatch]
SyncPubSub --> Worker
Worker -->|lease + state + cursor| DB
Worker -->|fetch history/messages| GmailAPI[Gmail API]
GmailAPI --> Worker
Worker -->|durable events| DB
Worker --> Tasks[Cloud Tasks / Local Scheduler]
Tasks --> Webhook[Customer Webhook Endpoint]
Scheduler[Cloud Scheduler] -->|control jobs| Worker
.
├── apps/
│ ├── api/ # Public HTTP API
│ ├── worker/ # Sync, Gmail push, webhook delivery, control jobs
│ ├── cli/ # Local dev and operator commands
│ └── docs/ # Mintlify docs app
│
├── packages/
│ ├── core/ # Contracts, use cases, service interfaces
│ ├── db/ # Drizzle schema, persistence adapters, migrations
│ ├── gmail/ # Gmail OAuth, sync provider, watch provider, token crypto
│ ├── queue/ # Local dispatch, Pub/Sub dispatch, Cloud Tasks scheduling
│ └── config/ # Runtime configuration
│
├── sdks/ # Programmatically generated client SDKs
│ └── typescript/ # TypeScript SDK for the Mailmon API
│
├── infra/ # Terraform-managed GCP infrastructure
├── plans/ # Architecture and implementation plans
└── docker-compose.yml
The Mailmon API can be accessed via HTTP requests or using the official TypeScript SDK.
npm install @mailmon.dev/sdk
# or pnpm, yarnAll public API examples require a workspace API key.
Using curl:
export MAILMON_API_KEY=mm_test_...Using the TypeScript SDK:
import { MailmonClient } from "@mailmon.dev/sdk";
const client = new MailmonClient({
token: "mm_test_...",
// environment: "http://127.0.0.1:3000", // For local development
});Tip
Webhook events include an id field. Consumers must deduplicate by event ID—delivery is at-least-once, not exactly-once.
Using curl:
curl -X POST http://127.0.0.1:3000/v1/mailboxes/connect-sessions \
-H "authorization: Bearer $MAILMON_API_KEY" \
-H "content-type: application/json" \
-d '{
"provider": "gmail",
"tenantExternalId": "tenant_demo",
"mailboxExternalId": "primary",
"redirectUrl": "http://localhost:3000/connected"
}'Using the TypeScript SDK:
const response = await client.postV1MailboxesConnectSessions({
provider: "gmail",
tenantExternalId: "tenant_demo",
mailboxExternalId: "primary",
redirectUrl: "http://localhost:3000/connected",
});Example response:
{
"id": "mcs_...",
"object": "connect_session",
"connectUrl": "http://127.0.0.1:3000/oauth/gmail/mcs_...",
"expiresAt": "2026-05-04T10:15:00.000Z"
}Send the user to connectUrl. After OAuth succeeds, Mailmon creates the mailbox and dispatches initial sync.
Using curl:
curl http://127.0.0.1:3000/v1/mailboxes/mbx_... \
-H "authorization: Bearer $MAILMON_API_KEY"Using the TypeScript SDK:
const response = await client.getV1MailboxesByMailboxId({
mailboxId: "mbx_...",
});Example response:
{
"id": "mbx_...",
"object": "mailbox",
"provider": "gmail",
"emailAddress": "user@example.com",
"status": "active",
"syncState": "healthy",
"watchState": "active",
"initializedAt": "2026-05-04T10:00:00.000Z",
"lastSuccessfulSyncAt": "2026-05-04T10:01:00.000Z",
"lastError": null
}curl "http://127.0.0.1:3000/v1/messages?mailboxId=mbx_...&limit=50" \
-H "authorization: Bearer $MAILMON_API_KEY"
curl "http://127.0.0.1:3000/v1/threads?mailboxId=mbx_...&limit=50" \
-H "authorization: Bearer $MAILMON_API_KEY"
curl "http://127.0.0.1:3000/v1/mailboxes/mbx_.../sync-runs" \
-H "authorization: Bearer $MAILMON_API_KEY"
curl "http://127.0.0.1:3000/v1/mailboxes/mbx_.../observability" \
-H "authorization: Bearer $MAILMON_API_KEY"Using curl:
# Create endpoint
curl -X POST http://127.0.0.1:3000/v1/webhook-endpoints \
-H "authorization: Bearer $MAILMON_API_KEY" \
-H "content-type: application/json" \
-d '{"url": "https://example.com/webhooks/mailmon"}'
# Subscribe to events
curl -X POST http://127.0.0.1:3000/v1/webhook-endpoints/whe_.../subscriptions \
-H "authorization: Bearer $MAILMON_API_KEY" \
-H "content-type: application/json" \
-d '{"mailboxIds": ["mbx_..."], "eventTypes": ["message.created"]}'Using the TypeScript SDK:
const endpoint = await client.postV1WebhookEndpoints({
url: "https://example.com/webhooks/mailmon",
});
await client.postV1WebhookEndpointsByEndpointIdSubscriptions({
endpointId: endpoint.id,
mailboxIds: ["mbx_..."],
eventTypes: ["message.created"],
});Using curl:
# Create replay job
curl -X POST http://127.0.0.1:3000/v1/replays \
-H "authorization: Bearer $MAILMON_API_KEY" \
-H "content-type: application/json" \
-d '{
"mailboxId": "mbx_...",
"webhookEndpointId": "whe_...",
"startTime": "2026-05-04T09:00:00.000Z",
"endTime": "2026-05-04T10:00:00.000Z"
}'Using the TypeScript SDK:
await client.postV1Replays({
mailboxId: "mbx_...",
webhookEndpointId: "whe_...",
startTime: "2026-05-04T09:00:00.000Z",
endTime: "2026-05-04T10:00:00.000Z",
});Mailmon emits:
message.createdmessage.updatedthread.updated
Example event:
{
"id": "evt_...",
"type": "message.created",
"schemaVersion": 1,
"occurredAt": "2026-05-04T10:00:00.000Z",
"workspaceId": "wsp_...",
"tenantExternalId": "tenant_demo",
"mailboxId": "mbx_...",
"data": {
"messageId": "msg_...",
"threadId": "thr_...",
"providerMessageId": "195f8c...",
"providerThreadId": "195f8b...",
"subject": "Hello",
"snippet": "Email snippet...",
"receivedAt": "2026-05-04T09:59:00.000Z",
"labelIds": ["INBOX", "UNREAD"]
}
}Webhook requests include:
x-mailmon-delivery-id
x-mailmon-event-id
x-mailmon-attempt
x-mailmon-signature
Signature format:
t=<timestamp>,v1=<hex_hmac>
Signed payload:
<timestamp>.<raw_body>
Important
Webhook delivery is at-least-once. Consumers must deduplicate by event.id.
Used for development.
- API dispatches sync to the local worker
- webhook delivery can run through local scheduler/CLI
- internal worker routes do not require OIDC auth
- no local Pub/Sub or Cloud Tasks required
MAILMON_ASYNC_TRANSPORT_MODE=localUsed for staging/production.
- API publishes sync requests to Pub/Sub
- Pub/Sub pushes sync jobs to the worker
- Gmail push arrives through Pub/Sub
- Cloud Tasks schedules webhook delivery
- Cloud Scheduler runs control jobs
- worker internal routes verify Google OIDC tokens
MAILMON_ASYNC_TRANSPORT_MODE=gcpNote
Mailmon is GCP-native. We optimize for Pub/Sub, Cloud Tasks, and Cloud SQL. We do not target cloud-agnostic or Kubernetes-generic deployments.
GCP deployments commonly set:
GCP_PROJECT_ID=...
GCP_REGION=...
MAILMON_WORKER_BASE_URL=...
MAILMON_GMAIL_PUBSUB_TOPIC_NAME=...
MAILMON_SYNC_DISPATCH_PUBSUB_TOPIC_NAME=...
MAILMON_GCP_TASKS_SERVICE_ACCOUNT_EMAIL=...
MAILMON_GCP_SCHEDULER_SERVICE_ACCOUNT_EMAIL=...
MAILMON_GCP_WEBHOOK_DELIVERY_QUEUE_ID=mailmon-webhook-deliveries
MAILMON_GCP_TASKS_AUDIENCE=...The infra/ directory contains Terraform for the GCP topology:
flowchart LR
API[Cloud Run API] --> SQL[(Cloud SQL PostgreSQL)]
Worker[Cloud Run Worker] --> SQL
GmailPush[Pub/Sub Gmail Push] --> Worker
SyncTopic[Pub/Sub Sync Dispatch] --> Worker
DLQ[Pub/Sub Dead Letter] --> Worker
Tasks[Cloud Tasks] --> Worker
Scheduler[Cloud Scheduler] --> Worker
Secrets[Secret Manager + KMS] --> API
Secrets --> Worker
Staging/production resources include:
- Cloud Run API
- Cloud Run worker
- Cloud SQL PostgreSQL
- Pub/Sub topic for Gmail push
- Pub/Sub topic for mailbox sync dispatch
- Pub/Sub dead-letter topic for exhausted sync dispatch
- Cloud Tasks queue for webhook delivery
- Cloud Scheduler jobs for watch renewal and stuck sync recovery
- Secret Manager and KMS for secrets
- Cloud Logging metrics and alert policies
pnpm install # Install dependencies
pnpm docker:up # Start local containers
pnpm dev # Run API and worker
pnpm build # Build all packages
pnpm test # Run tests
pnpm lint # Lint and formatFor migrations, watch modes, and full reference, see Development Guide.
Near-term work:
-
Broader end-to-end coverage for cloud transport and failure recovery
- cross-service GCP mode flows
- Pub/Sub and Cloud Tasks retry behavior
- control-job behavior against production-like runtimes
-
Operator lifecycle
- API key labels
last_used_at- key rotation UX
- audit logs
-
Public docs
- API reference
- webhook verification guide
- deployment guide
- sync guarantees document