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.
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.
| Platform | Architecture | Download | Store |
|---|---|---|---|
| Web | — | mail.forwardemail.net | — |
| Windows | x64 | .msi / .exe on GitHub Releases |
— |
| Windows | arm64 | -setup.exe on GitHub Releases |
— |
| macOS | Apple Silicon & Intel | .dmg on GitHub Releases |
App Store (Coming Soon) |
| Linux | x64 | .deb / .AppImage / .rpm on GitHub Releases |
— |
| Linux | arm64 | .deb / .AppImage / .rpm on GitHub Releases |
— |
| Android | Universal | .apk / .aab on GitHub Releases |
Google Play (Coming Soon) |
| iOS | arm64 | TestFlight / App Store distribution | App Store (Coming Soon) |
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.debFor Ubuntu arm64 / aarch64, install the matching arm64 .deb the same way:
sudo apt update
sudo apt install ./Forward.Email_<version>_arm64.debIf you prefer a portable binary, download the matching .AppImage for your architecture from GitHub Releases, make it executable, and run it directly:
chmod +x Forward.Email_<version>_<arch>.AppImage
./Forward.Email_<version>_<arch>.AppImageReplace <arch> with amd64 or arm64 to match the asset you downloaded. 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
.dmgfrom 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/ForwardEmail.appReplace
/Applications/ForwardEmail.appwith the actual path if you installed the app elsewhere.
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:
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:
- 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.
- 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:
| Type | Examples |
|---|---|
| Platform Authenticators | Apple Touch ID, Face ID, Optic ID, Windows Hello, Android Biometrics (fingerprint, face) |
| Hardware Security Keys | YubiKey 5 Series, YubiKey Bio, Google Titan, Feitian ePass/BioPass, SoloKeys, Nitrokey 3, HID Crescendo, Ledger |
| Cloud/Software Passkeys | iCloud Keychain, Google Password Manager, Samsung Pass, 1Password, Dashlane, Bitwarden, Proton Pass |
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 EV Code Signing) and the Tauri updater uses Ed25519 signatures to verify the integrity of every update package. All auto-updates, notifications, and IDLE-like real-time push support are handled directly between the app and our servers with zero third-party involvement.
- 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.
- Zero Third-Party Involvement: Auto-updates, notifications, and real-time IDLE-like push support are handled directly by the app with no third-party servers or services involved.
- 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.
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.
| Category | Technologies |
|---|---|
| Web | Svelte 5, Vite, pnpm |
| Desktop & Mobile | Tauri v2 (Rust backend, Svelte frontend) |
| Styling | Tailwind CSS 4, PostCSS |
| State | Svelte Stores |
| Database | Dexie 4 (IndexedDB) |
| Search | FlexSearch |
| Editor | TipTap 2 |
| Calendar | schedule-x |
| Real-time | WebSocket with msgpackr binary encoding |
| Encryption | libsodium-wrappers (XSalsa20-Poly1305, Argon2id), OpenPGP |
| Passkeys | @passwordless-id/webauthn (FIDO2/WebAuthn with PRF extension) |
| Testing | Vitest, Playwright for E2E tests, WebdriverIO for Tauri binary tests |
| Tooling | ESLint 9, Prettier 3, Husky, commitlint |
- 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
Detailed architecture documentation is available in the docs/ directory:
- Architecture — Full architecture document
- Vision & Architecture — Design principles and architectural patterns
- Worker Architecture — Worker responsibilities and message passing
- Cache & Indexing — Storage layers and data flow
- Search — FlexSearch setup and query parsing
- Service Worker — Asset caching strategy
- DB Schema & Recovery — Database management
- Desktop Build CI — How the desktop build is triggered and tested
- Desktop CI Secrets — CI secrets setup for desktop signing
- Desktop Contributing — Desktop architecture and IPC patterns
- Desktop Setup — Developer environment setup for desktop
- Desktop & Mobile Development — Platform-specific development guide
- iOS Setup — Local signing, CI, and TestFlight workflow
- Release Process — How releases are managed
- Security Hardening — Security practices and hardening
- App Lock Architecture — Client-side encryption and App Lock design
- Push Notifications — Push notification setup
- WebSocket — Real-time WebSocket protocol
- Tauri Testing — Testing Tauri desktop/mobile apps
- Secrets — Secrets management for CI/CD
- Workers — Worker mesh architecture
- Technology Stack — Technology choices and rationale
- Mailbox Loading Flow — Full request lifecycle for loading messages
- Clear-Site-Data — Client reset kill switch specification
- Deployment Checklist — Step-by-step deployment guide
- Building Webmail Series — Technical deep-dive blog series overview
- Vision Gap Analysis — Gap analysis between vision spec and implementation
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
- Node.js 20+
- pnpm 9.0.0+
- Rust and Tauri v2 prerequisites (for desktop/mobile development)
pnpm installpnpm dev # Start web dev server (http://localhost:5174)
pnpm tauri dev # Start desktop dev mode
pnpm tauri android dev # Start Android dev mode
pnpm tauri ios dev # Start iOS dev modepnpm build # Build to dist/ + generate service worker
pnpm preview # Preview production build locally
pnpm analyze # Build with bundle analyzerpnpm lint # Run ESLint
pnpm lint:fix # Fix linting issues
pnpm format # Check formatting
pnpm format:fix # Fix formatting
pnpm check # Run svelte-check# 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 testsThis project uses Conventional Commits enforced by commitlint. Every commit message must follow the format:
type(scope): description
| Type | When to use | Version bump |
|---|---|---|
feat |
New feature | minor |
fix |
Bug fix | patch |
docs |
Documentation only | none |
refactor |
Code change that neither fixes nor adds | none |
perf |
Performance improvement | patch |
test |
Adding or updating tests | none |
chore |
Build, CI, tooling changes | none |
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
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 ReleaseThis command will:
- Verify a clean working tree and up-to-date
mainbranch - Run lint, format, tests, and build
- Bump the version in
package.jsonand create a git tag - Push the commit and tag to GitHub
- 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.
Create a .env file to override defaults:
# API base URL (Vite requires VITE_ prefix for client exposure)
VITE_WEBMAIL_API_BASE=https://api.forwardemail.netFirst time setup? See the complete Deployment Checklist for step-by-step instructions on Cloudflare, GitHub Actions, and DNS configuration.
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
| Asset Type | Cache-Control | Reason |
|---|---|---|
index.html, /mailbox, /calendar, etc. |
no-cache, no-store |
Always fetch fresh HTML for updates |
/assets/* (JS, CSS) |
immutable, max-age=31536000 |
Fingerprinted by Vite, safe to cache forever |
sw.js, sw-*.js, version.json |
no-cache, must-revalidate |
Service worker must check for updates |
/icons/* |
max-age=2592000 |
30 days, rarely change |
Fonts (.woff2) |
immutable, max-age=31536000 |
Fingerprinted, cache forever |
This repository currently documents two primary release workflows:
- Release Desktop (Tauri) (
.github/workflows/release-desktop.yml) for 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. - Release Mobile (
.github/workflows/release-mobile.yml) for signed Android APK/AAB output and signed iOS IPA upload to TestFlight. See Release Process, iOS Setup, and Secrets for the exact signing inputs and release flow.
Local validation for release work should still cover the standard web checks before tagging a release:
- Install —
pnpm install --frozen-lockfile - Lint —
pnpm lint - Format —
pnpm format - Unit tests —
pnpm test -- --run - Build —
pnpm build - Desktop build smoke test —
pnpm tauri:buildfor 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.
GitHub Secrets:
| Secret | Description |
|---|---|
R2_ACCOUNT_ID |
Cloudflare account ID (also used for Workers) |
R2_ACCESS_KEY_ID |
R2 API access key |
R2_SECRET_ACCESS_KEY |
R2 API secret key |
CLOUDFLARE_ZONE_ID |
Zone ID for cache purge |
CLOUDFLARE_API_TOKEN |
API token with R2 + Workers + cache-purge permissions |
TAURI_SIGNING_PRIVATE_KEY |
Tauri updater Ed25519 signing key |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
Password for the Tauri signing key |
APPLE_CERTIFICATE |
Base64-encoded macOS .p12 signing certificate |
APPLE_CERTIFICATE_PASSWORD |
Password used to export the macOS .p12 |
APPLE_SIGNING_IDENTITY |
Apple Developer ID signing identity |
APPLE_ID |
Apple ID for notarization |
APPLE_PASSWORD |
App-specific password for notarization |
APPLE_TEAM_ID |
Apple Developer Team ID |
WINDOWS_CERTIFICATE |
Base64-encoded exportable Windows .pfx code-signing certificate |
WINDOWS_CERTIFICATE_PASSWORD |
Password used to export the Windows .pfx |
ANDROID_KEYSTORE_BASE64 |
Android signing keystore (base64) |
ANDROID_KEYSTORE_PASSWORD |
Password for the Android keystore |
ANDROID_KEY_ALIAS |
Android signing key alias |
ANDROID_KEY_PASSWORD |
Password for the Android signing key |
IOS_CERTIFICATE_BASE64 |
Base64-encoded iOS Apple Distribution .p12 |
IOS_CERTIFICATE_PASSWORD |
Password used to export the iOS .p12 |
IOS_PROVISIONING_PROFILE_BASE64 |
Base64-encoded App Store provisioning profile |
APP_STORE_CONNECT_API_KEY |
Full contents of the downloaded App Store Connect .p8 key |
APP_STORE_CONNECT_KEY_ID |
App Store Connect API key ID |
APP_STORE_CONNECT_ISSUER_ID |
App Store Connect issuer ID |
GitHub Variables:
| Variable | Description |
|---|---|
R2_BUCKET |
R2 bucket name for static assets |
IOS_SIGNING_IDENTITY |
Optional iOS signing identity override; defaults to Apple Distribution |
For generation steps and exact setup instructions, see docs/SECRETS.md.
Create a token at My Profile → API Tokens → Create Token → Create Custom Token:
Permissions:
| Scope | Permission | Access |
|---|---|---|
| User | User Details | Read |
| Account | Workers Scripts | Edit |
| Zone | Cache Purge | Purge |
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.
The CDN worker (worker/) handles:
- SPA Routing — Returns
index.htmlfor navigation requests to/mailbox,/calendar,/contacts,/login - Cache Headers — Sets correct
Cache-Controlper asset type - Security Headers —
X-Content-Type-Options,X-Frame-Options
After first deployment, configure the custom domain:
- Cloudflare Dashboard → Workers & Pages → webmail-cdn
- Settings → Triggers → Add Custom Domain
- Enter your domain (e.g.,
mail.example.com)
# 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}'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.jsonis being fetched fresh (no cache) - Verify
sw.jshasno-cacheheader in Network tab
Business Source License 1.1 - Forward Email LLC