Skip to content

Local-First Architecture Series II: Environment Setup, SQLite Validation & Architectural Foundations

Posted on:November 28, 2025

Welcome, Developer!

If Part 1 reframed how we think about mobile networking and reliability, Part 2 focuses on something far more foundational: ensuring your development environment is predictable before you build anything Local-First.

Local-First architectures often fail before they even start — not because of schema design or sync complexity, but because the environment itself behaves inconsistently. SQLite might silently fail to initialize. Background tasks may never run. Offline restarts may break hydration paths. Resume events might trigger inconsistent state. These failures hide beneath the surface, producing bugs that look like “sync issues” but are actually environment issues.

Before you build a write pipeline, schema, sync engine, or conflict resolver, you must ensure the development environment is properly set. Part 2 exists to make that true.

Creating a Predictable Environment

Begin with a clean Expo project:

npx create-expo-app local-first-app
cd local-first-app
npm run ios   # or: npm run android

This gives you:

If you do not have a simulator/emulator ready, check the Expo docs for setup guidance.

Install Required Dependencies

npx expo install expo-sqlite expo-background-task uuid

Why:

These dependencies form the technical foundation of the Local-First architecture.

What We Must Validate Before Building Anything Else

Before writing schema or sync logic, we need to verify that the environment supports:

These checks remove the environmental uncertainty that causes 80% of Local-First failures.

Validating SQLite

SQLite is the backbone of Local-First.
If SQLite fails silently, nothing else will work.

Before designing schema or migrations, we must confirm that SQLite can:

  1. open
  2. create tables
  3. write data
  4. read data
  5. persist across restarts
  6. operate offline

Let’s test that.

1. Create the SQLite Test Module

mkdir -p src/db
touch src/db/sqliteTest.ts

Add:

import * as SQLite from "expo-sqlite";
 
export async function testSQLite() {
  const db = await SQLite.openDatabaseAsync("test.db");
 
  await db.withTransactionAsync(async () => {
    await db.execAsync(`
      CREATE TABLE IF NOT EXISTS t (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        value TEXT
      );
    `);
 
    await db.runAsync("INSERT INTO t (value) VALUES (?)", ["hello sqlite"]);
 
    const rows = await db.getAllAsync("SELECT * FROM t");
    console.log("SQLite test rows:", rows);
  });
}

2. How This Test Works

Opening the Database

Ensures SQLite can create and access a persistent DB file.

Running a Transaction

Guarantees atomicity — required for write pipelines.

Creating the Table

Confirms schema creation, SQL parsing, and DB write capability.

Inserting a Row

Verifies local write permissions and parameter binding.

Selecting the Row

Validates query execution and result parsing.

If you see at least one row, SQLite is functioning correctly.

3. Call It in Your Home Screen

import { useEffect } from "react";
import { testSQLite } from "@/src/db/sqliteTest";
 
export default function HomeScreen() {
  useEffect(() => {
    console.log("Running SQLite test…");
    testSQLite();
  }, []);
 
  return (...);
}

Expected Output:

Running SQLite test…
SQLite test rows: [ { id: 1, value: 'hello sqlite' } ]

Verify Durability Across Restarts (Critical Gate #2)

This is the only required check before moving on to Part 3.

Your DB must survive:

Quick Test

  1. Run testSQLite().
  2. Reload (⌘R or Reload in Android).
  3. Confirm the row still exists.
  4. Kill the app and reopen.
  5. Confirm persistence.
  6. Restart the simulator/device.
  7. Confirm persistence again.

If persistence fails, fix database initialization before continuing.

What You Accomplished in Part 2

You now have:

You eliminated the root cause of most Local-First failures:
an unpredictable environment.

Nice work, Developer.

Conclusion

Local-First architecture does not begin with clever sync logic or conflict resolution.
It begins with certainty — knowing the environment behaves predictably across restarts, kills, and offline scenarios.

By validating your storage engine and execution environment now, you ensure that every architectural layer built afterward—schema, pending actions, hybrid sync, conflict resolution—rests on solid ground.

Next up:
👉 Part 3 — Building the Local Database Layer

See you soon, Developer. Stay safe 💙