forwardemail/mail.forwardemail.net

Open-Source & Privacy-focused Webmail: A privacy-first, offline-capable Progressive Web App for Forward Email. Ships as static assets and runs entirely in the browser with local caching, full-text search, and multi-account support.

35

stars

731

commits

JavaScript

primary language

Sep 9, 2026

updated

mail.forwardemail.net
app
caching
javascript
js
mail
node
nodejs
offline
pwa
service-worker
webmail

README

Forward Email - Webmail, Desktop, and Mobile

This is the official, open-source, and end-to-end encrypted webmail client for Forward Email. It is available as a fast and modern web app, a cross-platform desktop app for Windows, macOS, and Linux, and a native mobile app for iOS and Android.

Table of Contents

Downloads & Releases

Official desktop and Android artifacts are published on the GitHub Releases page. Desktop builds are produced by the public GitHub Actions release workflows, and the desktop release matrix now targets macOS arm64/x64, Windows x64/arm64, and Linux x64/arm64.

Snap Store Flathub GitHub Release

PlatformArchitectureDownloadStore
Webmail.forwardemail.net
Windowsx64.msi / .exe on GitHub Releases
Windowsarm64-setup.exe on GitHub Releases
macOSApple Silicon & Intel.dmg on GitHub ReleasesApp Store (Coming Soon)
Linuxx64.deb / .AppImage / .rpm on GitHub Releases
Linuxarm64.deb / .rpm on GitHub Releases
Linuxx64 / arm64snap install forwardemail-mail after publicationSnap Store
Linuxx64 / arm64flatpak install flathub net.forwardemail.mail after publicationFlathub
AndroidUniversalDual-provider .apk / .aab on GitHub ReleasesGoogle Play (Coming Soon)
AndroidGoogle-freeforwardemail-mail_<version>_fdroid.apk on GitHub ReleasesSelf-hosted F-Droid repository / Obtainium
iOSarm64TestFlight / App Store distributionApp Store (Coming Soon)

Linux package managers

Snap Store

After the Snap Store listing is live, install the strict-confinement package with:

sudo snap install forwardemail-mail

Snap updates are managed by the operating system. To inspect the installed channel and version, run snap info forwardemail-mail.

Flathub

After the Flathub page is live, install and run the sandboxed Flatpak with:

flatpak install flathub net.forwardemail.mail
flatpak run net.forwardemail.mail

Flatpak manages updates for this installation; the in-app GitHub updater is intentionally disabled in the Flatpak build. If your system has not added Flathub yet, follow the Flathub setup instructions first.

Android without Google Play

F-Droid-compatible repository

The official repository publishes the Google-free UnifiedPush-only APK. After the repository is live, add this URL to an F-Droid-compatible client:

https://forwardemail.github.io/mail.forwardemail.net/fdroid/repo

Before trusting the repository, compare the fingerprint displayed by the client with the official value at the published fingerprint file. The URL and fingerprint are not interchangeable: verify both before installing. The repository is self-hosted and is not part of the public f-droid.org catalog.

Obtainium

Obtainium can track the Google-free APK directly from GitHub Releases. In Obtainium, choose Add App, enter the repository URL below, select the GitHub source if prompted, and choose the asset ending in _fdroid.apk:

https://github.com/forwardemail/mail.forwardemail.net

Install a compatible UnifiedPush distributor before turning on background notifications. Obtainium will then discover future Google-free release assets automatically.

Ubuntu / Debian installation

For Ubuntu x64 / amd64, download the current .deb asset from GitHub Releases, then install it with:

sudo apt update
sudo apt install ./Forward.Email_<version>_amd64.deb

For Ubuntu arm64 / aarch64, install the matching arm64 .deb the same way:

sudo apt update
sudo apt install ./Forward.Email_<version>_arm64.deb

For a portable Linux x64 / amd64 binary, download Forward.Email_<version>_amd64.AppImage from GitHub Releases, make it executable, and run it directly. The release matrix does not publish an arm64 AppImage; Linux arm64 users should install the .deb or .rpm asset instead.

chmod +x Forward.Email_<version>_amd64.AppImage
./Forward.Email_<version>_amd64.AppImage

If you are building your own custom Linux binary instead of installing a published release, use the desktop development guide in docs/desktop-setup.md.

Note for macOS users: If you download the .dmg from GitHub Releases, you may need to run the following command if you see a "damaged" or unverified app error:

sudo xattr -rd com.apple.quarantine "/Applications/Forward Email.app"

Replace /Applications/Forward Email.app with the actual path if you installed the app elsewhere.

Screenshots

Screenshots as of August 27, 2026.

These screenshots are captured automatically from the production Demo Account after each successful release. Expand a theme and device group to browse its views.

Dark mode — Desktop
ViewScreenshot
login pageDark mode login page
mail viewDark mode mail view
message viewDark mode message view
compose windowDark mode compose window
contacts pageDark mode contacts page
calendar pageDark mode calendar page
profile pageDark mode profile page
diagnostics pageDark mode diagnostics page
general settings pageDark mode general settings page
appearance settings pageDark mode appearance settings page
privacy and security pageDark mode privacy and security page
folders and labels settings pageDark mode folders and labels settings page
search settings pageDark mode search settings page
advanced settings pageDark mode advanced settings page
keyboard shortcuts pageDark mode keyboard shortcuts page
about and help pageDark mode about and help page
Dark mode — Mobile
ViewScreenshot
login pageDark mode mobile login page
mail viewDark mode mobile mail view
message viewDark mode mobile message view
compose windowDark mode mobile compose window
contacts pageDark mode mobile contacts page
calendar pageDark mode mobile calendar page
profile pageDark mode mobile profile page
diagnostics pageDark mode mobile diagnostics page
general settings pageDark mode mobile general settings page
appearance settings pageDark mode mobile appearance settings page
privacy and security pageDark mode mobile privacy and security page
folders and labels settings pageDark mode mobile folders and labels settings page
search settings pageDark mode mobile search settings page
advanced settings pageDark mode mobile advanced settings page
keyboard shortcuts pageDark mode mobile keyboard shortcuts page
about and help pageDark mode mobile about and help page
Light mode — Desktop
ViewScreenshot
login pageLight mode login page
mail viewLight mode mail view
message viewLight mode message view
compose windowLight mode compose window
contacts pageLight mode contacts page
calendar pageLight mode calendar page
profile pageLight mode profile page
diagnostics pageLight mode diagnostics page
general settings pageLight mode general settings page
appearance settings pageLight mode appearance settings page
privacy and security pageLight mode privacy and security page
folders and labels settings pageLight mode folders and labels settings page
search settings pageLight mode search settings page
advanced settings pageLight mode advanced settings page
keyboard shortcuts pageLight mode keyboard shortcuts page
about and help pageLight mode about and help page
Light mode — Mobile
ViewScreenshot
login pageLight mode mobile login page
mail viewLight mode mobile mail view
message viewLight mode mobile message view
compose windowLight mode mobile compose window
contacts pageLight mode mobile contacts page
calendar pageLight mode mobile calendar page
profile pageLight mode mobile profile page
diagnostics pageLight mode mobile diagnostics page
general settings pageLight mode mobile general settings page
appearance settings pageLight mode mobile appearance settings page
privacy and security pageLight mode mobile privacy and security page
folders and labels settings pageLight mode mobile folders and labels settings page
search settings pageLight mode mobile search settings page
advanced settings pageLight mode mobile advanced settings page
keyboard shortcuts pageLight mode mobile keyboard shortcuts page
about and help pageLight mode mobile about and help page

Security & Privacy

Security is the foundational principle of this application. We are committed to transparency and providing users with control over their data. For a detailed overview of our security practices, please see:

Client-Side Encryption & App Lock

The application offers a robust App Lock feature that enables cryptographic encryption for your entire client-side database and settings — in the browser, on desktop, or on mobile. When enabled from the Settings > Privacy & Security menu, all sensitive data stored locally (including message bodies, contacts, and API tokens) is encrypted at rest using the XSalsa20-Poly1305 stream cipher from the audited libsodium library.

This feature can be secured using two methods:

  1. Passkey (WebAuthn): For the highest level of security, you can lock and unlock the application using a FIDO2/WebAuthn-compliant authenticator. This allows you to use hardware security keys or your device's built-in biometrics. The encryption key is derived directly from the authenticator using the PRF extension, meaning the key is never stored on the device itself.
  2. PIN Code: For convenience, you can set a simple PIN code. This provides an iOS-like lock screen experience, ideal for quick access on mobile devices.

Our implementation supports a wide range of authenticators for Passkey-based App Lock:

TypeExamples
Platform AuthenticatorsApple Touch ID, Face ID, Optic ID, Windows Hello, Android Biometrics (fingerprint, face)
Hardware Security KeysYubiKey 5 Series, YubiKey Bio, Google Titan, Feitian ePass/BioPass, SoloKeys, Nitrokey 3, HID Crescendo, Ledger
Cloud/Software PasskeysiCloud Keychain, Google Password Manager, Samsung Pass, 1Password, Dashlane, Bitwarden, Proton Pass

Tamper-Proof Builds

All builds are handled by public GitHub Actions workflows directly from the source code. Desktop applications are signed with platform-specific certificates (Apple Developer ID and Windows Authenticode), and the Tauri updater uses Ed25519 signatures to verify every update package. Mailbox content and WebSocket updates travel directly between the app and Forward Email. Platform delivery infrastructure is used only where the operating system requires it: APNs on iOS, FCM or a user-selected UnifiedPush distributor on Android, and GitHub Releases for desktop update packages. Forward Email does not add advertising, analytics, or tracking intermediaries to those paths.

Features

  • Blazing Fast: Built with Rust and Svelte 5 for a lightweight and responsive experience.
  • End-to-End Encrypted: Cryptographic encryption for your entire client-side app — browser, desktop, or mobile.
  • Open-Source: All code for the web, desktop, and mobile apps is available on GitHub.
  • No Advertising or Tracking Intermediaries: Mailbox data and real-time WebSocket updates stay between the app and Forward Email. Platform push providers and release hosting are used only to deliver native notifications and signed application updates.
  • Real-time Updates: Mailbox updates are pushed instantly via WebSockets.
  • Cross-Platform Notifications: Native desktop and mobile push notifications.
  • Multi-account — Login with multiple Forward Email accounts, alias auth, and optional API key override.
  • Mailbox — Folders, message threading, bulk actions, keyboard shortcuts, attachment handling, PGP decryption.
  • Compose — Rich text editor (TipTap), CC/BCC, emoji picker, attachments, draft autosave, offline outbox queue.
  • Search — Full-text search with FlexSearch, optional body indexing, saved searches, background indexing.
  • Offline Support: A custom main-thread sync engine provides offline access and queues outgoing actions, replacing the need for a Service Worker and ensuring functionality on all platforms including Ionic/Capacitor mobile.
  • Calendar — Month/week/day views, quick add/edit/delete, iCal export.
  • Contacts — CRUD operations, vCard import/export, deep links to compose/search.
  • Demo Mode: Evaluate the app's features offline without an account.
  • mailto: Handler: Registers as the default email client on desktop platforms.
  • Auto-Updates: Desktop apps automatically check for and install new versions securely.

Architecture Overview

The application is built on a unified architecture that reuses the same Svelte 5 web application as the UI for all platforms. Tauri v2 provides the cross-platform shell, using a Rust backend for native capabilities and system webviews for rendering the UI.

graph TD
    subgraph "Web App (Svelte 5 + Vite)"
        A[UI Components] --> B(Stores)
        B --> C{API Client}
        C --> D[REST API]
        B --> E{WebSocket Client}
        E --> F[Real-time API]
    end
    subgraph "Tauri (Desktop & Mobile)"
        G[Rust Backend] --> H{System WebView}
        H -- loads --> A
        I[Tauri IPC] -- JS Bridge --> A
        G -- IPC --> I
        J[Tauri Plugins] --> G
    end
    D --- M(api.forwardemail.net)
    F --- M

For more detail, please see the full Architecture Document.

Tech Stack

CategoryTechnologies
WebSvelte 5, Vite, pnpm
Desktop & MobileTauri v2 (Rust backend, Svelte frontend)
StylingTailwind CSS 4, PostCSS
StateSvelte Stores
DatabaseDexie 4 (IndexedDB)
SearchFlexSearch
EditorTipTap 2
Calendarschedule-x
Real-timeWebSocket with msgpackr binary encoding
Encryptionlibsodium-wrappers (XSalsa20-Poly1305, Argon2id), OpenPGP
Passkeys@passwordless-id/webauthn (FIDO2/WebAuthn with PRF extension)
TestingVitest, Playwright for E2E tests, WebdriverIO for Tauri binary tests
ToolingESLint 9, Prettier 3, Husky, commitlint

Key Components

  • Main Thread — Svelte components, stores, routing, UI rendering
  • db.worker — Owns IndexedDB via Dexie, handles all database operations
  • sync.worker — API fetching, message parsing (PostalMime), data normalization
  • search.worker — FlexSearch indexing and query execution

Documentation

Detailed architecture documentation is available in the docs/ directory:

Project Structure

src/
├── main.ts                 # App bootstrap, routing, service worker registration
├── config.ts               # Environment configuration
├── stores/                 # Svelte stores (state management)
│   ├── mailboxStore.ts     # Message list, folders, threading
│   ├── mailboxActions.ts   # Move, delete, flag, label actions
│   ├── messageStore.ts     # Selected message, body, attachments
│   ├── searchStore.ts      # Search queries and index health
│   ├── settingsStore.ts    # User preferences, theme, PGP keys
│   └── ...
├── svelte/                 # Svelte components
│   ├── Mailbox.svelte      # Main email interface
│   ├── Compose.svelte      # Email composer
│   ├── Calendar.svelte     # Calendar view
│   ├── Contacts.svelte     # Contact management
│   ├── Settings.svelte     # User settings
│   └── components/         # Reusable components
├── workers/                # Web Workers
│   ├── db.worker.ts        # IndexedDB operations
│   ├── sync.worker.ts      # API sync and parsing
│   └── search.worker.ts    # Search indexing
├── utils/                  # Utilities
│   ├── remote.js           # API client
│   ├── db.js               # Database initialization
│   ├── storage.js          # LocalStorage management
│   └── ...
├── lib/components/ui/      # UI component library (shadcn/ui)
├── styles/                 # CSS (Tailwind + custom)
├── locales/                # i18n translations
└── types/                  # TypeScript definitions

Getting Started

Prerequisites

Installation

pnpm install

Development

pnpm dev              # Start web dev server (http://localhost:5174)
pnpm tauri dev        # Start desktop dev mode
pnpm tauri:android:dev  # Start Android dev mode (preflight checks + adb setup)
pnpm tauri:ios:dev      # Start iOS dev mode (preflight checks + simulator boot)

Build

pnpm build        # Build to dist/ + generate service worker
pnpm preview      # Preview production build locally
pnpm analyze      # Build with bundle analyzer

Code Quality

pnpm lint         # Run ESLint
pnpm lint:fix     # Fix linting issues
pnpm format       # Check formatting
pnpm format:fix   # Fix formatting
pnpm check        # Run svelte-check

Testing

# Unit tests (Vitest)
pnpm test              # Run all tests
pnpm test:watch        # Watch mode
pnpm test:coverage     # Generate coverage report

# E2E tests (Playwright)
pnpm exec playwright install --with-deps  # First-time setup
pnpm test:e2e          # Run e2e tests

Contributing

Commit Messages

This project uses Conventional Commits enforced by commitlint. Every commit message must follow the format:

type(scope): description
TypeWhen to useVersion bump
featNew featureminor
fixBug fixpatch
docsDocumentation onlynone
refactorCode change that neither fixes nor addsnone
perfPerformance improvementpatch
testAdding or updating testsnone
choreBuild, CI, tooling changesnone

Scope is optional: fix(compose): handle pasted recipients or fix: handle pasted recipients are both valid.

To trigger a major version bump, add a BREAKING CHANGE: footer:

feat: redesign settings page

BREAKING CHANGE: settings store schema changed, requires cache clear

Releasing

Releases are managed locally using np. Version bumps still flow through pnpm release, and desktop artifact publishing is handled by the Tauri desktop release workflow plus the pnpm release:desktop helper for desktop-only hotfixes.

pnpm release            # interactive version prompt, runs checks, pushes, publishes GitHub Release

This command will:

  1. Verify a clean working tree and up-to-date main branch
  2. Run lint, format, tests, and build
  3. Bump the version in package.json and create a git tag
  4. Push the commit and tag to GitHub
  5. Publish a GitHub Release

For desktop releases, pushing a desktop tag or running the workflow manually triggers Release Desktop (Tauri) (.github/workflows/release-desktop.yml), which creates or updates a draft GitHub Release and uploads the macOS x64/arm64, Windows x64/arm64, and Linux x64/arm64 desktop artifacts.

Configuration

Create a .env file to override defaults:

# API base URL (Vite requires VITE_ prefix for client exposure)
VITE_WEBMAIL_API_BASE=https://api.forwardemail.net

Deployment

First time setup? See the complete Deployment Checklist for step-by-step instructions on Cloudflare, GitHub Actions, and DNS configuration.

Infrastructure

graph TB
    subgraph Edge["Cloudflare Edge"]
        subgraph Worker["Cloudflare Worker"]
            W1["SPA routing (returns index.html for /mailbox, etc)"]
            W2["Cache headers (immutable for assets, no-cache HTML)"]
        end
        Worker --> R2
        subgraph R2["Cloudflare R2"]
            R2A["Static assets (dist/)"]
            R2B["Fingerprinted bundles (/assets/*.js, *.css)"]
        end
    end

Cache Strategy

Asset TypeCache-ControlReason
index.html, /mailbox, /calendar, etc.no-cache, no-storeAlways fetch fresh HTML for updates
/assets/* (JS, CSS)immutable, max-age=31536000Fingerprinted by Vite, safe to cache forever
sw.js, sw-*.js, version.jsonno-cache, must-revalidateService worker must check for updates
/icons/*max-age=259200030 days, rarely change
Fonts (.woff2)immutable, max-age=31536000Fingerprinted, cache forever

CI/CD Pipeline

The top-level Release workflow (.github/workflows/release.yml) is the production orchestrator for every v* tag. It runs the WebView E2E gate, creates the draft GitHub Release, calls the reusable desktop and mobile workflows, deploys the web application to Cloudflare R2 and Workers, publishes the release, generates checksums, and optionally sends a Matrix notification.

The orchestrator calls two reusable platform workflows:

  1. Release Desktop (Tauri) (.github/workflows/release-desktop.yml) builds macOS x64/arm64, Windows x64/arm64, and Linux x64/arm64 desktop artifacts. See Desktop Build CI guide for the platform matrix, runner details, and artifact expectations.
  2. Release Mobile (.github/workflows/release-mobile.yml) builds the signed dual-provider Android APK/AAB and the signed iOS IPA, then uploads configured store artifacts. See Release Process, iOS Setup, and Secrets for the exact signing inputs and release flow.

The separate deploy.yml workflow is a manual workflow_dispatch recovery path for redeploying the current web build; normal tagged releases deploy inline from release.yml.

Local validation for release work should still cover the standard web checks before tagging a release:

  1. Installpnpm install --frozen-lockfile
  2. Lintpnpm lint
  3. Formatpnpm format
  4. Unit testspnpm test -- --run
  5. Buildpnpm build
  6. Desktop build smoke testpnpm tauri:build for the target platform you are validating

For exact secret generation, GitHub environment setup, and platform-specific signing steps, use docs/SECRETS.md as the canonical guide, with docs/desktop-ci-secrets.md and docs/ios-setup.md as platform-specific companions.

Required Secrets & Variables

GitHub Secrets:

SecretDescription
R2_ACCOUNT_IDCloudflare account ID (also used for Workers)
R2_ACCESS_KEY_IDR2 API access key
R2_SECRET_ACCESS_KEYR2 API secret key
CLOUDFLARE_ZONE_IDZone ID for cache purge
CLOUDFLARE_API_TOKENAPI token with R2 + Workers + cache-purge permissions
TAURI_SIGNING_PRIVATE_KEYTauri updater Ed25519 signing key
TAURI_SIGNING_PRIVATE_KEY_PASSWORDPassword for the Tauri signing key
APPLE_CERTIFICATEBase64-encoded macOS .p12 signing certificate
APPLE_CERTIFICATE_PASSWORDPassword used to export the macOS .p12
APPLE_SIGNING_IDENTITYApple Developer ID signing identity
APPLE_IDApple ID for notarization
APPLE_PASSWORDApp-specific password for notarization
APPLE_TEAM_IDApple Developer Team ID
WINDOWS_CERTIFICATEBase64-encoded exportable Windows .pfx code-signing certificate
WINDOWS_CERTIFICATE_PASSWORDPassword used to export the Windows .pfx
ANDROID_KEYSTORE_BASE64Android signing keystore (base64)
ANDROID_KEYSTORE_PASSWORDPassword for the Android keystore
ANDROID_KEY_ALIASAndroid signing key alias
ANDROID_KEY_PASSWORDPassword for the Android signing key
IOS_CERTIFICATE_BASE64Base64-encoded iOS Apple Distribution .p12
IOS_CERTIFICATE_PASSWORDPassword used to export the iOS .p12
IOS_PROVISIONING_PROFILE_BASE64Base64-encoded App Store provisioning profile
APP_STORE_CONNECT_API_KEYFull contents of the downloaded App Store Connect .p8 key
APP_STORE_CONNECT_KEY_IDApp Store Connect API key ID
APP_STORE_CONNECT_ISSUER_IDApp Store Connect issuer ID
GOOGLE_SERVICES_JSON_BASE64Firebase Android client configuration for the dual-provider build
GOOGLE_PLAY_SERVICE_ACCOUNTOptional Google Play publishing service-account JSON
MATRIX_TOKENOptional repository secret for release and activity notifications
SNAPCRAFT_STORE_CREDENTIALSOptional release secret enabling Snap Store stable-channel publishing
FDROID_KEYSTORE_BASE64 / FDROID_KEYSTORE_PASSWORDOptional release secrets used to sign the self-hosted F-Droid repository index
HOMEBREW_TAP_TOKENOptional release secret limited to creating cask pull requests in the first-party tap

All secrets above except MATRIX_TOKEN belong in the release environment. MATRIX_TOKEN is a repository Actions secret because the notification jobs do not attach the release environment. GITHUB_TOKEN is supplied automatically by GitHub Actions and must not be created manually.

GitHub Variables:

VariableDescription
R2_BUCKETR2 bucket name for static assets
IOS_SIGNING_IDENTITYOptional iOS signing identity override; defaults to Apple Distribution
VAPID_PUBLIC_KEYRequired public half of the backend VAPID pair embedded in Android release builds
PLAY_TRACKOptional Google Play track; defaults to internal
ALLOW_NO_UPDATEREmergency desktop override; true permits release artifacts without updater signing
PUBLISH_SNAP_STOREOptional; set to true after Snap Store credentials and listing approval are ready
PUBLISH_FDROID_REPOSITORYOptional; set to true after F-Droid key and GitHub Pages are configured
PUBLISH_HOMEBREW_TAPOptional; set to true after the separate Homebrew tap and token are configured

ALLOW_NO_UPDATER is a break-glass repository variable, not a normal release setting. Leave it unset so desktop releases fail closed when TAURI_SIGNING_PRIVATE_KEY is missing.

For generation steps, storage locations, required/optional status, and exact setup instructions, see docs/SECRETS.md and the distribution publishing guide.

Cloudflare API Token Setup

Create a token at My Profile → API Tokens → Create Token → Create Custom Token:

Permissions:

ScopePermissionAccess
UserUser DetailsRead
AccountWorkers ScriptsEdit
ZoneCache PurgePurge

Account Resources:

  • Select Include → Specific account → [Your Account]
  • Or Include → All accounts (if you have only one)

Zone Resources:

  • Select Include → Specific zone → [Your Domain]
  • Or Include → All zones

Common mistake: Setting permissions but leaving Account/Zone Resources as "All accounts from..." dropdown without explicitly selecting. You must click and select your specific account/zone.

Worker Setup

The CDN worker (worker/) handles:

  1. SPA Routing — Returns index.html for navigation requests to /mailbox, /calendar, /contacts, /login
  2. Cache Headers — Sets correct Cache-Control per asset type
  3. Security HeadersX-Content-Type-Options, X-Frame-Options

After first deployment, configure the custom domain:

  1. Cloudflare Dashboard → Workers & Pages → webmail-cdn
  2. Settings → Triggers → Add Custom Domain
  3. Enter your domain (e.g., mail.example.com)

Manual Deployment

# Build the app
pnpm build

# Deploy to R2 (requires AWS CLI configured with R2 credentials)
aws --endpoint-url "https://ACCOUNT_ID.r2.cloudflarestorage.com" \
    s3 sync dist/ "s3://BUCKET_NAME/" --delete

# Deploy worker
cd worker
pnpm install
npx wrangler deploy

# Purge Cloudflare cache
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
    -H "Authorization: Bearer API_TOKEN" \
    -H "Content-Type: application/json" \
    --data '{"purge_everything":true}'

Troubleshooting

Stale assets after deploy:

  • Verify cache purge succeeded in GitHub Actions logs
  • Check browser DevTools → Network → Disable cache and refresh
  • Users with disk-cached HTML may need to clear browser cache or wait for the fallback recovery UI

SPA routes return 404:

  • Ensure the worker is deployed and bound to your domain
  • Check worker logs: cd worker && npx wrangler tail

Service worker not updating:

  • Check version.json is being fetched fresh (no cache)
  • Verify sw.js has no-cache header in Network tab

License

Business Source License 1.1 - Forward Email LLC

Contributors

shaunwarman

519 commits

titanism

169 commits

forwardemail/mail.forwardemail.net

Open-Source & Privacy-focused Webmail: A privacy-first, offline-capable Progressive Web App for Forward Email. Ships as static assets and runs entirely in the browser with local caching, full-text search, and multi-account support.

35

stars

731

commits

JavaScript

primary language

Sep 9, 2026

updated

mail.forwardemail.net
app
caching
javascript
js
mail
node
nodejs
offline
pwa
service-worker
webmail

README

Forward Email - Webmail, Desktop, and Mobile

This is the official, open-source, and end-to-end encrypted webmail client for Forward Email. It is available as a fast and modern web app, a cross-platform desktop app for Windows, macOS, and Linux, and a native mobile app for iOS and Android.

Table of Contents

Downloads & Releases

Official desktop and Android artifacts are published on the GitHub Releases page. Desktop builds are produced by the public GitHub Actions release workflows, and the desktop release matrix now targets macOS arm64/x64, Windows x64/arm64, and Linux x64/arm64.

Snap Store Flathub GitHub Release

PlatformArchitectureDownloadStore
Webmail.forwardemail.net
Windowsx64.msi / .exe on GitHub Releases
Windowsarm64-setup.exe on GitHub Releases
macOSApple Silicon & Intel.dmg on GitHub ReleasesApp Store (Coming Soon)
Linuxx64.deb / .AppImage / .rpm on GitHub Releases
Linuxarm64.deb / .rpm on GitHub Releases
Linuxx64 / arm64snap install forwardemail-mail after publicationSnap Store
Linuxx64 / arm64flatpak install flathub net.forwardemail.mail after publicationFlathub
AndroidUniversalDual-provider .apk / .aab on GitHub ReleasesGoogle Play (Coming Soon)
AndroidGoogle-freeforwardemail-mail_<version>_fdroid.apk on GitHub ReleasesSelf-hosted F-Droid repository / Obtainium
iOSarm64TestFlight / App Store distributionApp Store (Coming Soon)

Linux package managers

Snap Store

After the Snap Store listing is live, install the strict-confinement package with:

sudo snap install forwardemail-mail

Snap updates are managed by the operating system. To inspect the installed channel and version, run snap info forwardemail-mail.

Flathub

After the Flathub page is live, install and run the sandboxed Flatpak with:

flatpak install flathub net.forwardemail.mail
flatpak run net.forwardemail.mail

Flatpak manages updates for this installation; the in-app GitHub updater is intentionally disabled in the Flatpak build. If your system has not added Flathub yet, follow the Flathub setup instructions first.

Android without Google Play

F-Droid-compatible repository

The official repository publishes the Google-free UnifiedPush-only APK. After the repository is live, add this URL to an F-Droid-compatible client:

https://forwardemail.github.io/mail.forwardemail.net/fdroid/repo

Before trusting the repository, compare the fingerprint displayed by the client with the official value at the published fingerprint file. The URL and fingerprint are not interchangeable: verify both before installing. The repository is self-hosted and is not part of the public f-droid.org catalog.

Obtainium

Obtainium can track the Google-free APK directly from GitHub Releases. In Obtainium, choose Add App, enter the repository URL below, select the GitHub source if prompted, and choose the asset ending in _fdroid.apk:

https://github.com/forwardemail/mail.forwardemail.net

Install a compatible UnifiedPush distributor before turning on background notifications. Obtainium will then discover future Google-free release assets automatically.

Ubuntu / Debian installation

For Ubuntu x64 / amd64, download the current .deb asset from GitHub Releases, then install it with:

sudo apt update
sudo apt install ./Forward.Email_<version>_amd64.deb

For Ubuntu arm64 / aarch64, install the matching arm64 .deb the same way:

sudo apt update
sudo apt install ./Forward.Email_<version>_arm64.deb

For a portable Linux x64 / amd64 binary, download Forward.Email_<version>_amd64.AppImage from GitHub Releases, make it executable, and run it directly. The release matrix does not publish an arm64 AppImage; Linux arm64 users should install the .deb or .rpm asset instead.

chmod +x Forward.Email_<version>_amd64.AppImage
./Forward.Email_<version>_amd64.AppImage

If you are building your own custom Linux binary instead of installing a published release, use the desktop development guide in docs/desktop-setup.md.

Note for macOS users: If you download the .dmg from GitHub Releases, you may need to run the following command if you see a "damaged" or unverified app error:

sudo xattr -rd com.apple.quarantine "/Applications/Forward Email.app"

Replace /Applications/Forward Email.app with the actual path if you installed the app elsewhere.

Screenshots

Screenshots as of August 27, 2026.

These screenshots are captured automatically from the production Demo Account after each successful release. Expand a theme and device group to browse its views.

Dark mode — Desktop
ViewScreenshot
login pageDark mode login page
mail viewDark mode mail view
message viewDark mode message view
compose windowDark mode compose window
contacts pageDark mode contacts page
calendar pageDark mode calendar page
profile pageDark mode profile page
diagnostics pageDark mode diagnostics page
general settings pageDark mode general settings page
appearance settings pageDark mode appearance settings page
privacy and security pageDark mode privacy and security page
folders and labels settings pageDark mode folders and labels settings page
search settings pageDark mode search settings page
advanced settings pageDark mode advanced settings page
keyboard shortcuts pageDark mode keyboard shortcuts page
about and help pageDark mode about and help page
Dark mode — Mobile
ViewScreenshot
login pageDark mode mobile login page
mail viewDark mode mobile mail view
message viewDark mode mobile message view
compose windowDark mode mobile compose window
contacts pageDark mode mobile contacts page
calendar pageDark mode mobile calendar page
profile pageDark mode mobile profile page
diagnostics pageDark mode mobile diagnostics page
general settings pageDark mode mobile general settings page
appearance settings pageDark mode mobile appearance settings page
privacy and security pageDark mode mobile privacy and security page
folders and labels settings pageDark mode mobile folders and labels settings page
search settings pageDark mode mobile search settings page
advanced settings pageDark mode mobile advanced settings page
keyboard shortcuts pageDark mode mobile keyboard shortcuts page
about and help pageDark mode mobile about and help page
Light mode — Desktop
ViewScreenshot
login pageLight mode login page
mail viewLight mode mail view
message viewLight mode message view
compose windowLight mode compose window
contacts pageLight mode contacts page
calendar pageLight mode calendar page
profile pageLight mode profile page
diagnostics pageLight mode diagnostics page
general settings pageLight mode general settings page
appearance settings pageLight mode appearance settings page
privacy and security pageLight mode privacy and security page
folders and labels settings pageLight mode folders and labels settings page
search settings pageLight mode search settings page
advanced settings pageLight mode advanced settings page
keyboard shortcuts pageLight mode keyboard shortcuts page
about and help pageLight mode about and help page
Light mode — Mobile
ViewScreenshot
login pageLight mode mobile login page
mail viewLight mode mobile mail view
message viewLight mode mobile message view
compose windowLight mode mobile compose window
contacts pageLight mode mobile contacts page
calendar pageLight mode mobile calendar page
profile pageLight mode mobile profile page
diagnostics pageLight mode mobile diagnostics page
general settings pageLight mode mobile general settings page
appearance settings pageLight mode mobile appearance settings page
privacy and security pageLight mode mobile privacy and security page
folders and labels settings pageLight mode mobile folders and labels settings page
search settings pageLight mode mobile search settings page
advanced settings pageLight mode mobile advanced settings page
keyboard shortcuts pageLight mode mobile keyboard shortcuts page
about and help pageLight mode mobile about and help page

Security & Privacy

Security is the foundational principle of this application. We are committed to transparency and providing users with control over their data. For a detailed overview of our security practices, please see:

Client-Side Encryption & App Lock

The application offers a robust App Lock feature that enables cryptographic encryption for your entire client-side database and settings — in the browser, on desktop, or on mobile. When enabled from the Settings > Privacy & Security menu, all sensitive data stored locally (including message bodies, contacts, and API tokens) is encrypted at rest using the XSalsa20-Poly1305 stream cipher from the audited libsodium library.

This feature can be secured using two methods:

  1. Passkey (WebAuthn): For the highest level of security, you can lock and unlock the application using a FIDO2/WebAuthn-compliant authenticator. This allows you to use hardware security keys or your device's built-in biometrics. The encryption key is derived directly from the authenticator using the PRF extension, meaning the key is never stored on the device itself.
  2. PIN Code: For convenience, you can set a simple PIN code. This provides an iOS-like lock screen experience, ideal for quick access on mobile devices.

Our implementation supports a wide range of authenticators for Passkey-based App Lock:

TypeExamples
Platform AuthenticatorsApple Touch ID, Face ID, Optic ID, Windows Hello, Android Biometrics (fingerprint, face)
Hardware Security KeysYubiKey 5 Series, YubiKey Bio, Google Titan, Feitian ePass/BioPass, SoloKeys, Nitrokey 3, HID Crescendo, Ledger
Cloud/Software PasskeysiCloud Keychain, Google Password Manager, Samsung Pass, 1Password, Dashlane, Bitwarden, Proton Pass

Tamper-Proof Builds

All builds are handled by public GitHub Actions workflows directly from the source code. Desktop applications are signed with platform-specific certificates (Apple Developer ID and Windows Authenticode), and the Tauri updater uses Ed25519 signatures to verify every update package. Mailbox content and WebSocket updates travel directly between the app and Forward Email. Platform delivery infrastructure is used only where the operating system requires it: APNs on iOS, FCM or a user-selected UnifiedPush distributor on Android, and GitHub Releases for desktop update packages. Forward Email does not add advertising, analytics, or tracking intermediaries to those paths.

Features

  • Blazing Fast: Built with Rust and Svelte 5 for a lightweight and responsive experience.
  • End-to-End Encrypted: Cryptographic encryption for your entire client-side app — browser, desktop, or mobile.
  • Open-Source: All code for the web, desktop, and mobile apps is available on GitHub.
  • No Advertising or Tracking Intermediaries: Mailbox data and real-time WebSocket updates stay between the app and Forward Email. Platform push providers and release hosting are used only to deliver native notifications and signed application updates.
  • Real-time Updates: Mailbox updates are pushed instantly via WebSockets.
  • Cross-Platform Notifications: Native desktop and mobile push notifications.
  • Multi-account — Login with multiple Forward Email accounts, alias auth, and optional API key override.
  • Mailbox — Folders, message threading, bulk actions, keyboard shortcuts, attachment handling, PGP decryption.
  • Compose — Rich text editor (TipTap), CC/BCC, emoji picker, attachments, draft autosave, offline outbox queue.
  • Search — Full-text search with FlexSearch, optional body indexing, saved searches, background indexing.
  • Offline Support: A custom main-thread sync engine provides offline access and queues outgoing actions, replacing the need for a Service Worker and ensuring functionality on all platforms including Ionic/Capacitor mobile.
  • Calendar — Month/week/day views, quick add/edit/delete, iCal export.
  • Contacts — CRUD operations, vCard import/export, deep links to compose/search.
  • Demo Mode: Evaluate the app's features offline without an account.
  • mailto: Handler: Registers as the default email client on desktop platforms.
  • Auto-Updates: Desktop apps automatically check for and install new versions securely.

Architecture Overview

The application is built on a unified architecture that reuses the same Svelte 5 web application as the UI for all platforms. Tauri v2 provides the cross-platform shell, using a Rust backend for native capabilities and system webviews for rendering the UI.

graph TD
    subgraph "Web App (Svelte 5 + Vite)"
        A[UI Components] --> B(Stores)
        B --> C{API Client}
        C --> D[REST API]
        B --> E{WebSocket Client}
        E --> F[Real-time API]
    end
    subgraph "Tauri (Desktop & Mobile)"
        G[Rust Backend] --> H{System WebView}
        H -- loads --> A
        I[Tauri IPC] -- JS Bridge --> A
        G -- IPC --> I
        J[Tauri Plugins] --> G
    end
    D --- M(api.forwardemail.net)
    F --- M

For more detail, please see the full Architecture Document.

Tech Stack

CategoryTechnologies
WebSvelte 5, Vite, pnpm
Desktop & MobileTauri v2 (Rust backend, Svelte frontend)
StylingTailwind CSS 4, PostCSS
StateSvelte Stores
DatabaseDexie 4 (IndexedDB)
SearchFlexSearch
EditorTipTap 2
Calendarschedule-x
Real-timeWebSocket with msgpackr binary encoding
Encryptionlibsodium-wrappers (XSalsa20-Poly1305, Argon2id), OpenPGP
Passkeys@passwordless-id/webauthn (FIDO2/WebAuthn with PRF extension)
TestingVitest, Playwright for E2E tests, WebdriverIO for Tauri binary tests
ToolingESLint 9, Prettier 3, Husky, commitlint

Key Components

  • Main Thread — Svelte components, stores, routing, UI rendering
  • db.worker — Owns IndexedDB via Dexie, handles all database operations
  • sync.worker — API fetching, message parsing (PostalMime), data normalization
  • search.worker — FlexSearch indexing and query execution

Documentation

Detailed architecture documentation is available in the docs/ directory:

Project Structure

src/
├── main.ts                 # App bootstrap, routing, service worker registration
├── config.ts               # Environment configuration
├── stores/                 # Svelte stores (state management)
│   ├── mailboxStore.ts     # Message list, folders, threading
│   ├── mailboxActions.ts   # Move, delete, flag, label actions
│   ├── messageStore.ts     # Selected message, body, attachments
│   ├── searchStore.ts      # Search queries and index health
│   ├── settingsStore.ts    # User preferences, theme, PGP keys
│   └── ...
├── svelte/                 # Svelte components
│   ├── Mailbox.svelte      # Main email interface
│   ├── Compose.svelte      # Email composer
│   ├── Calendar.svelte     # Calendar view
│   ├── Contacts.svelte     # Contact management
│   ├── Settings.svelte     # User settings
│   └── components/         # Reusable components
├── workers/                # Web Workers
│   ├── db.worker.ts        # IndexedDB operations
│   ├── sync.worker.ts      # API sync and parsing
│   └── search.worker.ts    # Search indexing
├── utils/                  # Utilities
│   ├── remote.js           # API client
│   ├── db.js               # Database initialization
│   ├── storage.js          # LocalStorage management
│   └── ...
├── lib/components/ui/      # UI component library (shadcn/ui)
├── styles/                 # CSS (Tailwind + custom)
├── locales/                # i18n translations
└── types/                  # TypeScript definitions

Getting Started

Prerequisites

Installation

pnpm install

Development

pnpm dev              # Start web dev server (http://localhost:5174)
pnpm tauri dev        # Start desktop dev mode
pnpm tauri:android:dev  # Start Android dev mode (preflight checks + adb setup)
pnpm tauri:ios:dev      # Start iOS dev mode (preflight checks + simulator boot)

Build

pnpm build        # Build to dist/ + generate service worker
pnpm preview      # Preview production build locally
pnpm analyze      # Build with bundle analyzer

Code Quality

pnpm lint         # Run ESLint
pnpm lint:fix     # Fix linting issues
pnpm format       # Check formatting
pnpm format:fix   # Fix formatting
pnpm check        # Run svelte-check

Testing

# Unit tests (Vitest)
pnpm test              # Run all tests
pnpm test:watch        # Watch mode
pnpm test:coverage     # Generate coverage report

# E2E tests (Playwright)
pnpm exec playwright install --with-deps  # First-time setup
pnpm test:e2e          # Run e2e tests

Contributing

Commit Messages

This project uses Conventional Commits enforced by commitlint. Every commit message must follow the format:

type(scope): description
TypeWhen to useVersion bump
featNew featureminor
fixBug fixpatch
docsDocumentation onlynone
refactorCode change that neither fixes nor addsnone
perfPerformance improvementpatch
testAdding or updating testsnone
choreBuild, CI, tooling changesnone

Scope is optional: fix(compose): handle pasted recipients or fix: handle pasted recipients are both valid.

To trigger a major version bump, add a BREAKING CHANGE: footer:

feat: redesign settings page

BREAKING CHANGE: settings store schema changed, requires cache clear

Releasing

Releases are managed locally using np. Version bumps still flow through pnpm release, and desktop artifact publishing is handled by the Tauri desktop release workflow plus the pnpm release:desktop helper for desktop-only hotfixes.

pnpm release            # interactive version prompt, runs checks, pushes, publishes GitHub Release

This command will:

  1. Verify a clean working tree and up-to-date main branch
  2. Run lint, format, tests, and build
  3. Bump the version in package.json and create a git tag
  4. Push the commit and tag to GitHub
  5. Publish a GitHub Release

For desktop releases, pushing a desktop tag or running the workflow manually triggers Release Desktop (Tauri) (.github/workflows/release-desktop.yml), which creates or updates a draft GitHub Release and uploads the macOS x64/arm64, Windows x64/arm64, and Linux x64/arm64 desktop artifacts.

Configuration

Create a .env file to override defaults:

# API base URL (Vite requires VITE_ prefix for client exposure)
VITE_WEBMAIL_API_BASE=https://api.forwardemail.net

Deployment

First time setup? See the complete Deployment Checklist for step-by-step instructions on Cloudflare, GitHub Actions, and DNS configuration.

Infrastructure

graph TB
    subgraph Edge["Cloudflare Edge"]
        subgraph Worker["Cloudflare Worker"]
            W1["SPA routing (returns index.html for /mailbox, etc)"]
            W2["Cache headers (immutable for assets, no-cache HTML)"]
        end
        Worker --> R2
        subgraph R2["Cloudflare R2"]
            R2A["Static assets (dist/)"]
            R2B["Fingerprinted bundles (/assets/*.js, *.css)"]
        end
    end

Cache Strategy

Asset TypeCache-ControlReason
index.html, /mailbox, /calendar, etc.no-cache, no-storeAlways fetch fresh HTML for updates
/assets/* (JS, CSS)immutable, max-age=31536000Fingerprinted by Vite, safe to cache forever
sw.js, sw-*.js, version.jsonno-cache, must-revalidateService worker must check for updates
/icons/*max-age=259200030 days, rarely change
Fonts (.woff2)immutable, max-age=31536000Fingerprinted, cache forever

CI/CD Pipeline

The top-level Release workflow (.github/workflows/release.yml) is the production orchestrator for every v* tag. It runs the WebView E2E gate, creates the draft GitHub Release, calls the reusable desktop and mobile workflows, deploys the web application to Cloudflare R2 and Workers, publishes the release, generates checksums, and optionally sends a Matrix notification.

The orchestrator calls two reusable platform workflows:

  1. Release Desktop (Tauri) (.github/workflows/release-desktop.yml) builds macOS x64/arm64, Windows x64/arm64, and Linux x64/arm64 desktop artifacts. See Desktop Build CI guide for the platform matrix, runner details, and artifact expectations.
  2. Release Mobile (.github/workflows/release-mobile.yml) builds the signed dual-provider Android APK/AAB and the signed iOS IPA, then uploads configured store artifacts. See Release Process, iOS Setup, and Secrets for the exact signing inputs and release flow.

The separate deploy.yml workflow is a manual workflow_dispatch recovery path for redeploying the current web build; normal tagged releases deploy inline from release.yml.

Local validation for release work should still cover the standard web checks before tagging a release:

  1. Installpnpm install --frozen-lockfile
  2. Lintpnpm lint
  3. Formatpnpm format
  4. Unit testspnpm test -- --run
  5. Buildpnpm build
  6. Desktop build smoke testpnpm tauri:build for the target platform you are validating

For exact secret generation, GitHub environment setup, and platform-specific signing steps, use docs/SECRETS.md as the canonical guide, with docs/desktop-ci-secrets.md and docs/ios-setup.md as platform-specific companions.

Required Secrets & Variables

GitHub Secrets:

SecretDescription
R2_ACCOUNT_IDCloudflare account ID (also used for Workers)
R2_ACCESS_KEY_IDR2 API access key
R2_SECRET_ACCESS_KEYR2 API secret key
CLOUDFLARE_ZONE_IDZone ID for cache purge
CLOUDFLARE_API_TOKENAPI token with R2 + Workers + cache-purge permissions
TAURI_SIGNING_PRIVATE_KEYTauri updater Ed25519 signing key
TAURI_SIGNING_PRIVATE_KEY_PASSWORDPassword for the Tauri signing key
APPLE_CERTIFICATEBase64-encoded macOS .p12 signing certificate
APPLE_CERTIFICATE_PASSWORDPassword used to export the macOS .p12
APPLE_SIGNING_IDENTITYApple Developer ID signing identity
APPLE_IDApple ID for notarization
APPLE_PASSWORDApp-specific password for notarization
APPLE_TEAM_IDApple Developer Team ID
WINDOWS_CERTIFICATEBase64-encoded exportable Windows .pfx code-signing certificate
WINDOWS_CERTIFICATE_PASSWORDPassword used to export the Windows .pfx
ANDROID_KEYSTORE_BASE64Android signing keystore (base64)
ANDROID_KEYSTORE_PASSWORDPassword for the Android keystore
ANDROID_KEY_ALIASAndroid signing key alias
ANDROID_KEY_PASSWORDPassword for the Android signing key
IOS_CERTIFICATE_BASE64Base64-encoded iOS Apple Distribution .p12
IOS_CERTIFICATE_PASSWORDPassword used to export the iOS .p12
IOS_PROVISIONING_PROFILE_BASE64Base64-encoded App Store provisioning profile
APP_STORE_CONNECT_API_KEYFull contents of the downloaded App Store Connect .p8 key
APP_STORE_CONNECT_KEY_IDApp Store Connect API key ID
APP_STORE_CONNECT_ISSUER_IDApp Store Connect issuer ID
GOOGLE_SERVICES_JSON_BASE64Firebase Android client configuration for the dual-provider build
GOOGLE_PLAY_SERVICE_ACCOUNTOptional Google Play publishing service-account JSON
MATRIX_TOKENOptional repository secret for release and activity notifications
SNAPCRAFT_STORE_CREDENTIALSOptional release secret enabling Snap Store stable-channel publishing
FDROID_KEYSTORE_BASE64 / FDROID_KEYSTORE_PASSWORDOptional release secrets used to sign the self-hosted F-Droid repository index
HOMEBREW_TAP_TOKENOptional release secret limited to creating cask pull requests in the first-party tap

All secrets above except MATRIX_TOKEN belong in the release environment. MATRIX_TOKEN is a repository Actions secret because the notification jobs do not attach the release environment. GITHUB_TOKEN is supplied automatically by GitHub Actions and must not be created manually.

GitHub Variables:

VariableDescription
R2_BUCKETR2 bucket name for static assets
IOS_SIGNING_IDENTITYOptional iOS signing identity override; defaults to Apple Distribution
VAPID_PUBLIC_KEYRequired public half of the backend VAPID pair embedded in Android release builds
PLAY_TRACKOptional Google Play track; defaults to internal
ALLOW_NO_UPDATEREmergency desktop override; true permits release artifacts without updater signing
PUBLISH_SNAP_STOREOptional; set to true after Snap Store credentials and listing approval are ready
PUBLISH_FDROID_REPOSITORYOptional; set to true after F-Droid key and GitHub Pages are configured
PUBLISH_HOMEBREW_TAPOptional; set to true after the separate Homebrew tap and token are configured

ALLOW_NO_UPDATER is a break-glass repository variable, not a normal release setting. Leave it unset so desktop releases fail closed when TAURI_SIGNING_PRIVATE_KEY is missing.

For generation steps, storage locations, required/optional status, and exact setup instructions, see docs/SECRETS.md and the distribution publishing guide.

Cloudflare API Token Setup

Create a token at My Profile → API Tokens → Create Token → Create Custom Token:

Permissions:

ScopePermissionAccess
UserUser DetailsRead
AccountWorkers ScriptsEdit
ZoneCache PurgePurge

Account Resources:

  • Select Include → Specific account → [Your Account]
  • Or Include → All accounts (if you have only one)

Zone Resources:

  • Select Include → Specific zone → [Your Domain]
  • Or Include → All zones

Common mistake: Setting permissions but leaving Account/Zone Resources as "All accounts from..." dropdown without explicitly selecting. You must click and select your specific account/zone.

Worker Setup

The CDN worker (worker/) handles:

  1. SPA Routing — Returns index.html for navigation requests to /mailbox, /calendar, /contacts, /login
  2. Cache Headers — Sets correct Cache-Control per asset type
  3. Security HeadersX-Content-Type-Options, X-Frame-Options

After first deployment, configure the custom domain:

  1. Cloudflare Dashboard → Workers & Pages → webmail-cdn
  2. Settings → Triggers → Add Custom Domain
  3. Enter your domain (e.g., mail.example.com)

Manual Deployment

# Build the app
pnpm build

# Deploy to R2 (requires AWS CLI configured with R2 credentials)
aws --endpoint-url "https://ACCOUNT_ID.r2.cloudflarestorage.com" \
    s3 sync dist/ "s3://BUCKET_NAME/" --delete

# Deploy worker
cd worker
pnpm install
npx wrangler deploy

# Purge Cloudflare cache
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
    -H "Authorization: Bearer API_TOKEN" \
    -H "Content-Type: application/json" \
    --data '{"purge_everything":true}'

Troubleshooting

Stale assets after deploy:

  • Verify cache purge succeeded in GitHub Actions logs
  • Check browser DevTools → Network → Disable cache and refresh
  • Users with disk-cached HTML may need to clear browser cache or wait for the fallback recovery UI

SPA routes return 404:

  • Ensure the worker is deployed and bound to your domain
  • Check worker logs: cd worker && npx wrangler tail

Service worker not updating:

  • Check version.json is being fetched fresh (no cache)
  • Verify sw.js has no-cache header in Network tab

License

Business Source License 1.1 - Forward Email LLC

Contributors

shaunwarman

519 commits

titanism

169 commits

Languages

JavaScript

26.9%

TypeScript

25.5%

Svelte

21.6%

HTML

20.2%

Rust

2.0%

Kotlin

1.4%

CSS

1.2%