<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joel CODJA's Log]]></title><description><![CDATA[Join me every week as I explore the world of backend and mobile development. From deep-dive tutorials to practical guides, I share the tools, patterns, and disc]]></description><link>https://blog-cjdevcom.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/68455df0ca4254b98e81f77a/797a4d3f-60d7-4a09-a71b-6aa4db8624ef.png</url><title>Joel CODJA&apos;s Log</title><link>https://blog-cjdevcom.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 15:16:44 GMT</lastBuildDate><atom:link href="https://blog-cjdevcom.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Implementing AES-256-GCM Encryption at Rest in Node.js]]></title><description><![CDATA[Introduction
Modern web applications rely heavily on TLS (HTTPS) to protect data while it travels between clients and servers. However, this protection ends once the data reaches the application. If s]]></description><link>https://blog-cjdevcom.hashnode.dev/implementing-aes-256-gcm-encryption-at-rest-in-nodejs</link><guid isPermaLink="true">https://blog-cjdevcom.hashnode.dev/implementing-aes-256-gcm-encryption-at-rest-in-nodejs</guid><dc:creator><![CDATA[Joel CODJA]]></dc:creator><pubDate>Wed, 15 Jul 2026 20:21:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68455df0ca4254b98e81f77a/bb8ee22d-094e-46f3-b617-a2e0b16a6277.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>Modern web applications rely heavily on TLS (HTTPS) to protect data while it travels between clients and servers. However, this protection ends once the data reaches the application. If sensitive information is stored without additional protection, a database compromise can expose that data in readable form.</p>
<p>Application-level encryption addresses this problem by encrypting sensitive data before it is written to persistent storage. Instead of storing plaintext values, the database stores encrypted data that can only be recovered by the application with the correct encryption key.</p>
<p>In this article, we will explore the principles behind encryption at rest and implement a secure application-level encryption system in Node.js using the native <code>crypto</code> module. We will cover the cryptographic concepts involved, the rules required for secure encryption, and build reusable encryption and decryption utilities for protecting database records.</p>
<hr />
<h1>II. Understanding Encryption at Rest</h1>
<p><strong>Encryption at rest</strong> is the practice of protecting stored data by converting it into an unreadable format before it is written to persistent storage. This applies to databases, file systems, backups, and object storage.</p>
<p>Encryption at rest is different from encryption in transit. TLS protects information while it moves across a network, but it does not protect data after it has reached the application and database.</p>
<h2>The Threat Model</h2>
<p>A typical backend application processes data through several stages:</p>
<ol>
<li><p><strong>Client → Application</strong></p>
<p>Data is transmitted securely using TLS (HTTPS).</p>
</li>
<li><p><strong>Application Processing</strong></p>
<p>The application receives and processes the plaintext data.</p>
</li>
<li><p><strong>Database Storage</strong></p>
<p>The data is stored. Without additional encryption, sensitive values remain readable.</p>
</li>
</ol>
<p>The security gap exists at the storage layer. If an attacker gains access to the database through stolen credentials, SQL injection, exposed backups, or incorrect permissions, stored information may be immediately accessible.</p>
<p>TLS cannot protect data after it has already been stored.</p>
<h2>Encryption in Transit vs. Encryption at Rest</h2>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Encryption in Transit</th>
<th>Encryption at Rest</th>
</tr>
</thead>
<tbody><tr>
<td>Purpose</td>
<td>Protect data while moving between systems</td>
<td>Protect stored data</td>
</tr>
<tr>
<td>Example</td>
<td>HTTPS/TLS</td>
<td>Encrypted database fields</td>
</tr>
<tr>
<td>Protects Against</td>
<td>Network interception</td>
<td>Database compromise and storage exposure</td>
</tr>
<tr>
<td>Applied By</td>
<td>Infrastructure and networking layers</td>
<td>Database or application layer</td>
</tr>
</tbody></table>
<p>Both forms of encryption are important. TLS protects data while it travels, while encryption at rest protects it after storage.</p>
<h2>Choosing the Right Encryption Approach</h2>
<p>Before implementing encryption in Node.js, it is important to understand the main categories of cryptographic algorithms and where each one should be used.</p>
<hr />
<h1>III. Symmetric vs. Asymmetric Encryption</h1>
<p>Modern cryptography is divided into two major categories: <strong>symmetric encryption</strong> and <strong>asymmetric encryption</strong>.</p>
<p>Both provide security, but they solve different problems.</p>
<h2>Symmetric Encryption</h2>
<p>Symmetric encryption uses a single secret key for both encryption and decryption.</p>
<p>The same key that transforms plaintext into ciphertext is required to recover the original data.</p>
<h3>Advantages</h3>
<ul>
<li><p>Very fast and efficient</p>
</li>
<li><p>Suitable for encrypting large amounts of data</p>
</li>
<li><p>Ideal for protecting stored application data</p>
</li>
</ul>
<h3>Challenge</h3>
<p>The main difficulty is protecting the encryption key. Anyone who obtains the key can decrypt the protected data.</p>
<p>Common symmetric algorithms include:</p>
<ul>
<li><p>AES (Advanced Encryption Standard)</p>
</li>
<li><p>ChaCha20</p>
</li>
</ul>
<h2>Asymmetric Encryption</h2>
<p>Asymmetric encryption uses a pair of keys:</p>
<ul>
<li><p>A public key that can be shared</p>
</li>
<li><p>A private key that must remain secret</p>
</li>
</ul>
<p>Data encrypted with the public key can only be decrypted using the matching private key.</p>
<h3>Advantages</h3>
<ul>
<li><p>Enables secure key exchange</p>
</li>
<li><p>Supports digital signatures</p>
</li>
<li><p>Removes the need to share a secret key publicly</p>
</li>
</ul>
<h3>Limitations</h3>
<ul>
<li><p>Much slower than symmetric encryption</p>
</li>
<li><p>Not suitable for encrypting large amounts of application data</p>
</li>
</ul>
<p>Common asymmetric algorithms include:</p>
<ul>
<li><p>RSA</p>
</li>
<li><p>Elliptic Curve Cryptography (ECC)</p>
</li>
<li><p>Ed25519</p>
</li>
</ul>
<h2>Which Approach Should Be Used for Database Encryption?</h2>
<p>For encrypting database records, symmetric encryption is the appropriate choice.</p>
<p>A backend application typically writes and reads its own data. There is no need to exchange encryption keys with another party, making the speed and simplicity of symmetric encryption the better option.</p>
<p>Asymmetric encryption remains essential for other areas of security, such as authentication, digital signatures, and secure key exchange.</p>
<p>For protecting stored application data, we need a fast symmetric encryption algorithm with strong security guarantees.</p>
<hr />
<h1>IV. What is AES-256-GCM?</h1>
<p>AES-256-GCM combines three cryptographic concepts:</p>
<ul>
<li><p><strong>AES (Advanced Encryption Standard)</strong>: the encryption algorithm</p>
</li>
<li><p><strong>256</strong>: the size of the encryption key</p>
</li>
<li><p><strong>GCM (Galois/Counter Mode)</strong>: the mode that provides encryption and integrity verification</p>
</li>
</ul>
<p>Together, they provide a modern authenticated encryption system suitable for protecting sensitive application data.</p>
<h2>AES</h2>
<p>AES is a symmetric block cipher standardized by NIST and widely adopted across modern systems.</p>
<p>It supports three key sizes:</p>
<ul>
<li><p>128-bit</p>
</li>
<li><p>192-bit</p>
</li>
<li><p>256-bit</p>
</li>
</ul>
<p>AES is considered secure because it has been extensively analyzed, efficiently implemented, and widely deployed across operating systems, cloud providers, and security standards.</p>
<h2>Why AES-256?</h2>
<p>The "256" in AES-256 refers to the key size.</p>
<p>A 256-bit key provides an extremely large number of possible combinations, making brute-force attacks computationally impractical.</p>
<p>AES-128 is still considered secure, but AES-256 provides a larger security margin and is commonly selected when protecting highly sensitive information.</p>
<h2>Understanding GCM</h2>
<p>AES only defines the encryption algorithm. A mode of operation determines how AES is applied to larger amounts of data.</p>
<p>GCM (Galois/Counter Mode) is a modern mode that provides <strong>Authenticated Encryption with Associated Data (AEAD)</strong>.</p>
<p>This provides two important security properties:</p>
<h3>Confidentiality</h3>
<p>The original data is encrypted and cannot be read without the encryption key.</p>
<h3>Integrity</h3>
<p>Any modification to the encrypted data is detected during decryption.</p>
<p>AES-GCM produces three important values:</p>
<ul>
<li><p>Ciphertext</p>
</li>
<li><p>Nonce (IV)</p>
</li>
<li><p>Authentication tag</p>
</li>
</ul>
<p>The authentication tag allows the application to verify that the encrypted data has not been modified. If the ciphertext, nonce, or key is incorrect, decryption fails instead of returning invalid data.</p>
<p>With the fundamentals of AES-256-GCM understood, we can now examine the rules required to use it securely.</p>
<h1>V. The Three Critical Rules of AES-GCM</h1>
<p>AES-256-GCM provides strong security guarantees, but only when it is implemented correctly. Most AES-GCM vulnerabilities do not come from breaking the algorithm itself, in fact, they come from incorrect usage.</p>
<p>Three rules are essential for maintaining the security properties of AES-GCM.</p>
<hr />
<h2>Rule 1: Never Reuse a Nonce with the Same Key</h2>
<p>A <strong>nonce</strong> (Number Used Once), also called an initialization vector (IV), is a unique value used during encryption.</p>
<p>With AES-GCM, every encryption operation using the same key must use a different nonce.</p>
<p>The nonce is used internally by GCM to generate the encryption stream. Reusing the same nonce with the same key causes the encryption process to reuse the same internal state, which can expose information about the original plaintext and compromise the authentication mechanism.</p>
<p>Nonce reuse can result in:</p>
<ul>
<li><p>Loss of confidentiality</p>
</li>
<li><p>Exposure of relationships between encrypted messages</p>
</li>
<li><p>Ability to forge valid authentication tags</p>
</li>
</ul>
<p>The correct approach is to generate a fresh random nonce for every encryption operation:</p>
<pre><code class="language-javascript">const iv = crypto.randomBytes(12);

const cipher = crypto.createCipheriv(
    'aes-256-gcm',
    key,
    iv
);
</code></pre>
<p>AES-GCM is optimized for <strong>96-bit (12-byte) nonces</strong>, which is the recommended size for most applications.</p>
<p>The nonce does not need to be secret. It must simply be unique and stored alongside the encrypted data so it can be provided during decryption.</p>
<p>Example storage format:</p>
<pre><code class="language-json">{
  "iv": "...",
  "tag": "...",
  "data": "..."
}
</code></pre>
<hr />
<h2>Rule 2: Always Verify the Authentication Tag</h2>
<p>AES-GCM is an authenticated encryption algorithm. It does not only encrypt data: it also verifies that the encrypted data has not been modified.</p>
<p>During encryption, GCM generates an <strong>authentication tag</strong>.</p>
<p>During decryption, this tag must be provided and verified before the plaintext is returned.</p>
<p>In Node.js:</p>
<pre><code class="language-javascript">decipher.setAuthTag(authTag);

const plaintext = Buffer.concat([
    decipher.update(ciphertext),
    decipher.final()
]);
</code></pre>
<p>If the ciphertext has been modified, the wrong key is used, or the authentication tag does not match, <code>decipher.final()</code> throws an error.</p>
<p>A successful decryption should always mean:</p>
<ul>
<li><p>The key is correct</p>
</li>
<li><p>The nonce is correct</p>
</li>
<li><p>The ciphertext is unchanged</p>
</li>
<li><p>The authentication tag is valid</p>
</li>
</ul>
<p>Never ignore authentication failures or return partially decrypted data.</p>
<hr />
<h2>Rule 3: Protect Your Encryption Key</h2>
<p>The encryption key is the most sensitive component of the entire system.</p>
<p>AES-256-GCM can only protect data if the key remains secret. If an attacker obtains the encryption key, they can decrypt all data protected by that key.</p>
<p>Common ways keys are accidentally exposed include:</p>
<ul>
<li><p>Hardcoding keys in source code</p>
</li>
<li><p>Committing secrets to version control</p>
</li>
<li><p>Logging sensitive configuration values</p>
</li>
<li><p>Storing keys in insecure files</p>
</li>
<li><p>Exposing production secrets to unnecessary users</p>
</li>
</ul>
<h3>Key Management Best Practices</h3>
<h3>Never hardcode encryption keys</h3>
<p>Encryption keys should never exist directly in application code.</p>
<p>Bad:</p>
<pre><code class="language-javascript">const key = "a1b2c3d4...";
</code></pre>
<h3>Use environment variables for development</h3>
<p>Example:</p>
<pre><code class="language-env">AES_256_KEY=your_secret_key_here
</code></pre>
<p>Ensure environment files are excluded from version control.</p>
<h3>Use a secrets manager in production</h3>
<p>Production systems should use dedicated secret storage solutions, such as:</p>
<ul>
<li><p>AWS Secrets Manager</p>
</li>
<li><p>Azure Key Vault</p>
</li>
<li><p>Google Secret Manager</p>
</li>
<li><p>HashiCorp Vault</p>
</li>
</ul>
<h3>Restrict access</h3>
<p>Only the services and users that require access to the encryption key should be able to retrieve it.</p>
<h3>Plan for key rotation</h3>
<p>A production system should have a strategy for replacing keys without losing access to existing encrypted data.</p>
<p>Example key loading:</p>
<pre><code class="language-javascript">const secretKeyHex = process.env.AES_256_KEY;

if (!secretKeyHex) {
    throw new Error('AES_256_KEY is missing');
}

if (secretKeyHex.length !== 64) {
    throw new Error('AES_256_KEY must contain 32 bytes');
}

const key = Buffer.from(secretKeyHex, 'hex');
</code></pre>
<p>Following these three rules ensures AES-256-GCM provides the security guarantees it was designed to deliver.</p>
<hr />
<h1>VI. Implementation in Node.js</h1>
<p>Node.js includes a built-in <code>crypto</code> module that provides native support for AES-256-GCM.</p>
<p>No external dependencies are required.</p>
<p>Before implementing encryption, remember that AES-GCM produces three values that must be stored together:</p>
<ul>
<li><p><strong>Ciphertext</strong>: the encrypted data</p>
</li>
<li><p><strong>Nonce (IV)</strong>: required for decryption</p>
</li>
<li><p><strong>Authentication tag</strong>: verifies data integrity</p>
</li>
</ul>
<p>The database does not need to understand these values. It only needs to store them so the application can decrypt the data later.</p>
<hr />
<h2>Step 1: Generate an Encryption Key</h2>
<p>AES-256 requires a 256-bit (32-byte) key.</p>
<p>The key must be generated using a cryptographically secure random generator.</p>
<p>Generate a key:</p>
<pre><code class="language-bash">node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
</code></pre>
<p>Example output:</p>
<pre><code class="language-plaintext">8f3a9b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1
</code></pre>
<p>Store the key securely:</p>
<pre><code class="language-env">AES_256_KEY=your_generated_key_here
</code></pre>
<p>Never commit this value to source control.</p>
<hr />
<h2>Step 2: Create the Encryption Utility</h2>
<p>Create a reusable encryption module:</p>
<pre><code class="language-javascript">// crypto.js

const crypto = require('crypto');

const ALGORITHM = 'aes-256-gcm';

const KEY_HEX = process.env.AES_256_KEY;

if (!KEY_HEX || KEY_HEX.length !== 64) {
    throw new Error(
        'AES_256_KEY must be a 64-character hexadecimal string'
    );
}

const KEY = Buffer.from(KEY_HEX, 'hex');


function encrypt(value) {
    if (value === null || value === undefined) {
        return null;
    }

    const plaintext = JSON.stringify(value);

    const iv = crypto.randomBytes(12);

    const cipher = crypto.createCipheriv(
        ALGORITHM,
        KEY,
        iv
    );

    const ciphertext = Buffer.concat([
        cipher.update(plaintext, 'utf8'),
        cipher.final()
    ]);

    const authTag = cipher.getAuthTag();

    return JSON.stringify({
        iv: iv.toString('hex'),
        tag: authTag.toString('hex'),
        data: ciphertext.toString('hex')
    });
}


function decrypt(payload) {
    if (!payload) {
        return null;
    }

    const {
        iv,
        tag,
        data
    } = JSON.parse(payload);


    const decipher = crypto.createDecipheriv(
        ALGORITHM,
        KEY,
        Buffer.from(iv, 'hex')
    );


    decipher.setAuthTag(
        Buffer.from(tag, 'hex')
    );


    const plaintext = Buffer.concat([
        decipher.update(Buffer.from(data, 'hex')),
        decipher.final()
    ]);


    return JSON.parse(
        plaintext.toString('utf8')
    );
}


module.exports = {
    encrypt,
    decrypt
};
</code></pre>
<hr />
<h2>How the Implementation Works</h2>
<h3>Encryption Flow</h3>
<p>The encryption process follows these steps:</p>
<ol>
<li>Generate a unique nonce:</li>
</ol>
<pre><code class="language-javascript">crypto.randomBytes(12)
</code></pre>
<ol>
<li><p>Create an AES-256-GCM cipher using the encryption key.</p>
</li>
<li><p>Encrypt the plaintext into ciphertext.</p>
</li>
<li><p>Extract the authentication tag.</p>
</li>
<li><p>Store the nonce, tag, and ciphertext together.</p>
</li>
</ol>
<hr />
<h3>Decryption Flow</h3>
<p>The decryption process reverses the operation:</p>
<ol>
<li><p>Read the stored encrypted payload.</p>
</li>
<li><p>Extract the nonce, authentication tag, and ciphertext.</p>
</li>
<li><p>Initialize AES-GCM with the same key and nonce.</p>
</li>
<li><p>Provide the authentication tag.</p>
</li>
<li><p>Decrypt the ciphertext.</p>
</li>
</ol>
<p>If any value has been modified, authentication fails and decryption stops.</p>
<hr />
<h2>Database Storage Format</h2>
<p>Encrypted values can be stored as a JSON string:</p>
<pre><code class="language-json">{
  "iv": "a1b2c3d4e5f6789012345678",
  "tag": "f9e8d7c6b5a43210fedcba9876543210",
  "data": "8a7b6c5d4e3f20112233445566778899"
}
</code></pre>
<p>A typical database column can simply use a text-based type:</p>
<ul>
<li><p>PostgreSQL: <code>TEXT</code></p>
</li>
<li><p>MySQL: <code>TEXT</code> or <code>VARCHAR</code></p>
</li>
<li><p>MongoDB: <code>String</code></p>
</li>
</ul>
<p>One important limitation of application-level encryption is that encrypted fields cannot be queried normally. Searching, sorting, or indexing encrypted values requires additional design decisions.</p>
<hr />
<h1>VII. Conclusion</h1>
<p>Encryption at rest is an essential part of protecting sensitive data in modern backend applications. TLS protects information while it travels between systems, but it does not protect data after it reaches the application and database.</p>
<p>By encrypting sensitive values before storage, applications can reduce the impact of database breaches and unauthorized access.</p>
<p>In this article, we explored the foundations of encryption at rest, the differences between symmetric and asymmetric encryption, and the principles behind AES-256-GCM. We then implemented a reusable encryption layer in Node.js using the native <code>crypto</code> module.</p>
<p>The implementation follows the core requirements for secure AES-GCM usage:</p>
<ul>
<li><p>Using authenticated encryption</p>
</li>
<li><p>Generating a unique nonce for every encryption operation</p>
</li>
<li><p>Protecting encryption keys outside the application code</p>
</li>
<li><p>Verifying authentication tags before accepting decrypted data</p>
</li>
</ul>
<p>However, encryption is only one layer of a complete security strategy. Secure systems also require proper access controls, auditing, monitoring, and a clear approach to handling potential compromises.</p>
<p>The goal of encryption is not simply to make data unreadable. It is to ensure that sensitive information remains protected even when other security layers fail.</p>
<p>Building encryption into your architecture from the beginning creates systems that are more resilient, trustworthy, and prepared for real-world threats.</p>
]]></content:encoded></item><item><title><![CDATA[Clean Architecture in Flutter: Why It Matters
]]></title><description><![CDATA[The Problem
Flutter makes it incredibly easy to build applications. In just a few hours, you can have authentication, API calls, local storage, and multiple screens working together.
The challenge com]]></description><link>https://blog-cjdevcom.hashnode.dev/clean-architecture-in-flutter-why-it-matters</link><guid isPermaLink="true">https://blog-cjdevcom.hashnode.dev/clean-architecture-in-flutter-why-it-matters</guid><dc:creator><![CDATA[Joel CODJA]]></dc:creator><pubDate>Sun, 05 Jul 2026 14:05:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68455df0ca4254b98e81f77a/7e3822d8-3cf5-40fa-a19d-57f0636dc384.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>The Problem</h2>
<p>Flutter makes it incredibly easy to build applications. In just a few hours, you can have authentication, API calls, local storage, and multiple screens working together.</p>
<p>The challenge comes as the application grows.</p>
<p>What starts as a simple app with a few screens soon expands to include features like payments, notifications, offline support, analytics, and dozens of API endpoints. Without a clear structure, widgets gradually take on multiple responsibilities. They fetch data, parse JSON, handle business logic, validate input, manage navigation, and update the UI.</p>
<p>Over time, this creates tightly coupled code where a change in one feature can unexpectedly affect several others. The app still works, but it becomes increasingly difficult to maintain, test, and extend.</p>
<p>This is exactly the problem Clean Architecture is designed to solve.</p>
<hr />
<h2>The Solution</h2>
<p>Rather than organizing code by features or folders alone, Clean Architecture organizes an application by <strong>responsibility</strong>.</p>
<p>Each layer has a single purpose, and clear boundaries prevent implementation details from leaking throughout the application.</p>
<p>Its core principle is simple:</p>
<blockquote>
<p><strong>Business logic should not depend on Flutter, external services, or implementation details.</strong></p>
</blockquote>
<p>Flutter is simply the framework used to present your application. The business rules should remain independent of the UI, backend, or storage solution.</p>
<hr />
<h2>The Three Layers</h2>
<h3>Presentation</h3>
<p>The Presentation layer handles everything related to the user interface:</p>
<ul>
<li><p>Screens and widgets</p>
</li>
<li><p>User interactions</p>
</li>
<li><p>State management (Bloc, Cubit, Riverpod, Provider, GetX, etc.)</p>
</li>
</ul>
<p>Its job is to display data and respond to user actions, not to contain business logic or communicate directly with APIs.</p>
<h3>Domain</h3>
<p>The Domain layer contains the application's business rules.</p>
<p>It defines what the application does, regardless of how it's implemented. Whether it's authenticating a user, validating an order, or calculating a payment, these rules remain independent of Flutter, databases, and networking.</p>
<p>Because of this separation, the Domain layer is reusable, framework independent, and easy to test.</p>
<h3>Data</h3>
<p>The Data layer is responsible for interacting with external systems such as REST APIs, GraphQL, Firebase, SQLite, Hive, or secure storage.</p>
<p>Its role is to retrieve, store, and transform data before exposing it to the rest of the application.</p>
<p>Changing your backend or storage solution should only require changes in this layer.</p>
<hr />
<h2>How the Layers Work Together</h2>
<p>Communication flows in one direction:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68455df0ca4254b98e81f77a/a4ae9c1b-8fa6-474a-b226-91cef8d25210.png" alt="" style="display:block;margin:0 auto" />

<p>The Presentation layer requests an action. The Domain layer decides what should happen. The Data layer retrieves or persists the required data.</p>
<p>Each layer only knows about its own responsibility, keeping the application modular and easy to reason about.</p>
<hr />
<h2>Flutter Is Flexible, The Architecture Shouldn't Be</h2>
<p>One of the biggest advantages of Clean Architecture is that it isn't tied to any specific Flutter technology.</p>
<p>Whether you use Bloc, Riverpod, Provider, or GetX for state management, or Firebase, REST, GraphQL, SQLite, or Hive for data, the overall architecture remains the same.</p>
<p>The tools may change, but the responsibilities of each layer do not.</p>
<hr />
<h2>Benefits of Clean Architecture</h2>
<ul>
<li><p><strong>Maintainability:</strong> Changes are isolated to the appropriate layer.</p>
</li>
<li><p><strong>Scalability:</strong> New features can be added without increasing complexity.</p>
</li>
<li><p><strong>Testability:</strong> Business logic can be tested independently of Flutter.</p>
</li>
<li><p><strong>Technology Independence:</strong> Swapping APIs, databases, or state management solutions requires minimal changes.</p>
</li>
<li><p><strong>Collaboration:</strong> Teams can work on different layers with fewer conflicts.</p>
</li>
</ul>
<hr />
<h2>Conclusion</h2>
<p>Clean Architecture isn't about creating more folders or adding unnecessary complexity. It's about managing complexity as your application grows.</p>
<p>By separating Presentation, Domain, and Data, you create Flutter applications that are easier to understand, test, maintain, and evolve over time. Regardless of the state management or backend technologies you choose, these principles remain the same, making your codebase more resilient to change and far easier to scale.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Robust Flutter Todo App: MVC Architecture and Offline-First Approach]]></title><description><![CDATA[Building a Robust Flutter Todo App: Offline-First Architecture for Beginners
Building a Todo app is the classic first project in any framework, but most tutorials focus only on the UI. This article ta]]></description><link>https://blog-cjdevcom.hashnode.dev/building-a-robust-flutter-todo-app</link><guid isPermaLink="true">https://blog-cjdevcom.hashnode.dev/building-a-robust-flutter-todo-app</guid><dc:creator><![CDATA[Joel CODJA]]></dc:creator><pubDate>Mon, 29 Jun 2026 16:50:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68455df0ca4254b98e81f77a/2821bd9c-4fc4-4eb1-b806-556be054a086.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Building a Robust Flutter Todo App: Offline-First Architecture for Beginners</h1>
<p>Building a Todo app is the classic first project in any framework, but most tutorials focus only on the UI. This article takes a different approach: it walks through building a production-ready Todo app with a clean architecture that scales : offline-first, locally persisted, and fully reactive.</p>
<p>By the end, you'll understand how to structure a Flutter project using the <strong>Repository pattern</strong>, <strong>Cubit state management</strong>, and the <strong>Isar database</strong> for offline storage. No cloud services, no complex setup : just a solid foundation you can extend later.</p>
<h2>Project Structure</h2>
<p>Before writing any code, lay out a clear folder structure. Each layer has one responsibility:</p>
<pre><code class="language-plaintext">lib/
├── model/          # Data representation
├── repository/     # Data access
├── cubit/          # State management
├── screens/        # Full-page UI
├── widgets/        # Reusable UI pieces
├── theme/          # Colors, fonts, sizes
└── utils/          # Dependency injection setup
</code></pre>
<p>This separation means changing the database only touches the repository. Changing the look only touches screens and widgets. Each layer is independent.</p>
<h2>1. The Model : Keeping Data Simple</h2>
<p>The model is a plain Dart class that represents a task. It uses <strong>Isar</strong>, an embedded database that stores data permanently on the device.</p>
<pre><code class="language-dart">@collection
class Task {
  Id id = Isar.autoIncrement;
  late String title;
  String? description;
  bool isCompleted = false;
  late DateTime date;

  Task({required this.title, this.description, this.isCompleted = false})
    : date = DateTime.now();
}
</code></pre>
<p>Key concepts:</p>
<ul>
<li><p><code>@collection</code> tells Isar this class should be stored in the database.</p>
</li>
<li><p><code>Id</code> is the auto-incrementing primary key : every new task gets a unique number.</p>
</li>
<li><p><code>isCompleted</code> defaults to <code>false</code>; a new task is always pending.</p>
</li>
<li><p><code>date</code> is automatically set to the current time when the task is created.</p>
</li>
</ul>
<p>A <code>copyWith</code> method and a <code>dateFormatee</code> getter are added for convenience. The model contains no logic and no UI code : just data.</p>
<h2>2. The Repository : Abstracting the Database</h2>
<p>The <strong>repository pattern</strong> creates a wall between business logic and the database. The app never talks directly to Isar. It talks to a repository, which handles the details.</p>
<h3>The Interface (Contract)</h3>
<p>First, define <em>what</em> the repository can do:</p>
<pre><code class="language-dart">abstract class TaskRepository {
  Stream&lt;List&lt;Task&gt;&gt; watchAllTasks();
  Future&lt;int&gt; saveTask(Task task);
  Future&lt;void&gt; deleteTask(int id);
  Future&lt;Task?&gt; getTaskById(int id);
  Future&lt;int&gt; toggleStatus(int id);
}
</code></pre>
<h3>The Implementation (How)</h3>
<p>Then implement <em>how</em> it does it with Isar:</p>
<pre><code class="language-dart">class TaskRepositoryImpl implements TaskRepository {
  final Isar _isar;

  @override
  Future&lt;int&gt; saveTask(Task task) async {
    if (task.title.trim().isEmpty) throw ArgumentError('Title is required');
    return await _isar.writeTxn(() =&gt; _isar.tasks.put(task));
  }

  @override
  Future&lt;void&gt; deleteTask(int id) async {
    await _isar.writeTxn(() =&gt; _isar.tasks.delete(id));
  }

  @override
  Stream&lt;List&lt;Task&gt;&gt; watchAllTasks() {
    return _isar.tasks.where().watchLazy(fireImmediately: true)
        .asyncMap((_) =&gt; _isar.tasks.where().findAll());
  }
}
</code></pre>
<p>Why an interface? If the database changes from Isar to SQLite or anything else, only a new implementation class is needed. The rest of the app remains untouched.</p>
<h2>3. Cubit : The State Manager</h2>
<p>The <strong>Cubit</strong> acts as the brain of the app. It listens to the repository and tells the UI what to display.</p>
<h3>Defining States</h3>
<p>States are defined as a sealed class hierarchy:</p>
<pre><code class="language-dart">sealed class TaskCubitState extends Equatable {
  const TaskCubitState();
}

class TaskLoading extends TaskCubitState {}
class TaskMainState extends TaskCubitState {
  final List&lt;Task&gt; tasks;
  const TaskMainState({required this.tasks});
}
class TaskFailureState extends TaskCubitState {
  final String message;
  const TaskFailureState({required this.message});
}
</code></pre>
<h3>The Cubit</h3>
<p>The Cubit subscribes to the repository's stream. When data changes (a task is added, deleted, or toggled), the stream emits a new list, and the Cubit emits a new state. The UI rebuilds automatically.</p>
<pre><code class="language-dart">class TaskCubit extends Cubit&lt;TaskCubitState&gt; {
  final TaskRepository _repository;

  void watchAllTasks() {
    emit(const TaskLoading());
    _subscription = _repository.watchAllTasks().listen(
      (tasks) =&gt; emit(TaskMainState(tasks: tasks)),
      onError: (e) =&gt; emit(TaskFailureState(message: e.toString())),
    );
  }

  Future&lt;void&gt; saveTask(Task task) async {
    try {
      await _repository.saveTask(task);
    } catch (e) {
      emit(TaskFailureState(message: e.toString()));
    }
  }
}
</code></pre>
<p>Notice that <code>saveTask</code> does not emit a success state. The stream subscription in <code>watchAllTasks</code> automatically emits the updated list. The UI stays in sync without manual refresh.</p>
<h2>4. The UI : Putting It All Together</h2>
<p>The <code>HomeScreen</code> uses <code>BlocConsumer</code> to listen to the Cubit and rebuild when the state changes.</p>
<pre><code class="language-dart">BlocConsumer&lt;TaskCubit, TaskCubitState&gt;(
  listener: (context, state) {
    if (state is TaskFailureState) showErrorSnackBar(context, state.message);
  },
  builder: (context, state) {
    if (state is TaskLoading) return const CircularProgressIndicator();
    if (state is TaskMainState) return _buildTaskList(state.tasks);
    return const SizedBox.shrink();
  },
);
</code></pre>
<p>Tasks are grouped by date with section headers. Active tasks appear first, completed ones in a separate section. Each task card supports swipe-to-delete and tap-to-view-details.</p>
<p>Reusable widgets keep the code clean:</p>
<ul>
<li><p><strong>TaskCard</strong> : displays a single task with checkbox, title, date, and swipe action.</p>
</li>
<li><p><strong>AddTaskPopup</strong> : a bottom sheet form with real-time validation (button stays disabled until the title is filled).</p>
</li>
<li><p><strong>TaskDetailPopup</strong> : shows full task info in a bottom sheet.</p>
</li>
<li><p><strong>EmptyState</strong> : friendly illustration when no tasks exist.</p>
</li>
</ul>
<h2>5. The Theme : Keeping It Consistent</h2>
<p>All colors, sizes, fonts, and styles live in dedicated files under <code>lib/theme/</code>. No hardcoded values anywhere.</p>
<pre><code class="language-dart">class AppColors {
  AppColors._();  // Private constructor prevents instantiation
  static const pink = Color(0xFFEC4899);
  static const success = Color(0xFF22C55E);
  static const error = Color(0xFFEF4444);
}
</code></pre>
<p>Both light and dark themes are defined using <code>ThemeData</code> with Material 3 and a pink seed color. Switching themes is a single button tap in the AppBar.</p>
<h2>Why Offline-First Matters</h2>
<p>Everything writes to Isar first. The app works 100 % without internet. No loading spinners waiting for a server. No "No connection" errors. Just instant, local data.</p>
<p>This is the foundation. Later, cloud sync (Firestore, Supabase, or a custom API) can be added on top : the repository pattern makes it trivial to extend.</p>
<h2>Key Takeaways</h2>
<p><strong>1. Separate concerns.</strong> The model knows nothing about the UI. The UI knows nothing about the database. The Cubit bridges them cleanly.</p>
<p><strong>2. Streams eliminate manual refresh.</strong> Instead of calling a "refresh list" function after every action, the database stream does the work automatically. Add a task? Stream fires. Delete a task? Stream fires. The UI updates reactively.</p>
<p><strong>3. Private constructors on utility classes</strong> (<code>AppColors._()</code>) prevent creating instances where none are needed. A small habit that keeps the codebase clean.</p>
<p><strong>4. Declarative UI means</strong> describing <em>what</em> the screen should look like for each state, not <em>how</em> to transition between states. Flutter handles the rest.</p>
<h2>Final Thoughts</h2>
<p>A well-structured Todo app teaches more than any tutorial. The patterns shown here : Repository, Cubit, offline-first with Isar : apply to any Flutter project, from simple apps to complex production systems.</p>
<p>The complete source code is available on GitHub : <a href="https://github.com/cjdotdev/Flutter-Todoapp">github.com/cjdotdev/Flutter-Todoapp</a>. Clone it, experiment, and make it your own.</p>
]]></content:encoded></item></channel></rss>