# An Offline-First Folder Architecture for Expo Apps

> By Lawrence Arya, Founder & CEO of VP0. Published 2026-06-08. 7 min read.
> Source: https://vp0.com/blogs/expo-app-offline-folder-architecture-ai

**TL;DR.** An offline-first Expo app works because of how it is structured, not by adding offline at the end. The architecture is three layers in clear folders: a local data store like SQLite or WatermelonDB that the UI always reads from, a sync layer that queues mutations and reconciles them on reconnect, and screens that never call the network directly. Get the folders right and offline, optimistic updates, and conflict handling follow. Start from a template and give your AI agent the same structure.

## Offline-first is an architecture, not a feature

Offline support is not something you sprinkle on at the end. An app either is built offline-first, where local data is the source of truth and the network is a background sync, or it is built online-first and offline is a fragile patch over screens that assume a connection. The difference shows up in the folder structure: an offline-first app has a clear place for local data, a clear place for sync, and screens that never touch the network directly. Get that layout right and offline, optimistic updates, and conflict handling fall out of it; get it wrong and you fight the network in every component.

So the useful question is not "how do I add offline" but "how do I structure the app so offline is the default." In [Expo](https://docs.expo.dev/), that is a folder decision made early.

## The three layers and their folders

The architecture is three layers, each with its own home. A data layer holds the local store, its schema, and its models, often [WatermelonDB](https://github.com/Nozbe/WatermelonDB), with more than 11,000 stars, or SQLite for relational data, plus a key-value store for small settings. A sync layer holds the mutation queue, the reconciler, and the network-status logic. And a features layer holds the screens and their hooks, which read and write through the data layer and never call an API directly. A rough shape is a `data/` folder, a `sync/` folder, and a `features/<name>/` folder per screen, with a thin `lib/` for shared utilities.

Naming the folders this way is half the battle, because it makes the wrong thing, a `fetch` inside a component, look obviously out of place.

## The UI reads local, always

The rule that makes everything else work: the UI reads from the local store, always, never from the network. Screens subscribe to reactive local queries, so when data changes, whether from a user action or a background sync, the view updates automatically. A write goes to the local store immediately and optimistically, so the interface responds at once, and the same write is recorded for the sync layer to send later. The user never waits on a request to see their own change, which is exactly what makes an offline-first app feel fast even online.

This is the inversion people miss. The network does not feed the UI; it feeds the local store, and the local store feeds the UI.

## The sync layer: queue and reconcile

The sync layer is where the network lives, isolated from everything else. Local writes append to a mutation queue, and when connectivity returns, the layer flushes that queue to the server in order, then pulls remote changes back into the local store. Two details make it trustworthy: mutations carry enough identity to be retried safely without duplicating, and conflicts have a defined resolution, last-write-wins for simple cases or per-field merging where it matters, rather than silently dropping one side. The same queue-and-reconcile discipline is the heart of a [WatermelonDB offline sync](/blogs/watermelondb-offline-app-sync-react-native/).

Keep this layer the only thing that knows about the server. When sync is the one place network logic lives, the rest of the app stays simple and testable.

## Why this matters when an AI agent builds it

An AI agent left to its own devices scatters `fetch` calls through components, because that is the most common pattern in its training data, and the result is an app that breaks the moment the network does. Handing the agent this architecture, the data, sync, and features split, turns vague prompts into a maintainable app: it puts queries in the data layer, network logic in the sync layer, and keeps screens reading local. This is the same up-front-structure benefit as starting from a solid base, like the [best boilerplate for React Native and Expo](/blogs/best-boilerplate-for-react-native-expo-2026/), and it pairs with moving off Expo Go to a [development build](/blogs/migrate-from-expo-go-to-development-build-ai/) so native storage works.

## Building it from a template

The folder layout, the local store wiring, the mutation queue, and the reactive screens are the same in every offline-first app, so they are worth starting from. A free [VP0](https://vp0.com) design gives you the screens already shaped to read from a local store, with a machine-readable source page, so pasting the link into Claude Code or Cursor lets the agent build the UI against the data layer instead of wiring fetches into components. The structure does the heavy lifting; the agent fills it in.

## Common mistakes building offline-first

The recurring ones come from online-first habits. Calling the network directly from components ties the UI to connectivity and breaks offline. Treating local storage as a cache rather than the source of truth keeps the app fundamentally online. Skipping the mutation queue loses writes made while offline. Having no conflict resolution silently drops changes on reconnect. And mixing network logic into screens instead of isolating it in a sync layer makes the whole app hard to reason about and test.

## Key takeaways: offline-first Expo architecture

- **It is structure, not a feature.** Build offline-first from the folder layout, not as a late patch.
- **Three layers, three folders.** Data, sync, and features, with screens that never call the network.
- **The UI reads local, always.** Reactive local queries and optimistic writes make it fast online and offline.
- **Isolate the network in the sync layer.** Queue mutations, reconcile on reconnect, resolve conflicts deliberately.
- **Give the agent the architecture.** A free VP0 template plus the data-sync-features split keeps an AI build maintainable.

## Frequently asked questions

**How do I structure an offline-first Expo app?** Use three layers in clear folders: a data layer with the local store, schema, and models, often WatermelonDB or SQLite plus a key-value store; a sync layer with the mutation queue, reconciler, and network-status logic; and a features layer with screens that read and write through the data layer and never call an API directly. The UI subscribes to reactive local queries, writes go to the local store optimistically and into the queue, and the sync layer flushes and reconciles on reconnect. A free template gives you the screens already shaped for this.

**What is the safest way to build this with Claude Code or Cursor?** Hand the agent the architecture explicitly, the data, sync, and features split, and a template for the screens. A free VP0 design has a machine-readable source page for screens that read from a local store, so Claude Code or Cursor builds the UI against the data layer instead of scattering fetch calls through components. That is the difference between an app that survives a dropped connection and one that breaks the moment the network does, which is the default an agent produces without the structure.

**Can VP0 provide a free React Native or Expo template for offline-first screens?** Yes. VP0 has free React Native designs with screens shaped to read from a local store rather than the network, each exposing an AI-readable source page. Because the screens are structured for a data layer, your agent wires them to SQLite, WatermelonDB, or your store of choice and to a sync layer, instead of building online-first screens you later have to retrofit for offline.

**Should the local database be the source of truth or a cache?** The source of truth. In an offline-first app the UI reads from the local store always, and the network syncs to and from that store in the background, so the user sees their own changes instantly and the app works with no connection. Treating local storage as a mere cache, where the network is still the real source, keeps the app fundamentally online and brings back all the loading-spinner fragility offline-first is meant to remove.

**What common errors happen when vibe coding an offline app?** Calling the network directly from components, treating local storage as a cache instead of the source of truth, and skipping the mutation queue so offline writes are lost are the frequent ones. Having no conflict resolution silently drops changes on reconnect, and mixing network logic into screens makes the app hard to test. Keep the UI reading local, isolate the network in a sync layer, queue mutations, and resolve conflicts deliberately.

## Frequently asked questions

### How do I structure an offline-first Expo app?

Use three layers in clear folders: a data layer with the local store, schema, and models, often WatermelonDB or SQLite plus a key-value store; a sync layer with the mutation queue, reconciler, and network-status logic; and a features layer with screens that read and write through the data layer and never call an API directly. The UI subscribes to reactive local queries, writes go to the local store optimistically and into the queue, and the sync layer flushes and reconciles on reconnect. A free template gives you the screens already shaped for this.

### What is the safest way to build this with Claude Code or Cursor?

Hand the agent the architecture explicitly, the data, sync, and features split, and a template for the screens. A free VP0 design has a machine-readable source page for screens that read from a local store, so Claude Code or Cursor builds the UI against the data layer instead of scattering fetch calls through components. That is the difference between an app that survives a dropped connection and one that breaks the moment the network does, which is the default an agent produces without the structure.

### Can VP0 provide a free React Native or Expo template for offline-first screens?

Yes. VP0 has free React Native designs with screens shaped to read from a local store rather than the network, each exposing an AI-readable source page. Because the screens are structured for a data layer, your agent wires them to SQLite, WatermelonDB, or your store of choice and to a sync layer, instead of building online-first screens you later have to retrofit for offline.

### Should the local database be the source of truth or a cache?

The source of truth. In an offline-first app the UI reads from the local store always, and the network syncs to and from that store in the background, so the user sees their own changes instantly and the app works with no connection. Treating local storage as a mere cache, where the network is still the real source, keeps the app fundamentally online and brings back all the loading-spinner fragility offline-first is meant to remove.

### What common errors happen when vibe coding an offline app?

Calling the network directly from components, treating local storage as a cache instead of the source of truth, and skipping the mutation queue so offline writes are lost are the frequent ones. Having no conflict resolution silently drops changes on reconnect, and mixing network logic into screens makes the app hard to test. Keep the UI reading local, isolate the network in a sync layer, queue mutations, and resolve conflicts deliberately.

---
*Published on the [VP0 Journal](https://vp0.com/blogs). Free to read, index and cite with attribution.*
