Building a note-taking app is almost a rite of passage for developers. There are already plenty of great options out there, so when I started working on Annota, I wanted to challenge myself with a different set of problems: building something local-first, fully offline-capable, and encrypted by default.
The result is Annota, a security-focused note-taking app with dedicated desktop applications built using Tauri (macOS and Windows) and mobile apps built with React Native + Expo (iOS today, Android coming soon).
One thing I cared about from the beginning was keeping the architecture maintainable as the project grows. The app is split into isolated packages with clear responsibilitiesโfor example, the editor and synchronization engine live independently from each other, making it easier to evolve each system without creating tight coupling.
Over the last few weeks, I documented some of the core architectural decisions behind Annota. Here's a high-level look at three of the biggest challenges I tackled.
๐ก๏ธ End-to-End Encryption
For a privacy-focused application, I wanted the server to know as little as possible about user data.
Everything starts on the user's device:
- A master seed is generated from a 12-word BIP39 recovery phrase.
- That seed is processed with Argon2id to derive a 256-bit master key.
- Using HKDF-SHA256, the master key is split into separate encryption keys for notes and file attachments.
- Before anything is synced, note content and metadata are serialized and encrypted locally using AES-256-GCM.
The server never sees plaintext note content.
๐ Full write-up: Architecture Walkthrough: Annota Encryption
๐ Synchronization
Building a sync engine for a local-first app turned out to be one of the most interesting parts of the project.
The challenge is finding a balance between responsiveness, reliability, and battery usage.
A few things Annota does:
- Sync operations are debounced for 10 seconds after the last edit to avoid excessive network requests.
- A safety timer forces a sync every 2 minutes during long editing sessions so changes don't remain local indefinitely.
- Deleted notes are tracked using tombstones, allowing deletions to propagate correctly across devices.
- File synchronization uses ID-based diffing so devices only download attachments they don't already have.
The goal is simple: keep devices in sync without wasting bandwidth or battery.
๐ Full write-up: Architecture Walkthrough: Annota Synchronization
๐ Public Notes
One challenge I found particularly interesting was public sharing.
If every note is encrypted, how do you allow users to publish selected notes to the web?
The solution was to separate public content from the encrypted note store entirely.
- Published notes are stored in a dedicated database table.
- Limits are enforced at the database level to prevent abuse.
- The web frontend accesses published content through a Supabase Edge Function rather than connecting directly to the database.
- Next.js ISR provides fast page loads, while webhooks immediately invalidate cached pages whenever a note is updated or unpublished.
This allows users to share public notes while keeping the core encrypted storage model intact.
๐ Full write-up: Architecture Walkthrough: Annota Public Notes
โก Hidden Performance Improvements
I recently shifted focus from UI work to optimizing Annota's core engine for scale. The goal was simple: keep the app fast and efficient even with thousands of notes.
Key improvements included:
- Reduced the mobile editor bundle by ~30% through dependency cleanup and lazy loading heavy features.
- Cut note saves from 6 database queries to just 3 by adding intelligent caching and change detection.
- Optimized task list handling to avoid unnecessary database updates when nothing changed.
- Eliminated dozens of redundant sync-related queries by batching operations and improving cache usage.
These changes may be invisible day-to-day, but they significantly reduce CPU usage, database overhead, and battery consumption as note collections grow.
Wrapping Up
Building Annota has been a great opportunity to explore local-first architecture, synchronization challenges, and practical end-to-end encryption. I've learned a lot along the way, and there are still plenty of interesting problems left to solve.
Right now, I am exploring how to securely integrate MCP (Model Context Protocol) Servers into the app. To protect the core privacy model, I already have a Bring-Your-Own-Key (BYOK) system where the local environment can strictly sandbox and expose only explicitly selected notes as context to the providers or local LLMs.
If you're building something in the local-first, offline-first, or privacy-focused space, I'd be interested to hear how you've approached these challenges. Feedback, questions, and architecture discussions are always welcome. Thanks for reading !
Top comments (0)