Cloudsteading
Inbox

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

@hieunc229

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.
Check current pricing ↗
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.

Gmail logoGmail ↗

Editorial workflow alternative: Reading and sending custom-domain email with folders, attachments and delegated mailboxes; no full Google Workspace parity.

See supporting source ↗
external SaaS target
varies
→ D1 + Durable Objects + Email Workers
external SaaS target
varies
→ D1 + Durable Objects + Email Workers

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
wrangler.jsonc ↗

Cloudflare Workers · compatibility 2026-05-20

mailflare · default

Entrypoint: ./worker.ts

Static assets: directory not declared · none

Cron triggers (UTC): 0 2 * * * · */5 * * * *

  • DB → D1
  • BUCKET → R2
  • REALTIME → Durable Objects · class RealtimeHub
  • AI → Workers AI
  • IMAGES → Images
  • INBOUND_QUEUE → Queues (producer) · queue mailflare-inbound
  • OUTBOUND_QUEUE → Queues (producer) · queue mailflare-outbound
  • AGENT_QUEUE → Queues (producer) · queue mailflare-agent
  • mailflare-inbound → Queues (consumer) · queue mailflare-inbound
  • mailflare-outbound → Queues (consumer) · queue mailflare-outbound
  • mailflare-agent → Queues (consumer) · queue mailflare-agent
  • WORKER_SELF_REFERENCE → Worker service · service mailflare
  • EMAIL → Send Email
  • ASSETS → Static assets
deploy/cloudflare-email-relay/wrangler.jsonc ↗

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.

    worker.ts ↗
    • 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

    deploy/cloudflare-email-relay/src/index.ts ↗
    • 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

    src/lib/email/inbound.ts ↗
    • 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

    src/lib/email/send.ts ↗
    • 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

    src/lib/email/webhooks.ts ↗
    • 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

    src/lib/email/incoming.ts ↗
    • L11 · resolveIncomingMail calls (conditional paths may differ): getDb, resolveInboundAddress, catch, recordRuleMatch, console.error
    • L35 · forwardMessage calls (conditional paths may differ): headers.set, message.forward, console.error
    src/lib/auth/session.ts ↗
    • 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
    src/lib/realtime/utils.ts ↗
    • 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

    src/lib/email/inbound-attachments.ts ↗
    • L27 · inboundAttachmentLimitReasonFromRaw calls (conditional paths may differ): parseRawMime, inboundAttachmentLimitReason
    src/lib/auth/origin.ts ↗
    • L2 · hasValidSessionMutationOrigin calls (conditional paths may differ): request.headers.get
    src/lib/email/account-forwarding.ts ↗
    • L10 · getAccountForwardingDestination calls (conditional paths may differ): getLicenseEntitlements, getDb, resolveInboundAddress, limit, where, from, db.select, eq, toLowerCase, getEmailAddress
    src/lib/backups/runner.ts ↗
    • 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

    src/lib/agent/jobs/utils.ts ↗
    • 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

    src/lib/agent/maintenance.ts ↗
    • L6 · runAgentMaintenance calls (conditional paths may differ): getDb, where, set, db.update, and, eq, lt, Date.now, db.delete, recoverAgentDraftJobs
    src/lib/backups/export.ts ↗
    • 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
    src/lib/backups/service.ts ↗
    • 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

    src/lib/backups/utils.ts ↗
    • 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
    src/lib/agent/model.ts ↗
    • 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

    src/lib/agent/provider.ts ↗
    • 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

    src/lib/agent/errors.ts ↗
    • L5 · readableText calls (conditional paths may differ): trim, value.replace, text.slice
    • L11 · parseResponseBody calls (conditional paths may differ): asRecord, JSON.parse
    • L17 · agentProviderErrorMessage calls (conditional paths may differ): asRecord, parseResponseBody, test, Number, readableText
    src/lib/agent/tools.ts ↗
    • 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
    src/lib/realtime/revision.ts ↗
    • 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.

    Dashboard Update · .github/workflows/deploy-update.yml ↗

    Triggers: workflow_dispatch

    update · no job dependencies declared

    1. Check update tokenif [ -z "${UPDATE_TOKEN}" ]; then echo "::error::Add MAILFLARE_UPDATE_TOKEN as a repository Actions secret." exit 1 fi
    2. Checkout repositoryactions/checkout@v4
    3. Replace repository with upstream sourceset -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…
    4. Push updateset -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}"
    package.json ↗
    • build: npm run db:bundle && vite build
    • build:next: npm run db:bundle && MAILFLARE_RUNTIME=node next build
    • deploy: npm run build && npm run deploy:worker
    • deploy:worker: wrangler deploy
    • deploy:local: npm run deploy
    • migrate: wrangler d1 migrations apply DB
    • build:node: npm run db:bundle && MAILFLARE_RUNTIME=node next build && node scripts/build-server.mjs

    Full upstream document by @hieunc229 · README.md · snapshot 8aa184d

    Mailflare

    Mailflare

    Mailflare is a self-hosted email inbox for custom domains, built on Cloudflare.

    Deploy to Cloudflare

    Screenshots

    Inbox
    Inbox
    Manage domains
    Manage domains
    Manage inboxes
    Manage inboxes
    Sequenzy Drivemug

    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:

    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 admin account.
    3. 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_TOKEN secret): 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 →
    No comments yet — be the first.