Self-Hosting
Deploy your own mails-agent instance on Cloudflare Workers. Free tier, full control, ~10 minutes.
Prerequisites
| Requirement | Notes |
|---|---|
| Cloudflare account | Free plan works. You need Workers, D1, and R2. |
| Domain name | Added to Cloudflare DNS (for receiving email via Email Routing). |
| Resend account | For sending email. Free tier: 100 emails/day, 3,000/month. Sign up at resend.com. |
| Node.js 18+ | For running Wrangler CLI. |
| Wrangler CLI | npm install -g wrangler |
Step 1: Clone the repository
git clone https://github.com/Digidai/mails.git
cd mails
Step 2: Create D1 database + R2 bucket
# Create the D1 database
wrangler d1 create mails-agent-db
# Output:
# Created D1 database mails-agent-db
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Create the R2 bucket (for attachments)
wrangler r2 bucket create mails-agent-attachments
Save the database_id from the output -- you will need it in the next step.
Step 3: Configure wrangler.toml
Open wrangler.toml and fill in your resource IDs:
name = "mails-agent"
main = "src/index.ts"
compatibility_date = "2024-12-01"
[[d1_databases]]
binding = "DB"
database_name = "mails-agent-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # from step 2
[[r2_buckets]]
binding = "ATTACHMENTS"
bucket_name = "mails-agent-attachments"
Step 4: Set secrets
# Resend API key (for sending emails)
wrangler secret put RESEND_API_KEY
# Paste your Resend API key when prompted
# Auth token (clients use this to authenticate)
wrangler secret put AUTH_TOKEN
# Choose a strong random token, e.g.:
# openssl rand -hex 24
# Resend webhook secret (for delivery status tracking)
wrangler secret put RESEND_WEBHOOK_SECRET
# Get this from Resend dashboard → Webhooks → Signing Secret
# Required: without this, delivery status updates are rejected
These secrets are encrypted and stored securely by Cloudflare. They are never visible in your code or dashboard.
Step 5: Run schema migration
# Apply the database schema
wrangler d1 execute mails-agent-db --file=./schema.sql
This creates the tables for mailboxes, emails, threads, webhooks, and other data.
Step 6: Deploy the Worker
wrangler deploy
After deployment, Wrangler prints your Worker URL:
# Output:
# Published mails-agent (1.2s)
# https://mails-agent.YOUR-SUBDOMAIN.workers.dev
Step 7: Configure Cloudflare Email Routing
This is the key step that makes your domain receive email and forward it to the Worker.
- Go to the Cloudflare dashboard.
- Select your domain.
- Navigate to Email > Email Routing > Routes.
- Click Catch-all address and set the action to Send to a Worker.
- Select your
mails-agentWorker. - Save.
Cloudflare automatically configures the required MX records for your domain:
| Type | Name | Value | Priority |
|---|---|---|---|
| MX | @ | route1.mx.cloudflare.net | 69 |
| MX | @ | route2.mx.cloudflare.net | 30 |
| MX | @ | route3.mx.cloudflare.net | 34 |
If your domain already has MX records (e.g., from Google Workspace), you can use a subdomain instead. For example, set up Email Routing for mail.yourdomain.com and your agents will get addresses like [email protected].
Step 8: Test it
# Check that the Worker is running
curl https://mails-agent.YOUR-SUBDOMAIN.workers.dev/v1/me \
-H "Authorization: Bearer YOUR_AUTH_TOKEN"
# Expected response:
# {"worker": "mails-worker", "mailbox": "...", "send": true}
# Send a test email
curl -X POST https://mails-agent.YOUR-SUBDOMAIN.workers.dev/v1/send \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "Hello from self-hosted mails-agent!",
"text": "If you receive this, everything is working."
}'
Then send an email to any address at your domain (e.g., [email protected]) and check it arrived:
curl https://mails-agent.YOUR-SUBDOMAIN.workers.dev/v1/inbox \
-H "Authorization: Bearer YOUR_AUTH_TOKEN"
DNS setup for custom domain (optional)
If you want a clean URL like api.yourdomain.com instead of *.workers.dev:
- Go to your Worker's settings in the Cloudflare dashboard.
- Under Triggers > Custom Domains, add
api.yourdomain.com. - Cloudflare automatically creates the DNS record.
Now your API base URL is https://api.yourdomain.com. Update your CLI config or SDK client accordingly:
# CLI
mails config set worker_url https://api.yourdomain.com
# Python SDK
from mails_agent import MailsClient
client = MailsClient(api_key="...", base_url="https://api.yourdomain.com")
# curl
curl https://api.yourdomain.com/v1/inbox \
-H "Authorization: Bearer YOUR_AUTH_TOKEN"
Sending email: Resend DNS verification
For Resend to send email from your domain, you need to verify it:
- Go to Resend dashboard > Domains.
- Add your domain.
- Add the DNS records Resend provides (SPF, DKIM, DMARC).
- Wait for verification (usually a few minutes).
Once verified, your agent's outbound emails will pass SPF/DKIM checks and land in recipients' inboxes instead of spam.
Upgrading
When a new version of mails-agent is released:
cd mails
git pull
wrangler deploy
If the release includes schema changes, run the migration first:
git pull
wrangler d1 execute mails-agent-db --file=./migrations/XXXX.sql
wrangler deploy
Check the release notes for migration instructions.
Architecture overview
| Component | Service | Purpose |
|---|---|---|
| Worker | Cloudflare Workers | API server, email processing |
| Database | Cloudflare D1 (SQLite) | Emails, mailboxes, threads, webhooks |
| Attachments | Cloudflare R2 | Email attachment storage |
| Email receiving | Cloudflare Email Routing | MX records, forwards to Worker |
| Email sending | Resend | Outbound SMTP via API |
Everything runs on Cloudflare's free tier except Resend (free for 100 emails/day, 3,000/month). There are no servers to maintain, no containers to manage, and no cold starts.
Troubleshooting
Emails are not arriving
- Check that Email Routing is enabled and the catch-all route points to your Worker.
- Verify MX records are set correctly:
dig MX yourdomain.com - Check Worker logs:
wrangler tail
Sending fails
- Verify the
RESEND_API_KEYsecret is set:wrangler secret list - Check that your domain is verified in Resend.
- Look at the delivery status:
GET /v1/email?id=msg_xxx
Auth errors
- Make sure you are passing the correct
AUTH_TOKENin theAuthorization: Bearerheader. - Re-set the secret if unsure:
wrangler secret put AUTH_TOKEN