Encrypted Journal Sync
Build a Flutter journal app where entries are encrypted on the phone before they're stored or sent anywhere. You unlock the journal with a passphrase; the app derives an encryption key from it and encrypts every entry with AES-GCM. Entries are saved in a local Drift (SQLite) database, and only the encrypted text is synced to a server, so the server can never read your journal. Changes made offline are synced later, deleted entries are synced too, and the newest edit wins when the same entry changed in two places. You'll learn key derivation and authenticated encryption with the cryptography package, Drift for local storage, Riverpod for state, and designing a sync protocol with a fake server you control.
- Estimate
- ~14.5h
- Steps
- 7
- Completed by
- 0
- Proposed by
- codeseed.app
Riverpod · Drift
Project roadmap
- 01
Store journal entries with Drift
~2hCreate a Flutter app with flutter_riverpod and drift. Define an entries table with id (UUID), ciphertext (a blob), nonce, updatedAt, deleted (a boolean, used to sync deletions), and dirty (true when the entry changed locally and hasn't been synced). Build a list screen showing entry dates and an editor screen. Done when: entries can be created and edited, and are still there after an app restart.
- 02
Derive a key from the passphrase
~2.5hUse the cryptography package. On first launch, ask for a passphrase twice, generate a random 16-byte salt, and derive a 256-bit key with Argon2id (or PBKDF2 with at least 600,000 iterations). Store only the salt and a small encrypted check value, never the passphrase or the key. Later launches ask for the passphrase once and check it against the stored value. Done when: the right passphrase unlocks the journal and a wrong one is rejected.
- 03
Encrypt every entry
~2hEncrypt each entry's title, text and date as JSON with AES-GCM, using a new random nonce every time you save. Store only the ciphertext and nonce in Drift; the plain text lives in memory and is shown on screen only. Keep the key in a Riverpod provider that is cleared when the app locks. Done when: saved entries show correctly in the app, and the database file contains no readable journal text.
- 04
Lock the journal automatically
~1.5hLock the journal when the app goes to the background for more than 60 seconds (use AppLifecycleListener) and when the user taps a Lock button. Locking clears the key and any decrypted text from memory and returns to the passphrase screen. Done when: after sending the app to the background for a minute and returning, the passphrase is required again.
- 05
Define the sync API with a fake server
~2hDefine two endpoints: POST /sync, which sends the client's dirty entries (id, ciphertext, nonce, updatedAt, deleted), and GET /changes?since=<timestamp>, which returns entries changed on the server since then. The server stores entries as opaque blobs and keeps the newer updatedAt when an entry already exists. Implement it as a fake server with http's MockClient (or a tiny shelf server). Done when: the fake server stores and returns entries following these rules.
- 06
Sync entries
~2.5hWrite a SyncService that uploads dirty entries, clears their dirty flag on success, then downloads changes and applies them: newer versions replace local ones, and deleted entries are removed locally. Remember the time of the last successful sync. Network errors leave entries dirty so they're retried next time. Run a sync on app start and when the user pulls to refresh. Done when: an entry created offline reaches the fake server after syncing, and a deletion syncs too.
- 07
Write tests
~2hWrite tests for: encrypting and decrypting an entry, a wrong passphrase being rejected, a tampered ciphertext failing to decrypt, the newer edit winning a conflict, deletions syncing, and a failed network call leaving entries dirty. Use an in-memory Drift database and the fake server. Done when: flutter test passes.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack