
Mailflare
Custom-domain mailboxes and a shared inbox in your Cloudflare account.
Mailflare is a self-hosted Fastmail/Gmail alternative built on Cloudflare (D1, Durable Objects, Email Workers, Images, Queues). Paid services required. Inspect the source and license in the linked repository.
Source & license
Upstream license: AGPL-3.0
License TL;DR
You can use and change it, even commercially. If people use your modified version over a network, offer them its corresponding source under the AGPL. Sharing copies has source-sharing duties too. Sharing source code is different from sharing users’ content.
Explain AGPL v3 in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Paid services required
Cloudflare sending requires Workers Paid, minimum $5 USD per account/month; domain, excess usage and optional AI/Images charges are additional.
Hosting requirements
- Sending requires a Workers Paid account, minimum $5 USD/month; receiving-only use is described upstream but the release scope includes sending.
- Provide a domain in the same Cloudflare account, Email Routing/Sending setup and scoped CF_TOKEN; domain registration is a separate cost.
- Keep D1, R2, SQLite DO and Queues usage inside paid included allocations; excess requests, storage and operations are billed.
- Workers AI and Images bindings are observed, but those optional features have independent quotas and may add charges.
- Keep the Worker name mailflare and use your own resource IDs; do not import the separate Docker email relay into this app diagram.
Sources checked 01/10/2026
Repository snapshot: 8aa184d. Hosting eligibility reflects the deployment documentation and listed assumptions.
- gmail ↗
**: Connect domains and set up Cloudflare Email Routing from the dashboard. - **Mailboxes**: Create personal and shared mailboxes with delegated access. - **Email**: Send and receive email with attachments, rich formatting, signatures, and automatic replies. - **Inbox organization**: Organize mail with search, custom folders, stars, snoozing, archive, spam, and trash. - **Routing rules**: Create routing rules to store, forward, reject, or categorize incoming messages. - **Notifications**: Get real-time inbox updates and new-message notifications. - **Mail and cont
- fastmail ↗
*: Send and receive email with attachments, rich formatting, signatures, and automatic replies. - **Inbox organization**: Organize mail with search, custom folders, stars, snoozing, archive, spam, and trash. - **Routing rules**: Create routing rules to store, forward, reject, or categorize incoming messages. - **Notifications**: Get real-time inbox updates and new-message notifications. - **Mail and contacts**: Import and export mail, manage contacts, and block unwanted senders. - **Administration**: Manage accounts, permissions, API keys, webhooks, audit logs, an
- workers ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "mailflare", "main": "./worker.ts", "compatibility_date": "2026-05-20", "keep_vars": true, "compatibility_flags": [ "nodejs_compat", "global_fetch_strictly_public" ], "assets": { "binding": "ASSETS", "not_found_handling": "none", }, "images": { "binding": "IMAGES", }, "services": [ { "binding": "WORKER_SELF_REFERENCE", "service": "mailflare", }, ], "durable_objects": { "bindings": [ { "name": "REALTIME", "class_name": "RealtimeHub",
- d1 ↗
: false, }, ], "ai": { "binding": "AI", "remote": true }, "d1_databases": [ { "binding": "DB", "database_name": "mailflare", "migrations_dir": "drizzle/migrations", }, ], "r2_buckets": [ { "binding": "BUCKET", "bucket_name": "mailflare-raw", }, ], "queues": { "producers": [ { "binding": "INBOUND_QUEUE", "queue": "mailflare-inbound", }, { "binding": "OUTBOUND_QUEUE", "queue": "mailflare-outbound", }, { "binding": "AGENT_QUEUE", "queue": "mailflare-agent", }, ], "consumers": [ { "
- r2 ↗
mailflare", "migrations_dir": "drizzle/migrations", }, ], "r2_buckets": [ { "binding": "BUCKET", "bucket_name": "mailflare-raw", }, ], "queues": { "producers": [ { "binding": "INBOUND_QUEUE", "queue": "mailflare-inbound", }, { "binding": "OUTBOUND_QUEUE", "queue": "mailflare-outbound", }, { "binding": "AGENT_QUEUE", "queue": "mailflare-agent", }, ], "consumers": [ { "queue": "mailflare-inbound", "max_batch_size": 5, "max_retries": 3, }, { "queue": "mailflare-outbound",
- durable-objects ↗
RENCE", "service": "mailflare", }, ], "durable_objects": { "bindings": [ { "name": "REALTIME", "class_name": "RealtimeHub", }, ], }, "migrations": [ { "tag": "v1", "new_sqlite_classes": [ "RealtimeHub", ], }, ], "triggers": { "crons": ["0 2 * * *", "*/5 * * * *"], }, "ratelimits": [ { "name": "LOGIN_RATE_LIMIT", "namespace_id": "1001", "simple": { "limit": 20, "period": 60, }, }, { "name": "AGENT_RATE_LIMIT", "namespace_id": "1002", "simple": { "limit": 120, "period": 60 }, },
- queues ↗
ET", "bucket_name": "mailflare-raw", }, ], "queues": { "producers": [ { "binding": "INBOUND_QUEUE", "queue": "mailflare-inbound", }, { "binding": "OUTBOUND_QUEUE", "queue": "mailflare-outbound", }, { "binding": "AGENT_QUEUE", "queue": "mailflare-agent", }, ], "consumers": [ { "queue": "mailflare-inbound", "max_batch_size": 5, "max_retries": 3, }, { "queue": "mailflare-outbound", "max_batch_size": 5, "max_retries": 3, }, { "queue": "mailflare-agent", "max_batch_siz
- workers-ai ↗
vability": { "enabled": true, }, "upload_source_maps": true, "send_email": [ { "name": "EMAIL", "remote": false, }, ], "ai": { "binding": "AI", "remote": true }, "d1_databases": [ { "binding": "DB", "database_name": "mailflare", "migrations_dir": "drizzle/migrations", }, ], "r2_buckets": [ { "binding": "BUCKET", "bucket_name": "mailflare-raw", }, ], "queues": { "producers": [ { "binding": "INBOUND_QUEUE", "queue": "mailflare-inbound", }, { "binding": "OUTBOUND_QUEUE", "queue": "mailflare-outbo
- images ↗
fetch_strictly_public" ], "assets": { "binding": "ASSETS", "not_found_handling": "none", }, "images": { "binding": "IMAGES", }, "services": [ { "binding": "WORKER_SELF_REFERENCE", "service": "mailflare", }, ], "durable_objects": { "bindings": [ { "name": "REALTIME", "class_name": "RealtimeHub", }, ], }, "migrations": [ { "tag": "v1", "new_sqlite_classes": [ "RealtimeHub", ], }, ], "triggers": { "crons": ["0 2 * * *", "*/5 * * * *"], }, "ratelimits": [ { "name": "LOGIN_RATE_LIMIT", "namespac
- email-workers ↗
, "period": 60 }, }, ], "observability": { "enabled": true, }, "upload_source_maps": true, "send_email": [ { "name": "EMAIL", "remote": false, }, ], "ai": { "binding": "AI", "remote": true }, "d1_databases": [ { "binding": "DB", "database_name": "mailflare", "migrations_dir": "drizzle/migrations", }, ], "r2_buckets": [ { "binding": "BUCKET", "bucket_name": "mailflare-raw", }, ], "queues": { "producers": [ { "binding": "INBOUND_QUEUE", "queue": "mailflare-inbound", }, { "binding": "OUTBOUND_QUE
- paid ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "mailflare", "main": "./worker.ts", "compatibility_date": "2026-05-20", "keep_vars": true, "compatibility_flags": [ "nodejs_compat", "global_fetch_strictly_public" ], "assets": { "binding": "ASSETS", "not_found_handling": "none", }, "images": { "binding": "IMAGES", }, "services": [ { "binding": "WORKER_SELF_REFERENCE", "service": "mailflare", }, ], "durable_objects": { "bindings": [ { "name": "REALTIME", "class_nam
- paid ↗
ount Manager. | | Requests<sup>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 second
- paid ↗
rs Paid](https://developers.cloudflare.com/workers/platform/pricing/#workers) | | --- | --- | --- | | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows | | Storage (per GB stored) | 5 GB (total) | First 5 GB included + $0.75 / GB-mo | Track your D1 usage To accurately track your usage, use the [meta object](https://developers.cloudflare.com/d1/worker-api/return-object/), [GraphQL Analytics API](https://developers.cloudflare.com/d1/obs
- paid ↗
f you have retrieved data (for infrequent access storage) for 1.1 GB, you will be billed for 2 GB. ### Free tier You can use the following amount of storage and operations each month for free. | | Free | | --- | --- | | Storage | 10 GB-month / month | | Class A Operations | 1 million requests / month | | Class B Operations | 10 million requests / month | | Egress (data transfer to Internet) | Free <sup>[1](#user-content-fn-1)</sup> | Caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. ### Storage usage S
- paid ↗
ute and storage. Note Durable Objects are available both on Workers Free and Workers Paid plans. - **Workers Free plan**: Only Durable Objects with [SQLite storage backend](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/#create-sqlite-backed-durable-object-class) are available. - **Workers Paid plan**: Durable Objects with the SQLite storage backend are available. The [key-value storage backend](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/#storage-backends) is only avail
- paid ↗
(egress) or throughput (bandwidth) charges. | | Workers Free | Workers Paid | | --- | --- | --- | | Standard operations | 10,000 operations/day included | 1,000,000 operations/month included + $0.40/million operations | | Message retention | 24 hours (non-configurable) | 4 days default, configurable up to 14 days | In most cases, it takes 3 operations to deliver a message: 1 write, 1 read, and 1 delete. Therefore, you can use the following formula to estimate your monthly bill: ```txt ((Number of Messages * 3) - 1,000,000) / 1,000,000 * $0.40 ``` Additionall
- paid ↗
evelopers.cloudflare.com/workers/platform/pricing/) and is priced at **$0.011 per 1,000 Neurons**. Our free allocation allows anyone to use a total of **10,000 Neurons per day at no charge**. To use more than 10,000 Neurons per day, you need to sign up for the [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/#workers). On Workers Paid, you will be charged at $0.011 / 1,000 Neurons for any usage above the free allocation of 10,000 Neurons per day. You can monitor your Neuron usage in the [Cloudflare Workers AI dashboard ↗︎](https://d
- paid ↗
dflare.com/images/pricing/index.md)| [Agent setup](https://developers.cloudflare.com/agent-setup/) By default, all users are on the Images Free plan. The Free plan includes access to the transformations feature, which lets you optimize images stored outside of Images, like in [R2](https://developers.cloudflare.com/r2/). The Paid plan allows transformations, as well as access to storage in Images. Pricing is dependent on which features you use. The table below shows which metrics are used for each use case. | Use case | Metrics | Availability | | --- | --- | --
- paid ↗
loudflare Email Service pricing is based on your Cloudflare plan and email usage. ## Plan pricing Email Routing is available on both the Workers Free and Workers Paid plans. Sending to arbitrary recipients requires the Workers Paid plan. Sending to [verified destination addresses](https://developers.cloudflare.com/email-service/configuration/email-routing-addresses/#destination-addresses) in your account is free on all plans, including when only Email Routing is configured. | | Workers Free | Workers Paid | | --- | --- | --- | | **Outbound emails (Email Sendin
- paid ↗
aid plan includes Workers, Pages Functions, Workers KV, Hyperdrive, and Durable Objects usage for a minimum charge of $5 USD per month for an account. The plan includes increased initial usage allotments, with clear charges for usage that exceeds the base plan. There are no additional charges for data transfer (egress) or throughput (bandwidth). All included usage is on a monthly basis. Pages Functions billing All [Pages Functions](https://developers.cloudflare.com/pages/functions/) are billed as Workers. All pricing and inclusions in this document apply to Pag
- paid ↗
r own R2 bucket. ## How much does it cost? You can setup Mailflare and receive email for free A [Paid Worker](https://developers.cloudflare.com/workers/platform/pricing/) plan ($5/month) is required to send email (and it's recommend to have a smooth experience) ## Deploy Getting started takes three steps: 1. **Deploy the app.** Click **Deploy to Cloudflare** and keep the app name as `mailflare`. The app will not work correctly under another Worker name. 2. **Complete setup.** Open the deployed app and follow `/setup` to check the installation and create your
- AGPL-3.0 ↗
GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
- architecture ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "mailflare", "main": "./worker.ts", "compatibility_date": "2026-05-20", "keep_vars": true, "compatibility_flags": [ "nodejs_compat", "global_fetch_strictly_public" ], "assets": { "binding": "ASSETS", "not_found_handling": "none", }, "images": { "binding": "IMAGES", }, "services": [ { "binding": "WORKER_SELF_REFERENCE", "service": "mailflare", }, ], "durable_objects": { "bindings": [ { "name": "REALTIME", "class_name": "RealtimeHub",
Upstream screenshot · hieunc229/mailflare repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Editorial workflow alternative: Reading and sending custom-domain email with folders, attachments and delegated mailboxes; no full Google Workspace parity.
See supporting source ↗Editorial workflow alternative: Custom-domain webmail and inbox organization; deliverability, migration and native client parity are not verified.
See supporting source ↗How it works
The shape of Mailflare on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit 8aa184db1b6e. Files were read as data; upstream applications and CI jobs were not executed.
Partial source coverage: 10 files outside collection bounds; 0 collection or parsing issues. Dynamic imports and generated entrypoints may need manual review.
Deployment configuration · 2 files
Cloudflare Workers · compatibility 2026-05-20
mailflare · default
Entrypoint: ./worker.ts
Static assets: directory not declared · none
Cron triggers (UTC): 0 2 * * * · */5 * * * *
DB→ D1BUCKET→ R2REALTIME→ Durable Objects · class RealtimeHubAI→ Workers AIIMAGES→ ImagesINBOUND_QUEUE→ Queues (producer) · queue mailflare-inboundOUTBOUND_QUEUE→ Queues (producer) · queue mailflare-outboundAGENT_QUEUE→ Queues (producer) · queue mailflare-agentmailflare-inbound→ Queues (consumer) · queue mailflare-inboundmailflare-outbound→ Queues (consumer) · queue mailflare-outboundmailflare-agent→ Queues (consumer) · queue mailflare-agentWORKER_SELF_REFERENCE→ Worker service · service mailflareEMAIL→ Send EmailASSETS→ Static assets
Cloudflare Workers · compatibility 2026-05-20
mailflare-email-relay · default
Entrypoint: src/index.ts
No resource bindings declared in this scope.
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L25 · fetch handler exported · references REALTIME · calls request.headers.get, hasValidSessionMutationOrigin, getUserFromSession, getSessionTokenFromRequest, env.REALTIME.getByName, hubRequest.headers.set, hub.fetch, vinextHandler.fetch
- L49 · email handler exported · references INBOUND_QUEUE · calls message.setReject, resolveIncomingMail, arrayBuffer, inboundAttachmentLimitReasonFromRaw, forwardMessage, message.headers.get, getAccountForwardingDestination, storeRawToR2, Object.fromEntries, env.INBOUND_QUEUE.send, console.error
- L97 · queue handler exported · calls isInboundQueueMessage, processInboundMessage, processAgentDraftJob, isWebhookRetryMessage, processWebhookRetry, processOutboundQueue, msg.ack, console.error, msg.retry
- L124 · scheduled handler exported · calls ctx.waitUntil, runScheduledDatabaseBackup, runAgentMaintenance
Environment references: env.REALTIME · env.INBOUND_QUEUE
- L26 · email handler exported · references MAILFLARE_URL, INBOUND_WEBHOOK_SECRET · calls message.setReject, arrayBuffer, Object.fromEntries, fetch, env.MAILFLARE_URL.replace, JSON.stringify, sign, response.json, console.error, message.forward
- L16 · sign calls (conditional paths may differ): crypto.subtle.importKey, encode, data.set, join, Array.from, crypto.subtle.sign, padStart, byte.toString
Environment references: env.MAILFLARE_URL · env.INBOUND_WEBHOOK_SECRET
- L35 · processInboundMessage calls (conditional paths may differ): getDb, resolveInboundAddress, console.warn, console.info, limit, where, from, db.select, and, eq, Date.now, existing.createdAt.getTime, scheduleAutoDraft, console.error, env.BUCKET.get, raw.arrayBuffer, parseRawMime, inboundAttachmentLimitReason, env.BUCKET.delete, inboundMessageId
- L246 · storeRawToR2 calls (conditional paths may differ): Date.now, newId, env.BUCKET.put
- L260 · getMessageWithBody calls (conditional paths may differ): getDb, limit, where, from, db.select, eq, getMessageContactNames, listMessageAttachments, getUnsubscribeUrlFromRawR2Key
- L274 · getMessageWithBodyForUser calls (conditional paths may differ): getDb, limit, where, from, db.select, eq, getMailboxAccessLevel, getMessageContactNames, listMessageAttachments, getUnsubscribeUrlFromRawR2Key
- L286 · getMessageMetadataForUser calls (conditional paths may differ): getDb, limit, where, from, db.select, eq, getMailboxAccessLevel, Promise.all, listMessageAttachments, getUnsubscribeUrlFromRawR2Key
Environment references: env.BUCKET
- L57 · toRecipientList calls (conditional paths may differ): Array.isArray, splitEmailAddressList, getEmailAddressList, seen.has, seen.add, result.push, entry.trim
- L70 · sendEmail calls (conditional paths may differ): getDb, getAuthorizedSenderAddress, validateAttachments, getOutboundAttachmentMaxMb, attachments.some, attachments.reduce, toRecipientList, upsertContactFromAddress, normalizeMessageId, Array.isArray, filter, input.references.map, parseMessageIdList, formatMessageIdHeader, encode, join, map, Object.entries, newId, requestedSchedule.getTime
- L181 · deliverEmail calls (conditional paths may differ): getDb, joinEmailAddressList, prepareCloudflareAttachments, where, set, db.update, eq, env.EMAIL.send, Object.keys, prepared.attachments.map, normalizeMessageId, dispatchWebhooks, createAuditLog
- L267 · enqueueScheduledDelivery calls (conditional paths may differ): Math.min, Math.max, Math.ceil, scheduledAt.getTime, Date.now, env.OUTBOUND_QUEUE.send, scheduledAt.toISOString
- L287 · processOutboundQueue calls (conditional paths may differ): getDb, limit, where, from, db.select, eq, JSON.parse, toRecipientList, normalizeMessageId, Array.isArray, filter, input.references.map, parseMessageIdList, formatMessageIdHeader, scheduledAt.getTime, Date.now, enqueueScheduledDelivery, loadMessageAttachmentContents, deliverEmail
Environment references: env.EMAIL · env.OUTBOUND_QUEUE
- L30 · getRetryDelaySeconds calls (conditional paths may differ): Math.min, Math.max
- L34 · parseWebhookEvents calls (conditional paths may differ): JSON.parse, Array.isArray, parsed.filter
- L43 · dispatchWebhooks calls (conditional paths may differ): getDb, where, from, db.select, eq, includes, parseWebhookEvents, JSON.stringify, createDelivery, attemptDelivery, console.error
- L68 · createDelivery calls (conditional paths may differ): newId, values, db.insert
- L87 · attemptDelivery calls (conditional paths may differ): Date.now, signPayload, fetch, AbortSignal.timeout, String, readResponseSnippet, getRetryDelaySeconds, where, set, db.update, error.slice, eq, scheduleRetry
- L149 · scheduleRetry calls (conditional paths may differ): env.OUTBOUND_QUEUE.send, console.error
- L159 · processWebhookRetry calls (conditional paths may differ): runDelivery
- L170 · runDelivery calls (conditional paths may differ): getDb, limit, where, from, db.select, eq, attemptDelivery
- L191 · sendTestDelivery calls (conditional paths may differ): getDb, JSON.stringify, createDelivery, attemptDelivery
- L211 · readResponseSnippet calls (conditional paths may differ): res.text, slice, text.trim
- L220 · signPayload calls (conditional paths may differ): crypto.subtle.importKey, encode, crypto.subtle.sign, join, map, Array.from, padStart, b.toString
Environment references: env.OUTBOUND_QUEUE
- L9 · generateSessionToken calls (conditional paths may differ): newId
- L13 · hashSessionToken calls (conditional paths may differ): crypto.subtle.digest, encode, join, map, Array.from, padStart, byte.toString
- L20 · createSession calls (conditional paths may differ): getDb, generateSessionToken, hashSessionToken, Date.now, values, db.insert, newId
- L36 · getUserFromSession calls (conditional paths may differ): getDb, hashSessionToken, limit, where, from, db.select, and, eq, gt
- L53 · deleteSession calls (conditional paths may differ): getDb, hashSessionToken, where, db.delete, eq
- L63 · deleteUserSessions calls (conditional paths may differ): getDb, hashSessionToken, where, db.delete, and, eq, ne
- L74 · getSessionTokenFromRequestHeaders calls (conditional paths may differ): request.headers.get, trim, authorization.slice, cookie.match, decodeURIComponent
- L7 · getSessionTokenFromRequest calls (conditional paths may differ): request.headers.get, cookie.split, split, part.trim, valueParts.join, decodeURIComponent
- L22 · getMailboxNotificationUserIds calls (conditional paths may differ): getDb, limit, where, innerJoin, from, db.select, eq, isTeamMailboxSharingEnabled, map, filter
- L51 · notifyUsersOfNewMessage calls (conditional paths may differ): Promise.allSettled, userIds.map, env.REALTIME.getByName, hub.fetch, JSON.stringify
Environment references: env.REALTIME
- L27 · inboundAttachmentLimitReasonFromRaw calls (conditional paths may differ): parseRawMime, inboundAttachmentLimitReason
- L2 · hasValidSessionMutationOrigin calls (conditional paths may differ): request.headers.get
- L10 · getAccountForwardingDestination calls (conditional paths may differ): getLicenseEntitlements, getDb, resolveInboundAddress, limit, where, from, db.select, eq, toLowerCase, getEmailAddress
- L8 · runDatabaseBackup calls (conditional paths may differ): getDb, where, set, db.update, eq, getBackupSettings, exportDatabaseRecords, createBackupFilename, env.BUCKET.put, deleteExpiredBackups
- L47 · runScheduledDatabaseBackup calls (conditional paths may differ): createScheduledBackupIfDue, runDatabaseBackup, deleteExpiredBackups
- L56 · deleteExpiredBackups calls (conditional paths may differ): getBackupSettings, Date.now, getDb, where, from, db.select, and, lt, inArray, env.BUCKET.delete, db.delete, eq
Environment references: env.DB · env.BUCKET
- L19 · isAutomaticMessage calls (conditional paths may differ): normalizeEmailAddress, test, Object.fromEntries, map, Object.entries, key.toLowerCase, value.toLowerCase
- L26 · scheduleAutoDraft calls (conditional paths may differ): isAutomaticMessage, getAgentEnabled, getDb, limit, where, from, db.select, eq, includes, getMailboxDomainAddresses, normalizeEmailAddress, getMailboxAccessLevel, newId, onConflictDoNothing, values, db.insert, and
- L43 · processAgentDraftJob calls (conditional paths may differ): getDb, limit, where, from, db.select, eq, returning, set, db.update, Date.now, and, or, lt, Math.floor, now.getTime, getAgentEnabled, skip, getMailboxAccessLevel, since.setUTCHours, gte
- L98 · recoverAgentDraftJobs calls (conditional paths may differ): getDb, limit, where, from, db.select, or, and, eq, lte, lt, Promise.allSettled, due.map, processAgentDraftJob
Environment references: env.AGENT_QUEUE
- L6 · runAgentMaintenance calls (conditional paths may differ): getDb, where, set, db.update, and, eq, lt, Date.now, db.delete, recoverAgentDraftJobs
- L40 · assertBackupTablesCoverDatabase calls (conditional paths may differ): join, INTERNAL_TABLE_PATTERNS.map, INTERNAL_TABLES.map, all, db.prepare, filter, result.results.map, covered.has, unlisted.join, BACKUP_TABLE_GROUPS.flatMap, BACKUP_TABLES.filter, assigned.filter
- L58 · exportDatabaseRecords calls (conditional paths may differ): assertBackupTablesCoverDatabase, getSelectedBackupTables, BACKUP_TABLES.filter, selected.has, databaseTables.has, all, db.prepare, toISOString, encode, JSON.stringify
- L76 · restoreDatabaseRecords calls (conditional paths may differ): parseDatabaseBackup, BACKUP_TABLE_GROUPS.every, group.tables.some, mergeLegacyMessageBodies, fillMissingBackupTables, validateDatabaseBackup, first, db.prepare, BACKUP_TABLES.filter, results.map, all, reverse, run, map, rows.slice, createInsertStatement, db.batch
- L100 · parseDatabaseBackup calls (conditional paths may differ): JSON.parse, decode, isDatabaseBackupDocument
- L107 · isDatabaseBackupDocument calls (conditional paths may differ): Array.isArray, document.includedTables.every, BACKUP_TABLES.includes, REQUIRED_BACKUP_TABLES.every, BACKUP_TABLES.every
- L121 · createInsertStatement calls (conditional paths may differ): filter, Object.keys, availableColumns.has, join, columns.map, column.replaceAll, bind, db.prepare
- L130 · fillMissingBackupTables calls (conditional paths may differ): join, map, Array.from, encode, padStart, byte.toString
- L139 · validateDatabaseBackup calls (conditional paths may differ): Array.isArray
- L9 · getBackupSettings calls (conditional paths may differ): getDb, limit, where, from, db.select, eq, parseExcludedBackupTableGroups
- L19 · listBackups calls (conditional paths may differ): limit, orderBy, from, select, getDb, desc
- L23 · createBackupRecord calls (conditional paths may differ): newId, values, insert, getDb
- L37 · createScheduledBackupIfDue calls (conditional paths may differ): getBackupSettings, isBackupDue, getUtcDayBounds, limit, where, from, select, getDb, and, eq, inArray, gte, lt, createBackupRecord
- L59 · updateBackupSettings calls (conditional paths may differ): where, set, update, getDb, JSON.stringify, eq
- L77 · deleteBackup calls (conditional paths may differ): getDb, limit, where, from, db.select, eq, env.BUCKET.delete, db.delete
Environment references: env.BUCKET
- L6 · isBackupDue calls (conditional paths may differ): now.getUTCDay, now.getUTCDate
- L16 · getUtcDayBounds calls (conditional paths may differ): Date.UTC, now.getUTCFullYear, now.getUTCMonth, now.getUTCDate
- L21 · createBackupFilename calls (conditional paths may differ): replace, now.toISOString
- L26 · mergeLegacyMessageBodies calls (conditional paths may differ): isDatabaseRecord, bodiesByMessageId.set, bodiesByMessageId.get, copyMissingBodyField
- L58 · isDatabaseRecord calls (conditional paths may differ): Array.isArray
- L6 · getAgentModel calls (conditional paths may differ): getAgentProviderConfig, config.models.includes, createWorkersAI, createOpenAICompatible, provider.chatModel
- L18 · agentSystemPrompt calls (conditional paths may differ): agentTimeContext, instructions.slice
Environment references: env.AI
- L9 · resolveAgentBaseUrl calls (conditional paths may differ): customUrl.trim, replace, url.toString
- L16 · getAgentEnabled calls (conditional paths may differ): limit, where, from, select, getDb, eq
- L21 · getAgentProviderConfig calls (conditional paths may differ): limit, where, from, select, getDb, eq, parseAgentModelRates, parseAgentModelIds, resolveAgentBaseUrl
- L39 · getAgentProviderPublicConfig calls (conditional paths may differ): getAgentProviderConfig
Environment references: env.AI_MODEL · env.AI_BASE_URL · env.AI_API_KEY · env.AI
- L66 · publicMessage calls (conditional paths may differ): encodeURIComponent
- L76 · requireAccess calls (conditional paths may differ): getMailboxAccessLevel, getDb
- L84 · ownMessage calls (conditional paths may differ): requireAccess, limit, where, from, select, getDb, and, eq
- L91 · createDraft calls (conditional paths may differ): requireAccess, formatAgentDraftBody, getDb, limit, where, innerJoin, from, db.select, eq, getAuthorizedSenderAddress, buildReplyReferences, parseMessageIdList, newId, values, db.insert, buildSnippet, formatMessageIdHeader, db.delete
- L119 · runEmailTool calls (conditional paths may differ): parse, getDb, requireAccess, eq, conditions.push, isNull, or, buildSearchConditions, ownMessage, lt, limit, orderBy, where, from, db.select, and, desc, map, rows.slice, listMessageAttachments
- L4 · getUserMailRevision calls (conditional paths may differ): all, bind, env.DB.prepare, JSON.stringify, result.results.map
Environment references: env.DB
Build and deployment pipeline · 1 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: workflow_dispatch
update · no job dependencies declared
- Check update token
if [ -z "${UPDATE_TOKEN}" ]; then echo "::error::Add MAILFLARE_UPDATE_TOKEN as a repository Actions secret." exit 1 fi - Checkout repository
actions/checkout@v4 - Replace repository with upstream source
set -euo pipefail git config user.name "mailflare-update" git config user.email "mailflare-update@users.noreply.github.com" upstream_url="https://github.com/${UPDATE_SOURCE_REPOSITORY}.git" if git remote get-url upstream >/dev/null 2>&1; then git remote set-url upstream "${upstream_url}" else git remote add upstream "${upstream_url}" fi git fetch --no-tags upstream upstream_branch="$(git remote show upstream | sed -n 's/.*HEAD branch: //p')" if [ -z "${upstream_branch}" ]; then echo "::error::C… - Push update
set -euo pipefail if [ -z "$(git log "origin/${GITHUB_REF_NAME}..HEAD" --oneline)" ]; then echo "Repository is already up to date." exit 0 fi git push origin "HEAD:${GITHUB_REF_NAME}"
build: npm run db:bundle && vite buildbuild:next: npm run db:bundle && MAILFLARE_RUNTIME=node next builddeploy: npm run build && npm run deploy:workerdeploy:worker: wrangler deploydeploy:local: npm run deploymigrate: wrangler d1 migrations apply DBbuild:node: npm run db:bundle && MAILFLARE_RUNTIME=node next build && node scripts/build-server.mjs
deploy: wrangler deploy
Repository README
View original on GitHub ↗Full upstream document by @hieunc229 · README.md · snapshot 8aa184d
Mailflare
Mailflare is a self-hosted email inbox for custom domains, built on Cloudflare.
Screenshots
![]() Inbox |
![]() Manage domains |
![]() Manage inboxes |
|---|
Featured sponsors
Want to support the mailflare? Start sponsoring
What you can do
- Domain setup: Connect domains and set up Cloudflare Email Routing from the dashboard.
- Mailboxes: Create personal and shared mailboxes with delegated access.
- Email: Send and receive email with attachments, rich formatting, signatures, and automatic replies.
- Inbox organization: Organize mail with search, custom folders, stars, snoozing, archive, spam, and trash.
- Routing rules: Create routing rules to store, forward, reject, or categorize incoming messages.
- Notifications: Get real-time inbox updates and new-message notifications.
- Mail and contacts: Import and export mail, manage contacts, and block unwanted senders.
- Administration: Manage accounts, permissions, API keys, webhooks, audit logs, and database backups.
- Email AI Assistant: Use an AI assistant to search mail, work with threads, and prepare drafts for a selected mailbox.
- MCP access: Connect external AI clients through MCP with mailbox or admin permissions chosen for each key.
How it works
Mailflare runs in your Cloudflare account. Email Routing delivers incoming messages to the app, while Cloudflare's email service handles outgoing messages. Your mail data stays in your own D1 database and attachments are stored in your own R2 bucket.
How much does it cost?
You can setup Mailflare and receive email for free
A Paid Worker plan ($5/month) is required to send email (and it's recommend to have a smooth experience)
Deploy
Getting started takes three steps:
- Deploy the app. Click Deploy to Cloudflare and keep the app name as
mailflare. The app will not work correctly under another Worker name. - Complete setup. Open the deployed app and follow
/setupto check the installation and create your admin account. - Connect your domain. Add a domain managed by the same Cloudflare account. Mailflare configures its email routing and helps you create the first mailbox.
⚠️ IMPORTANT: CF_TOKEN is required during deployment. Create a scoped Cloudflare API token with the following permissions for the domains you want to connect.
- All accounts - Email Sending:Edit, DNS Settings:Edit, Email Routing Addresses:Edit
- All zones - DNS Settings:Edit, Email Routing Rules:Edit, Zone Settings:Edit, DNS:Edit
Deploy with an AI coding agent
You can paste the prompt below into an agent that has terminal access. Give it the Cloudflare account ID and two separate scoped API tokens through the agent's secret input, not in a public chat, repository, or committed file:
- Deployment token (used locally by Wrangler as
CLOUDFLARE_API_TOKEN): scope it to the target account with Workers Scripts Edit (or Workers Admin if Cloudflare's newer granular roles are shown, since this is a new Worker), D1 Edit, Workers R2 Storage Edit, Queues Edit, and Account Settings Read. Add Workers Routes Edit for the target zone only if you want the agent to attach a custom domain or route. See Cloudflare's token permissions and Workers roles. - Runtime token (stored as the Worker's
CF_TOKENsecret): use the domain permissions listed above. Add Email Sending Edit if you want to send mail. This token is separate from the deployment token and must cover the zones you will connect in Mailflare.
Install Mailflare from https://github.com/hieunc229/mailflare in my Cloudflare account.
Ask me for my Cloudflare account ID, a scoped deployment API token, and a separate
runtime CF_TOKEN through a secret input. Never print, commit, or place either token
in a command argument or a tracked file. Use the deployment token only for Wrangler
authentication (CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID).
Read README.md, docs/deployment.md, and wrangler.jsonc first. Keep the Worker name
exactly mailflare. In the selected account, create or reuse the D1 database
mailflare, R2 bucket mailflare-raw, and Queues mailflare-inbound,
mailflare-outbound, and mailflare-agent. Set the D1 database_id in the local
Wrangler config without committing that account-specific ID. Install dependencies,
run npm run deploy, and set the runtime CF_TOKEN as a Worker secret. Do not run
remote D1 migrations manually; the /setup flow initializes the database.
Give me the deployed URL and any remaining Cloudflare account actions. I will
open /setup, create the first admin account, and connect my domain there.
See the deployment guide for required permissions, manual deployment, backups, and updates.
Self-host with Docker instead
Mailflare also runs as one container on any server, with SQLite and local files in place of D1 and R2, a built-in SMTP listener for inbound mail (or a small Cloudflare relay Worker if you want to keep MX on Cloudflare), and any SMTP relay or Cloudflare Email Sending for outbound.
cp .env.docker.example .env.docker
docker compose up -d --build
See docs/self-hosting.md.
Local development
cp .dev.vars.example .dev.vars
npm install
npm run db:migrate:local
npm run dev
Add your Cloudflare credentials to .dev.vars, then open http://localhost:3000. For sample local data, run npm run db:seed while the development server is running.
The Cloudflare app uses vinext and the Cloudflare Vite plugin, including local D1, R2, Queues, and Durable Objects. Remote bindings are disabled by default. To use Workers AI locally, authenticate with Wrangler, select your account with CLOUDFLARE_ACCOUNT_ID, and run CLOUDFLARE_REMOTE_BINDINGS=true npm run dev.
npm run build builds the complete Worker; npm run start previews that build locally. npm run deploy builds and deploys it. The separate Node/Docker runtime still uses Next.js and the existing build:node, start:node, and dev:node commands.
Documentation
License
See LICENSE.
Frequently asked about Mailflare
What is Mailflare?+
Mailflare is a self-hosted Fastmail/Gmail alternative built on the Cloudflare developer platform. Custom-domain mailboxes and a shared inbox in your Cloudflare account.
What does Mailflare replace?+
Mailflare is listed as an alternative to Fastmail, Gmail. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does Mailflare use?+
Mailflare is built on D1, Durable Objects, Email Workers, Images, Queues, R2, Workers, Workers AI.
How much does Mailflare cost to run?+
Cloudflare sending requires Workers Paid, minimum $5 USD per account/month; domain, excess usage and optional AI/Images charges are additional. Sending requires a Workers Paid account, minimum $5 USD/month; receiving-only use is described upstream but the release scope includes sending. Provide a domain in the same Cloudflare account, Email Routing/Sending setup and scoped CF_TOKEN; domain registration is a separate cost. Keep D1, R2, SQLite DO and Queues usage inside paid included allocations; excess requests, storage and operations are billed. Workers AI and Images bindings are observed, but those optional features have independent quotas and may add charges. Keep the Worker name mailflare and use your own resource IDs; do not import the separate Docker email relay into this app diagram. Check current Cloudflare pricing before deploying.
Is Mailflare open source?+
The upstream repository declares the AGPL-3.0 license. Read its terms at https://raw.githubusercontent.com/hieunc229/mailflare/8aa184db1b6e38ee48453cb7ed03ccabef9913f8/LICENSE. Source code and contributor credit are available at https://github.com/hieunc229/mailflare.





Discussion · 0
sign in to comment →