Empty directory, clear blueprint, one session. JefePass, a local-first password manager. Express/TypeScript backend with SQLite, Chrome Extension (Manifest V3) frontend. Went from empty directory to a buildable, testable, loadable extension in one session — 8 phases planned, 8 phases shipped.
Architecture Decisions
We adapted FreeChat's proven AES-256-GCM + PBKDF2 crypto but split the trust boundary: the backend stores only encrypted blobs and handles session management, while the extension's service worker holds the derived CryptoKey in memory and does all encrypt/decrypt client-side via Web Crypto API. Closing the browser kills the service worker, which clears the key — no persistent secrets on disk beyond the salt and encrypted verifier. Chose opaque session tokens over JWT (no need for claims in a single-user local service) and SQLite over PostgreSQL (no Docker dependency for what's essentially a personal vault).
What Shipped
| Layer | Highlights |
|---|---|
| Backend | 17 API endpoints, Zod validation, auth middleware, origin allowlist, password generator, encrypted backup/restore |
| Extension | Service worker message hub with auto-lock alarm, popup (setup/unlock/search/copy), full-tab vault page (CRUD, generator, backup, settings) |
| Content Scripts | Login form detection, autofill banner via Shadow DOM, form submission capture with save prompt |
| Tests | 32 tests passing — crypto, generator, and full integration lifecycle (setup → create → lock → unlock → delete) |
Issues Overcome
The change-password flow had a chicken-and-egg problem: the backend was generating the new salt server-side, but the client needs the salt to derive the new key and re-encrypt entries before sending them. Fixed by moving salt generation client-side — the extension generates a new salt, derives the new key, re-encrypts all entries and the verifier blob, then sends the complete package to the backend in one atomic transaction. Also wrestled with Vite's multi-entry output paths for the extension — HTML files were landing under dist/src/ instead of dist/, and asset paths defaulted to absolute (/popup/popup.js) which Chrome extensions can't resolve. Fixed with base: './' and moving HTML entry points to the extension root.
What's Next
- Proper branded icons (currently solid-color placeholder PNGs)
- Clipboard auto-clear after configurable timeout
- Real-world autofill testing across major login pages
- Add to Jenkins service launcher and port allocation table