Every tool in this stack serves 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, so there is no shared memory,
no contention, and 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. Compilation swaps every occurrence in source code with the
literal string, so these values 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.