Key Takeaways:
Architecture: Building a truly private application requires executing all cryptographic operations directly on the client's device, treating the cloud strictly as a zero-knowledge synchronization engine.
Cryptography: RozVibe employs AES-256-GCM for robust authenticated encryption and PBKDF2-HMAC-SHA256 (100,000 iterations) to securely derive cryptographic keys from user input.
Searchability: Encrypting data client-side breaks conventional database querying. To solve this, a blind indexing strategy utilizing HMAC-SHA256 token hashes allows for secure, offline-first search via local SQLite.
Development Stack: Leveraging Flutter (Dart 3.3+) and Riverpod allows for rapid cross-platform development while maintaining strict control over volatile memory where encryption keys reside.
Trade-offs: Privacy engineering requires balancing robust security with user experience—acknowledging threats like OS-level compromise while ensuring everyday usability through features like PIN locks and offline caching.

1. The Genesis: The Chilling Effect of Plaintext

As an indie hacker, my foray into privacy app development did not begin with a well-researched business plan or a desire to disrupt a market. It began with personal friction. For years, I had maintained a daily digital diary, documenting my professional hurdles, personal anxieties, and unfiltered thoughts in a popular cloud-based note-taking application. It was an incredibly convenient tool, seamlessly synchronizing my entries across my laptop, tablet, and smartphone.

However, over time, a subtle but profound shift occurred in how I used the application. I found myself pausing before typing certain sentences. I started using euphemisms or avoiding specific topics entirely. I was experiencing the chilling effect of surveillance capitalism in my most private space.

The realization was simple: my data was only encrypted "in transit" and "at rest." To the application provider, my journal was just rows of plaintext in an SQL database. It was accessible to database administrators, vulnerable to external breaches, and subject to opaque terms of service that allowed the platform to scan my content to train machine learning algorithms. When your deepest thoughts are sitting unencrypted on a server halfway across the world, you inevitably begin to self-censor. A journal that you cannot be honest with is inherently useless.

This frustration was the catalyst for my startup journey. I decided to build RozVibe—a privacy-first journaling application designed from the ground up to eliminate the need for server-side trust. My goal was to create a sanctuary where the architecture itself enforced confidentiality, utilizing robust client-side encryption. This article serves as a technical post-mortem and an engineering guide to how I built RozVibe as a solo developer.


2. Choosing the Tech Stack: Flutter & Firebase

As a solo founder, velocity is everything. You cannot afford to maintain separate, siloed codebases for Android, iOS, and the web while simultaneously architecting a complex security model. I needed a framework that allowed for rapid iteration without sacrificing native performance or the ability to implement low-level cryptographic operations.

I chose Flutter (specifically utilizing Dart SDK >=3.3.0). Flutter’s reactive UI model and robust ecosystem made it an ideal candidate. Furthermore, Dart’s evolving ecosystem provided access to reliable cryptographic libraries, allowing me to implement AES and PBKDF2 directly within the client application. For the core journaling experience, I integrated flutter_quill, a powerful package that enables rich-text editing. This allowed me to support complex text formatting, which is crucial for an expressive diary, alongside features like 5 distinct mood states (Radiant, Calm, Neutral, Low, Stormy), a comprehensive Calendar history, and an Emotional Insights dashboard.

For the backend and infrastructure, I selected Firebase. Firebase Authentication provided an immediate, reliable system for user identity (supporting both Email/Password and Google Sign-In), while Cloud Firestore offered a globally distributed NoSQL database with built-in offline caching—a critical feature for a local-first application. However, utilizing Firebase in a privacy-focused context presented a significant architectural challenge.


3. The Firebase Privacy Paradox

Firebase, by its very nature, is a cloud-first synchronization engine. Platforms like Cloud Firestore are designed to ingest plaintext JSON data, index it extensively on Google's servers, and enable rapid, complex querying across massive datasets. The platform's immense power comes from its ability to read, understand, and organize your data.

This paradigm is fundamentally at odds with a zero-knowledge, privacy-first architecture. If I simply pushed my users' journal entries to Firestore as standard JSON documents, I would be replicating the exact problem I set out to solve. Google’s servers would possess the plaintext, and the application would remain a trust-based system rather than a cryptographically secure one.

The solution was strict client-side encryption (sometimes referred to as local-first encryption). I needed to treat Firestore not as a database, but as a "dumb" synchronization pipe. The backend would only ever receive, store, and distribute opaque blobs of ciphertext. All intelligence, indexing, and querying would have to be pushed to the absolute edge of the network: the user's local device.


4. Architecting Client-Side Encryption

Designing an encryption system requires defining a clear threat model. My objective was to protect user data from server-side compromise, malicious database administrators, and data harvesting. It is important to note what this architecture does not protect against: compromised client devices, OS-level malware, or physical coercion. I explicitly avoid marketing terms like "military-grade encryption" because transparency demands honesty about limitations.

4.1 PBKDF2 and Key Derivation

The foundation of the security model is key management. Because we are building a zero-knowledge system, RozVibe cannot store the user's master encryption key on our servers. The key must be generated locally, exist only in volatile memory, and be deterministically re-created whenever the user logs in.

To achieve this, I implemented PBKDF2-HMAC-SHA256 (Password-Based Key Derivation Function 2). While newer algorithms like Argon2 exist and are highly regarded for their memory-hard properties, PBKDF2 remains a NIST-approved standard with exceptional native support across the Dart ecosystem, ensuring cross-platform stability without complex native bindings.

The derivation process requires several components:

  • Derivation Input: A combination of the user's unique Firebase UID and an optional local PIN. The template is ${userId}_${pin ?? "default_secure_vault"}.
  • Cryptographic Salt: A 16-byte random salt generated on the device during account creation via IV.fromSecureRandom(16). This salt is stored locally using FlutterSecureStorage (which wraps Android's EncryptedSharedPreferences) and is synchronized to Firestore. Because a salt is not a secret, syncing it is safe and necessary for multi-device login.
  • Iterations: Set to 100,000 iterations to purposefully slow down the derivation process, providing resistance against offline brute-force attacks.

The PBKDF2 function derives a total of 76 bytes of key material. This long byte stream is subsequently sliced into three distinct cryptographic keys:

  • Bytes 0-31 (32 bytes): The master AES-256 encryption key.
  • Bytes 32-43 (12 bytes): A legacy Initialization Vector (IV) reserved for backward compatibility with older database migrations.
  • Bytes 44-75 (32 bytes): The HMAC-SHA256 search key used for the blind indexing system.
// Simplified Dart implementation of the Key Derivation pipeline
import 'package:pointycastle/export.dart';
Future<Uint8List> deriveKeys(String userId, String? pin, Uint8List salt) async {
 final derivationInput = '\${userId}_\${pin ?? "default_secure_vault"}';
 final inputBytes = utf8.encode(derivationInput);
 final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64))
    ..init(Pbkdf2Parameters(salt, 100000, 76)); // 76 bytes total
return pbkdf2.process(inputBytes);
}
void assignKeyMaterial(Uint8List keyStream) {
  _aesKey = Key(keyStream.sublist(0, 32));     // 256-bit AES Key
  _legacyIv = IV(keyStream.sublist(32, 44));   // 96-bit Legacy IV
  _searchKey = keyStream.sublist(44, 76);      // 256-bit HMAC Key
}

4.2 Encryption using AES-256-GCM

With the 32-byte master key securely resident in RAM, we can handle the actual encryption of the journal payload. For this, RozVibe utilizes AES-256-GCM (Advanced Encryption Standard in Galois/Counter Mode). GCM is critical because it provides Authenticated Encryption with Associated Data (AEAD). It ensures confidentiality (preventing unauthorized reading) and data integrity (preventing unauthorized tampering). If a malicious actor alters a single byte of the ciphertext in Firestore, the GCM authentication tag will fail to validate upon decryption, and the client will reject the payload.

For every single document save operation, a fresh, cryptographically secure 12-byte Initialization Vector is generated using IV.fromSecureRandom(12). Reusing an IV in GCM mode is a catastrophic security failure, so this step is strictly enforced.

The resulting encrypted structure packages the IV alongside the ciphertext and the GCM authentication tag. This unified byte array is encoded into a Base64 string and stored in the Firestore document under the data field. The Firestore document itself contains minimal unencrypted metadata necessary for routing and UI presentation: the userId, a generic date_index, and a boolean isFavorite flag. Everything else—the rich text, the selected mood state, and emotional insights—resides entirely within the encrypted blob.

// Applying AES-256-GCM client-side before Firestore synchronization
final plaintext = jsonEncode(journalEntry.toMap());
final iv = IV.fromSecureRandom(12); // Fresh IV per encryption
final encrypter = Encrypter(AES(_aesKey, mode: AESMode.gcm));
final encrypted = encrypter.encrypt(plaintext, iv: iv);
// Construct the payload: [12-byte IV] + [Ciphertext + Auth Tag]
final payloadBytes = [...iv.bytes, ...encrypted.bytes];
final base64Payload = base64Encode(payloadBytes);
// Push to Firestore
await firestore.collection('entries').doc(entryId).set({
 'userId': currentUser.uid,
 'date_index': timestamp,
 'isFavorite': false,
 'data': base64Payload, // Only the encrypted blob touches the cloud
});

5. The Search Dilemma: Implementing Blind Indexing

Encrypting data before it leaves the device solves the privacy problem but immediately breaks application functionality. Specifically, how do you build a search feature? A standard SQL LIKE '%query%' operation is impossible when the database contains only Base64 encoded ciphertext. You cannot ask Firestore to search for the word "anxiety" if Firestore has no cryptographic capacity to decrypt the records.

The solution I implemented is a cryptographic technique known as Blind Indexing.

Instead of relying on the cloud for search, RozVibe builds a highly secure, localized index on the user's Android device utilizing SQLite (specifically, a database named rozvibe_search.db). When a user creates or edits a journal entry, the application processes the plaintext locally before encryption. It strips out punctuation, converts the text to lowercase, and splits the content into individual tokenized words.

Next, the application processes each token through an HMAC-SHA256 function utilizing the dedicated 32-byte _searchKey we derived earlier. The resulting cryptographic hashes are saved to the local SQLite database alongside the corresponding journal entry ID. When the user subsequently uses the search bar, the search query is put through the exact same tokenization and hashing pipeline. The app then queries the local SQLite database for matching hashes. Because HMAC is deterministic, the hashes match perfectly, returning the correct document IDs without ever revealing the underlying plaintext tokens to the storage medium.

To support multi-device environments, I engineered a lazy backfill system. When a user logs into a secondary device, their local SQLite index is empty. The application silently pulls the encrypted records from Firestore via Firebase's offline cache, decrypts them locally in an isolate (to prevent UI jank), and systematically rebuilds the blind index in the background.


6. Memory Safety and State Management

In a client-side encrypted application, the cryptographic keys are the absolute crown jewels. While they are mathematically secure at rest, they represent a significant vulnerability while in active use. Proper state management is not merely an architectural preference; it is a critical security requirement.

I architected RozVibe using Riverpod for dependency injection and state management. Riverpod provides granular control over the lifecycle of objects in memory. The master AES key (_aesKey) and the search key (_searchKey) are maintained strictly as in-memory state variables within a dedicated cryptographic provider. They are never written to local device storage, preferences, or SQLite.

When the user logs out, or when the application detects a prolonged period of inactivity, a strict clear() method is invoked. This method explicitly nullifies the variables in RAM, ensuring that the keys cannot be dumped from memory by a malicious process after the session has concluded. If the user wishes to access their journal again, they must re-authenticate and repeat the computationally expensive PBKDF2 derivation process.

Furthermore, recognizing that local access is often the weakest link, I built an application-level PIN lock. As an ultimate failsafe, I also integrated a "Danger Zone" feature deep within the settings menu. Activating this protocol aggressively executes local storage teardowns, wipes the SQLite rozvibe_search.db, and issues cascading delete commands to Firestore, entirely obliterating the user's digital footprint.


7. Balancing Security with UI/UX

One of the hardest lessons learned during this startup journey is that stringent security often degrades user experience. Every cryptographic safeguard introduces friction. The 100,000 iterations of PBKDF2 cause a noticeable, multi-second CPU spike during initial login. The lazy backfill system means search might be temporarily unavailable on a newly registered device.

As an indie developer, the goal is not to compromise security for convenience, but to design an interface that makes the friction acceptable. By incorporating smooth Flutter animations, intuitive loading states, and transparent messaging regarding "building your secure environment," users are more forgiving of the slight performance overhead. Furthermore, by leaning heavily into Firebase’s built-in offline caching capabilities, RozVibe functions as a true local-first application. Users can write, save, and edit entries while completely disconnected from the internet. The app gracefully queues the encrypted payloads and synchronizes them to Firestore only when a secure connection is restored.


8. Lessons Learned as an Indie Hacker

Building RozVibe has been a profound exercise in software engineering and privacy app development. For developers considering building privacy-centric tools, my advice is threefold:

  1. Do not roll your own cryptography. Rely on established, audited libraries (like the encrypt package in Dart) and adhere strictly to industry standards like AES-GCM and PBKDF2.
  2. Acknowledge your threat model. Be transparent with your users. Tell them exactly what your app can do (protect against server breaches) and what it cannot do (protect against a compromised Android OS with a keylogger). True privacy demands honesty, not marketing hyperbole.
  3. Decouple the backend from the business logic. If you design your app so that the backend is entirely ignorant of the data it holds, you insulate yourself from vast amounts of legal, ethical, and infrastructural liability.

The journey from a frustrated journaler to the founder of a privacy-first platform has reinforced my belief that software can—and should—respect human privacy by default. In an era dominated by AI data scraping and surveillance capitalism, local-first encryption is not just a technical feature; it is a fundamental digital right.


Frequently Asked Questions

Flutter (Dart 3.3+) allowed me to build a highly performant, cross-platform application with a single codebase. More importantly, Dart's evolving ecosystem provides robust cryptographic libraries, allowing complex operations like PBKDF2 key derivation and AES-GCM encryption to run natively and securely on the client device without relying on unstable third-party plugins.

RozVibe utilizes a cryptographic technique called blind indexing. The app tokenizes your journal entries locally, hashes each word using an HMAC-SHA256 algorithm with a derived search key, and stores these irreversible hashes in a local SQLite database. When you search, the app hashes your query and matches it against the local database, allowing offline-first search without exposing plaintext to the cloud.

Firebase is secure, but in a zero-knowledge architecture, we assume the server is untrusted. By encrypting the data locally using AES-256-GCM before sending it to Cloud Firestore, Firebase is reduced to a "dumb" synchronization engine. It only stores and routes base64-encoded ciphertext, meaning Google’s servers have zero technical capacity to read the journal contents.

Because RozVibe operates on a strict zero-knowledge framework, the encryption keys exist only on the user's device and are never backed up to our servers. Consequently, if a user forgets their credentials and loses their local session, it is mathematically impossible for us to recover or decrypt their data. This is a deliberate trade-off prioritizing absolute privacy over account recovery.

The term "end-to-end encryption" strictly refers to secure communication between two different parties (like messaging apps). Because a journaling app involves a single user synchronizing data across their own devices, the technically accurate term is "client-side encryption" or "local-first encryption." Using precise terminology avoids marketing hype and maintains technical transparency.



K

Keshav Chauhan

Founder, SezRonix & Creator of RozVibe

Keshav Chauhan is the founder of SezRonix and creator of RozVibe, a privacy-first encrypted journaling platform. He writes about digital privacy, encryption, journaling, Flutter development, and building thoughtful software.