A full-stack notes app inspired by Google Keep rebuilt from scratch with a focus on security, performance, and engineering rigour.
What makes this different: the drag-and-drop is custom-built with no libraries, and the encryption is a direct Web Crypto API implementation, not a wrapper around someone else's abstraction.
Zeronotes is a drag-and-drop notes app where your data is encrypted before it ever leaves your device. The server stores ciphertext: it has no access to your notes.
- Create, edit, pin, and organise notes in a masonry grid
- Drag and drop to reorder (smooth, physics-feel, no jank)
- Notes are end-to-end encrypted: only you can read them
- Full authentication flow with JWT-based auth
Most projects reach for react-beautiful-dnd or dnd-kit. I didn't.
The engine is built on pointer events, CSS transforms, and a position-tracking algorithm that handles reflow in a masonry grid. It matches Google Keep's behaviour (items shift smoothly as you drag, snapping into place on release) without a single drag-and-drop dependency.
This was the original challenge that started the project: can I reproduce this interaction from first principles?
Pointer events → position delta → CSS transform (no layout reflow)
↓
Reorder algorithm → grid reflow on drop
Encryption is implemented directly using the Web Crypto API — no third-party crypto library.
- Key derivation: PBKDF2 with a random salt, derived from the user's password
- Encryption: AES-GCM with a unique IV per note
- What the server sees: ciphertext + encryption metadata (e.g. salt, wrapped key, IVs/version) — never the plaintext, never the key
The key is derived client-side on login and never transmitted. Server compromise does not expose note content.
password + salt → PBKDF2 → CryptoKey
↓
plaintext + IV → AES-GCM encrypt → ciphertext → stored in DB
Monorepo structured around Domain-Driven Design principles:
zeronotes/
├── apps/
│ ├── frontend/ # React + TypeScript frontend
│ └── backend/ # Node.js + Express + TypeScript backend
├── packages/
│ └── shared/ # Shared types, validation schemas
└── turbo.json # Turborepo pipeline
- Frontend: React, TypeScript, Zustand for state, Tailwind CSS for styling
- Backend: Node.js, Express, TypeScript, structured SQL queries (no ORM)
- Auth: JWT via the Jose library with rate limiting
- Monorepo: pnpm workspaces + Turborepo
- Testing: Integration and unit tests covering edge cases — not just happy paths
| Layer | Tech |
|---|---|
| Frontend | React, TypeScript, Zustand, Tailwind CSS |
| Backend | Node.js, Express, TypeScript |
| Database | PostgreSQL (raw SQL) |
| Auth | JWT (Jose) |
| Crypto | Web Crypto API — AES-GCM, PBKDF2 |
| Monorepo | pnpm workspaces, Turborepo |
| Testing | Jest (Backend), Vitest (Frontend) |
# Clone and install
git clone https://github.com/amadeuserras/zeronotes
cd zeronotes
pnpm install
# Set up environment variables
cp apps/backend/.env.example apps/backend/.env
cp apps/backend/.env.test.example apps/backend/.env.test
cp apps/frontend/.env.example apps/frontend/.env
# Run database migrations
pnpm --filter @zeronotes/backend migrate
# Start both apps in dev mode
pnpm devRequires Node.js 18+, pnpm, and a PostgreSQL instance.
I wanted to understand what it actually takes to implement end-to-end encryption correctly; not call a library, but understand the primitives. And I wanted to know if I could reproduce Google Keep's drag-and-drop behaviour from scratch.
Both turned out to be more interesting than I expected. The encryption work pushed me to understand key derivation, IVs, and authenticated encryption properly. The drag-and-drop required thinking outside of the box, and the best solution ended up being building a mathematical grid system that tracks note positions in state and renders them via CSS transforms.
The result is an app I'd actually use and a codebase I'm proud to show.