Every tool in this stack was chosen to serve one goal: a static, client-only webmail app that behaves like a native client.
flowchart LR
subgraph Stack Principles
A["SMALL RUNTIME / BIG CAPABILITY<br/>Push heavy work to workers.<br/>Load features on demand."]
B["LOCAL-FIRST BY DEFAULT<br/>IndexedDB is the source of truth.<br/>The API only supplies deltas."]
C["DETERMINISTIC UPDATES<br/>Static shell. SW controls updates.<br/>No surprises mid-session."]
D["SECURITY AS BASELINE<br/>Sanitize HTML, encrypt secrets,<br/>zero third-party tracking."]
end
flowchart LR
A["Svelte 5<br/>Compile-time reactivity<br/>with runes"] --> B["Vite 5<br/>Fast dev HMR + build<br/>Vendor split"] --> C["Workbox 7<br/>Precache app shell<br/>SW updates"]
| Layer | Tool | Version | Why |
|---|---|---|---|
| Framework | Svelte | ^5.48 | Runes ($state, $derived, $effect), compiled reactivity, tiny runtime |
| Build | Vite | ^5.0 | Sub-second HMR, optimized chunking, ES modules |
| PWA | Workbox | ^7.0 | Reliable precaching, SPA fallback, cache-first |
| Styling | Tailwind CSS | ^4.1 | Utility-first, purged in production |
| Components | shadcn/ui | via Bits UI ^2.15 | Accessible, composable primitives |
| TypeScript | TypeScript | ^5.3 | Strict mode, worker typing |
| Package Mgr | pnpm | ^9.0 | Fast, disk-efficient, strict |
| Node | Node.js | 20 LTS | Stable, long-term support |
flowchart LR
subgraph DATA LAYER
A["Dexie 4<br/>IndexedDB wrapper<br/>13 tables, per-account"]
B["FlexSearch<br/>Full-text search<br/>index, per-account"]
C["Mutation Queue<br/>Offline actions<br/>meta table"]
end
| Component | Tool | Version | Why |
|---|---|---|---|
| Database | Dexie | ^4.2 | Schema layer over IndexedDB, compound keys, migrations |
| Search | FlexSearch | ^0.7 | Fast client-side full-text, persistent indexes |
| HTTP Client | ky | ^1.14 | Lightweight, retry-aware, hooks |
| Email Parsing | PostalMime | ^2.6 | RFC-compliant MIME parsing in workers |
flowchart TD
subgraph Workers & Concurrency
MT["Main Thread<br/>UI + Stores"] --> SW["sync.worker<br/>API fetch, PostalMime, OpenPGP"]
MT --> DW["db.worker<br/>Dexie CRUD, Schema"]
MT --> SEW["search.worker<br/>FlexSearch, Indexing, Queries"]
SVC["+ Service Worker (Workbox) for asset caching"]
end
Every worker communicates via MessageChannel — no shared memory, no
contention, no UI stalls.
| Component | Tool | Version | Why |
|---|---|---|---|
| Rich Text Editor | TipTap | ^2.6 | Extensible, ProseMirror-based, formatting |
| Calendar | Schedule-X | ^1.63 | Month/week/day views, no server rendering |
| Sanitizer | DOMPurify | ^3.1 | XSS protection for HTML email content |
| Encryption | OpenPGP | ^6.2 | Client-side PGP decrypt/encrypt |
| Markdown | marked | ^12.0 | Compose in markdown, render as HTML |
| Emoji | emoji-picker-element | ^1.21 | Native emoji selection |
| Icons | Lucide Svelte | ^0.562 | Tree-shakeable SVG icon library |
| Dates | date-fns | ^3.6 | Lightweight date formatting/parsing |
flowchart LR
subgraph SECURITY LAYERS
A["DOMPurify<br/>HTML email sanitized<br/>before display"]
B["Sandboxed Iframe<br/>Email body rendered<br/>isolated"]
C["OpenPGP<br/>End-to-end encryption<br/>in sync worker"]
D["+ Local-first storage (no server-side UI state)<br/>+ Static hosting (immutable, no server-rendered HTML)<br/>+ Zero third-party tracking<br/>+ CSP headers via Cloudflare Worker"]
end
| Tool | Purpose |
|---|---|
| ESLint 9 + TypeScript | Linting with strict rules |
| Prettier 3 | Consistent formatting |
| Husky + lint-staged | Pre-commit quality gates |
| Vitest 2.1 | Unit tests (jsdom) |
| Playwright 1.57 | E2E browser tests |
| Lighthouse CI | Performance, a11y, best practices (90+) |
| rollup-plugin-visualizer | Bundle analysis (pnpm analyze) |
Vite statically replaces these values at build time via the define option in
vite.config.js. Every occurrence in source code is swapped with the literal
string during compilation — they do not exist at runtime as real environment
variables.
| Variable | Source | Example Value | Purpose |
|---|---|---|---|
import.meta.env.VITE_PKG_VERSION |
package.json → version |
0.0.1 |
Semantic version used for clear-site-data checks |
import.meta.env.VITE_APP_VERSION |
${pkg.version}-${BUILD_HASH} |
0.0.1-a1b2c3d4 |
Full version with build hash for cache busting |
import.meta.env.VITE_BUILD_HASH |
MD5 of ${pkg.version}-${Date.now()} |
a1b2c3d4 |
Unique per-build identifier |
import.meta.env.VITE_WEBMAIL_API_BASE |
.env or CI environment |
https://api.forwardemail.net |
API base URL (set externally, not in define) |
// vite.config.js
const pkg = require('./package.json');
const BUILD_HASH = createHash('md5')
.update(`${pkg.version}-${Date.now()}`)
.digest('hex')
.slice(0, 8);
export default defineConfig({
define: {
'import.meta.env.VITE_APP_VERSION': JSON.stringify(`${pkg.version}-${BUILD_HASH}`),
'import.meta.env.VITE_BUILD_HASH': JSON.stringify(BUILD_HASH),
'import.meta.env.VITE_PKG_VERSION': JSON.stringify(pkg.version),
},
});At build time, Vite replaces source references like:
// Source
const version = import.meta.env.VITE_PKG_VERSION;
// After build
const version = '0.0.1';VITE_PKG_VERSION— Boot-time version check insrc/main.tscompares against a server-sideclear_belowthreshold to trigger cache clearing (see clear-site-data-spec). Also displayed in Settings and sent with feedback reports.VITE_APP_VERSION— Full version identifier for cache busting and diagnostics.VITE_BUILD_HASH— Unique build fingerprint for tracking deployments.
Build-time variables are typed in src/env.d.ts:
interface ImportMetaEnv {
readonly VITE_PKG_VERSION: string;
readonly VITE_APP_VERSION: string;
readonly VITE_BUILD_HASH: string;
}flowchart TD
subgraph PERFORMANCE TARGETS
direction TB
T["Lighthouse: 90+<br/>First Paint: < 1s<br/>Cached Boot: < 200ms<br/>Main Thread Work: Minimal<br/>Bundle (gzipped): Chunked"]
H["HOW WE HIT THEM:<br/>Svelte compiles away the framework<br/>Vendor chunk: svelte, dexie, ky, openpgp, tiptap<br/>Lazy routes: calendar, contacts, compose<br/>Virtual scrolling for message lists<br/>Workers for all CPU-heavy operations"]
end
pnpm dev # Dev server on :5174 with HMR
pnpm build # Production build + SW generation
pnpm preview # Preview production build
pnpm check # Svelte type checking
pnpm analyze # Bundle visualization → dist/stats.html
pnpm test # Unit tests (Vitest)
pnpm test:watch # Watch mode
pnpm test:coverage # Coverage report
pnpm test:e2e # E2E tests (Playwright)
pnpm lint # ESLint check
pnpm lint:fix # ESLint auto-fix
pnpm format # Prettier check
pnpm format:fix # Prettier auto-fixNext: Worker Mesh — the three-worker architecture that keeps the UI at 60fps.