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 androidThis gives you:
- a deterministic project structure
- up-to-date Expo runtimes
- consistent tooling across iOS and Android
- TypeScript-by-default (recommended)
- a stable baseline to validate SQLite and background tasks
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 uuidWhy:
- expo-sqlite → durable ACID-compliant local storage
- expo-background-task → OS-managed deferrable background execution
- uuid → deterministic IDs for offline-created entities
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:
- reliable offline reads and writes
- durability across reloads and restarts
- consistent resume behavior
- SQLite availability before React renders
- background task support (for later hybrid sync)
- a predictable folder structure for architecture components
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:
- open
- create tables
- write data
- read data
- persist across restarts
- operate offline
Let’s test that.
1. Create the SQLite Test Module
mkdir -p src/db
touch src/db/sqliteTest.tsAdd:
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:
- reloads
- app kills
- simulator/device restarts
Quick Test
- Run
testSQLite(). - Reload (
⌘Ror Reload in Android). - Confirm the row still exists.
- Kill the app and reopen.
- Confirm persistence.
- Restart the simulator/device.
- Confirm persistence again.
If persistence fails, fix database initialization before continuing.
What You Accomplished in Part 2
You now have:
- a deterministic development environment
- verified, persistent SQLite storage
- confidence that local storage is stable
- background task support ready for later chapters
- the structural foundation for the Local Write Pipeline
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 💙