
Learn how to build privacy-friendly offline-first web apps using RxDB, local databases, and seamless replication.
When building modern web applications, we often default to assuming a reliable, lightning-fast internet connection. We spin up a remote server, hook up an ORM like Prisma or Mongoose, and fetch data over HTTP on every single user action. But what happens when the network drops? Or when a user is on a spotty mobile connection while traveling?
Users hate loading spinners. They expect their apps to open instantly, work without a network connection, and sync seamlessly in the background when connectivity returns. That is where offline-first architecture comes in.
In my work building full-stack apps, I have found that putting the local database first changes everything. Instead of querying a remote server as the primary source of truth, your app queries a local browser database. In this guide, I want to show you how to set up an offline-first web app using RxDB, a fantastic NoSQL database for JavaScript applications.
Designing for offline-first is not just about reliability; it is also about privacy and speed. When user data lives primarily on the device inside a local storage engine, you reduce exposure to server breaches. It also puts the user in control of their data.
When I built Nept Track, my offline habit tracker and budget planner, local-first architecture was essential. Users want their personal financial records and daily habits to load instantly without waiting for an API response, and they want complete privacy. Using a local database allowed the app to run smoothly offline while keeping private data secure.
Traditional apps fetch data like this:
Offline-first apps flip this entirely:
Tagged with
Leave a comment
Raw IndexedDB is powerful, but working with its native API directly can be painful. You have to manage database upgrades, transaction scopes, and verbose callbacks. It is easy to end up writing dozens of lines of boilerplate just to fetch a single document.
Libraries like Dexie.js make IndexedDB easier, but RxDB takes it a step further by providing a reactive database built on top of storage primitives like IndexedDB, OPFS (Origin Private File System), or LokiJS. Because it is reactive, you can subscribe to query results just like you would with a state management library.
When a document changes anywhere in the app, your UI updates automatically. This removes the need for complex global state management or manual cache invalidation.
Let us walk through setting up RxDB with the IndexedDB plugin in a modern web application.
First, install the required packages:
npm install rxdb rxjs
Next, define your database schema. RxDB uses JSON Schema to validate all data before it enters the local storage engine. This ensures your local state remains clean and type-safe.
import { createRxDatabase, addRxPlugin } from 'rxdb';
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';
import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';
// Define the schema for a notes or tasks collection
const taskSchema = {
title: 'task schema',
version: 0,
primaryKey: 'id',
type: 'object',
properties: {
id: {
type: 'string',
maxLength: 36
},
title: {
type: 'string'
},
completed: {
type: 'boolean'
},
updatedAt: {
type: 'number'
}
},
required: ['id', 'title', 'completed', 'updatedAt']
};
export async function initDatabase() {
const db = await createRxDatabase({
name: 'myofflineappdb',
storage: wrappedValidateAjvStorage({
storage: getRxStorageDexie()
})
});
// Add collections
await db.addCollections({
tasks: {
schema: taskSchema
}
});
return db;
}
Once your database is initialized, interacting with it feels familiar if you have used MongoDB or Mongoose. Because RxDB is reactive, you can turn any query into an observable stream.
Here is how you can fetch and subscribe to tasks in a frontend component:
import { useEffect, useState } from 'react';
export function useTasks(db) {
const [tasks, setTasks] = useState([]);
useEffect(() => {
if (!db) return;
// Subscribe to query results
const subscription = db.tasks
.find({
selector: {
completed: { $eq: false }
},
sort: [{ updatedAt: 'desc' }]
})
$.subscribe(documents => {
setTasks(documents.map(doc => doc.toJSON()));
});
return () => subscription.unsubscribe();
}, [db]);
return tasks;
}
When a user adds or checks off a task, you simply write to the local collection:
async function addTask(db, titleText) {
await db.tasks.insert({
id: crypto.randomUUID(),
title: titleText,
completed: false,
updatedAt: Date.now()
});
}
Your UI updates instantly without waiting for any server response. The user experience feels snappy and native, even if they are offline on an airplane.
An offline-first app is only half-finished if it never syncs back to the cloud. RxDB provides built-in replication plugins that make it straightforward to sync local collections with a remote CouchDB-compatible endpoint, a GraphQL server, or a custom REST backend.
For most custom SaaS products, setting up a replication endpoint involves:
RxDB handles conflict resolution, batching, and retry logic when the connection drops and reconnects.
If you are planning to build your next product using an offline-first approach, keep these practical tips in mind:
Building offline-first web apps with local databases like RxDB gives your users the speed, reliability, and privacy they deserve. By treating the device as the primary database and the cloud as a sync target, you can create applications that feel instant and bulletproof.
Need help architecting an offline-first web app, mobile app, or custom SaaS product for your business? Get in touch and let us build something great together.