# How MakiSend runs on Cloudflare

One Worker, one database, one queue and a durable workflow. Nothing to keep running, all of it on your own Cloudflare account.

## Follow one email

What happens to one email, for three outcomes at your SMTP provider.

### Delivered

1. Your app calls POST /send-email with its API key.
2. The Worker checks the key, validates the JSON and encrypts the recipient and content.
3. D1 stores a pending log and the encrypted email. Your app already has its 202.
4. The queue carries only the email's id. No personal data travels through it.
5. A Workflow starts for this email, decrypts it and cleans the HTML.
6. nodemailer opens a TLS connection to your SMTP provider and hands the message over.
7. Delivered. The dashboard shows the email as sent, with each step.

### Retried

1. Your app calls POST /send-email with its API key.
2. The Worker checks the key, validates the JSON and encrypts the recipient and content.
3. D1 stores a pending log and the encrypted email. Your app already has its 202.
4. The queue carries only the email's id. No personal data travels through it.
5. A Workflow starts for this email, decrypts it and cleans the HTML.
6. The provider answers 421: busy, try again later. A temporary error.
7. The Workflow sleeps 30 seconds, durably, then tries again: attempt 2 of 4.
8. Second attempt accepted.
9. Delivered on the retry. The dashboard shows both attempts.

### Rejected

1. Your app calls POST /send-email with its API key.
2. The Worker checks the key, validates the JSON and encrypts the recipient and content.
3. D1 stores a pending log and the encrypted email. Your app already has its 202.
4. The queue carries only the email's id. No personal data travels through it.
5. A Workflow starts for this email, decrypts it and cleans the HTML.
6. The provider answers 535: authentication failed. A permanent error.
7. No retry for a permanent error: the email is marked failed and goes to the dead-letter queue. The reason is in your logs.

## The Cloudflare products behind it

### [Workers](https://developers.cloudflare.com/workers/)

One Worker runs everything: the Hono API, sign-in, the dashboard and the delivery code. nodejs_compat lets nodemailer and its SMTP client run unchanged. No server, no container.

### [Static Assets](https://developers.cloudflare.com/workers/static-assets/)

Serves the dashboard (an Astro build) from the same Worker, on the same origin as the API. Same origin means no CORS and one URL to remember.

### [D1](https://developers.cloudflare.com/d1/)

The database: email logs and history, encrypted contents and attachments, SMTP configurations, API keys, templates, accounts. Recipients, bodies, attachments and SMTP credentials are AES-256-GCM encrypted before they are written. Schema managed by Drizzle migrations.

### [Queues](https://developers.cloudflare.com/queues/)

Decouples the API from delivery: the request ends as soon as the email id is queued. Batches of up to 10, 3 consumer retries 30 seconds apart, then a dead-letter queue (makisend-send-email-dlq).

### [Workflows](https://developers.cloudflare.com/workflows/)

One durable run per email. Its id is the email id, so the same email can never be sent twice by two runs. 4 SMTP attempts, 30 seconds apart, that survive restarts. Permanent errors (auth, TLS, DNS, 5xx) stop at once. Every step is written to the email's history.

### [TCP sockets](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/)

The Worker opens the SMTP connection to your provider itself, over TLS. Ports 465, 587, 2465, 2525 and 2587. Port 25 is blocked by Cloudflare. Connections leave from shared Cloudflare IPs.

### [Email Service](https://developers.cloudflare.com/email-service/)

An alternative to SMTP: configurations on smtp.mx.cloudflare.net send through the Worker's send_email binding. No SMTP password needed; the sender address must use a domain onboarded to Cloudflare Email Sending.

### [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/)

Background jobs with no extra service: health checks, the update check and key rotation. Daily at 03:17 UTC and hourly at minute 17 (see below).

### [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/)

Request and invocation logs from the Worker, in your Cloudflare dashboard. Turned on in wrangler.jsonc. Email contents are never logged.

## What runs on a schedule

- **Every day, 03:17 UTC**: Tests every SMTP configuration and records the result, then checks GitHub for a new MakiSend release (unless UPDATE_CHECK is off).
- **Every hour, minute 17**: Key rotation: re-encrypts stored data still under a previous ENCRYPTION_KEY, in small batches.

## What to know before you rely on it

- No fixed outbound IP: a provider that requires IP allow-listing will refuse sends from a Worker.
- Sending is asynchronous: the 202 means queued, not delivered. The outcome is in the logs and the dashboard.
- One SMTP provider per configuration, no automatic failover. No bounce, open or click tracking yet.
- Deliverability depends on your sender domain (SPF, DKIM, DMARC) and your provider, not on Cloudflare.

Full design, data model and diagrams: [README](https://github.com/lixalistudio-oss/makisend#architecture) and [docs/ARCHITECTURE.md](https://github.com/lixalistudio-oss/makisend/blob/main/docs/ARCHITECTURE.md).
