I was about to update my iPhone and wipe it clean. One problem: I had a KeePass (.kdbx) file on the phone that might have passwords I'd added on the go—passwords that didn't exist in the copy on my laptop. I needed a way to compare the two files and see what was different. That's how this project started, almost exactly a year ago.
The Original Problem (February 2025)
KeePass is great for password management, but it doesn't have a built-in way to diff two database files. If you've been editing the same .kdbx file on two devices without syncing, you're stuck manually scrolling through hundreds of entries trying to spot what's different.
I didn't have time for that. I needed to know: does the phone copy have any entries that the laptop copy doesn't? If yes, I'd transfer them over before wiping the phone. If not, I could wipe it without worry.
So I built a quick Node.js tool. It used the kdbxweb library to open both files, compared entries by UUID, and spit out a diff. I added a basic web UI with Express so I could upload the files through a browser. Over three days (Feb 12–15, 2025), I got it working well enough to solve my immediate problem: I could see which entries existed only in one file or the other, and which had been modified.
I solved my problem, backed up the passwords I needed, and moved on. The code sat untouched for nearly a year.
The Speedrun (February 2026)
Fast forward to this past week. Same situation—another iPhone update, another potential password discrepancy—and I pulled up the old tool. It worked, but it was rough. Single-purpose, no transfer capability, no way to actually merge the entries I found. I'd have to manually re-enter them in KeePass.
This time I had my multi-agent Claude Code setup ready to go. I decided to rebuild the whole thing properly, in one sitting.
Here's what happened on the night of February 5–6, 2026:
- Phase 1–2: Extracted all the logic into clean library modules (
lib/) and built a proper bidirectionalDiffEnginethat matches entries by UUID first, then falls back to Title+UserName matching for entries that were created independently on different devices. - Phase 3–4: Rewrote the server with a session-based API (tokens via
X-Session-Tokenheader) and rebuilt the frontend as a tabbed SPA—Compare, Transfer, Duplicates, and Import tabs. - Phase 5–7: Added the features I actually wanted: transferring entries between databases, finding and removing duplicates, and bulk importing with three modes (skip existing, select specific entries, or import all).
- Phase 8: Cleanup, documentation, and security hardening.
Then I kept going. I added CSV import support (so you can import password exports from Chrome, Firefox, or Safari), attachment and history comparison, and group structure preservation so transferred entries land in the right folders.
Total time from first commit to feature-complete: about 12 hours. 35 commits across several feature branches, all squash-merged into main.
What It Does Now
The app has four main features, each in its own tab:
- Compare — Upload two
.kdbxfiles (with passwords and optional key files), get a full bidirectional diff. Entries are categorized as only-in-db1, only-in-db2, modified (with field-level diffs), or identical. Attachments and entry history are compared too. - Transfer — Select specific entries and copy them from one database to the other. Group structure is preserved—if an entry lives in
Internet/Social, it'll be created in the same path in the target database. - Duplicates — Scan a database for duplicate entries (matched by username+URL or title+username). The tool suggests which copies to keep and which to remove.
- Import — Bulk import from one database to another, or from a CSV password export. Three modes: skip entries that already exist, pick specific entries, or import everything.
After any operation, you can download the modified database as a new .kdbx file. Your original files are never modified.
Security Considerations
This is a tool that handles password databases, so security mattered. I ran a security audit using my SmolkAI agent setup and hardened the app across several areas:
- No credentials stored in sessions — the kdbxweb library associates credentials with the database object at load time, so we discard them from the session immediately after.
- Error sanitization — a
safeError()helper logs detailed errors server-side but returns only generic messages to the browser. No stack traces, no file paths leaked. - XSS prevention — the frontend uses DOM APIs (
createElement,textContent) instead ofinnerHTMLfor any user-controlled data like entry titles or UUIDs. - Timing-safe session tokens — session lookup uses
crypto.timingSafeEqual()to prevent timing attacks. - CSRF protection — Origin header validation on all state-changing requests, plus the custom
X-Session-Tokenheader provides implicit CSRF defense. - Upload limits — 10 MB file size cap, max 4 files, extension validation.
- Rate limiting — 30 requests per minute per IP, no external dependencies.
The app runs entirely locally. No data leaves your machine. That was a deliberate design choice—you should never have to upload your password database to someone else's server.
The Tech Stack
Intentionally simple:
- Backend: Node.js + Express, with 9 library modules handling KDBX operations, diffing, transfers, deduplication, and imports.
- Frontend: Vanilla JavaScript SPA, no build step. Static HTML/CSS/JS served directly by Express. Bootstrap-free, custom CSS.
- Key dependency: kdbxweb for reading and writing KDBX files. It handles all the crypto, including Argon2 key derivation (with an optional native module for better performance).
No React, no TypeScript, no Webpack. Just files that do what they say. The entire codebase is about 90 KB of code.
What I Learned
A few takeaways from building this:
- Solve your own problems. The best tools come from genuine need. I didn't set out to build a KeePass utility—I needed to not lose my passwords before wiping a phone.
- AI makes the second pass fast. The original three-day prototype was solid enough to solve the problem. But turning it into a proper multi-feature app with security hardening would have taken much longer without AI assistance. Having specialized agents for architecture, implementation, security auditing, and code review made it possible to do in one night what would normally take a weekend or more.
- UUID matching isn't always enough. When you create the same entry independently on two devices, the UUIDs will be different even though the entry is logically the same. The fallback to Title+UserName matching handles this, but it's an imperfect heuristic. The app flags these fallback matches so you can review them.
- Password tools should be local. There's no good reason for a KDBX comparison tool to require a network connection or cloud service. The whole point of KeePass is keeping your passwords off other people's servers.
Try It Out
The project is open source on GitHub: github.com/smolkapps/kdbx-diff
git clone https://github.com/smolkapps/kdbx-diff.git
cd kdbx-diff
npm install
npm start
Then open http://localhost:3000 in your browser, upload two .kdbx files, and see what's different.
Enjoyed this post?
Get notified when I publish something new. No spam, unsubscribe anytime.