DEV Community

Atul Srivastava
Atul Srivastava

Posted on

Building an Offline-First Multi-Tenant SaaS with React 19 and Dexie.js

When I set out to build ParkManager — a multi-tenant parking management SaaS — I knew "works offline" wasn't optional. Parking lots don't shut down when the internet goes out.

Here's how I architected an offline-first, multi-tenant application using React 19, Dexie.js (IndexedDB wrapper), and Appwrite as the cloud backend.


The Problem

Most SaaS apps assume always-on connectivity. But in the real world:

  • Internet can be spotty at parking lots
  • Attendants need to issue tickets regardless of connection
  • Payment records can't be lost
  • Multi-tenant data must stay isolated

The Architecture

Stack Overview

Layer Technology
Frontend React 19 + Vite
Local DB Dexie.js (IndexedDB)
Cloud Backend Appwrite
Desktop Electron
PWA Service Workers
Auth Role-Based Access Control (5 roles)

Offline-First with Dexie.js

The core idea: write locally first, sync to cloud when available.

// Dexie database schema
const db = new Dexie('ParkManagerDB');
db.version(1).stores({
  tickets: '++id, vehicleNumber, entryTime, exitTime, status, tenantId, synced',
  payments: '++id, ticketId, amount, method, tenantId, synced',
  users: '++id, email, role, tenantId'
});
Enter fullscreen mode Exit fullscreen mode

Every record gets a synced flag. When online, a background sync job pushes unsynced records to Appwrite:

async function syncToCloud() {
  const unsynced = await db.tickets
    .where('synced').equals(0)
    .toArray();

  for (const ticket of unsynced) {
    try {
      await appwrite.databases.createDocument(
        DB_ID, TICKETS_COLLECTION, ticket.id, ticket
      );
      await db.tickets.update(ticket.id, { synced: 1 });
    } catch (err) {
      console.log('Will retry on next sync cycle');
    }
  }
}

// Run sync every 30 seconds when online
setInterval(() => {
  if (navigator.onLine) syncToCloud();
}, 30000);
Enter fullscreen mode Exit fullscreen mode

Multi-Tenancy

Each record includes a tenantId. The Dexie queries always filter by tenant:

async function getTickets(tenantId) {
  return db.tickets
    .where('tenantId').equals(tenantId)
    .reverse()
    .sortBy('entryTime');
}
Enter fullscreen mode Exit fullscreen mode

On the Appwrite side, collection-level permissions ensure tenants can only access their own data.

5 RBAC Roles

ParkManager supports 5 distinct roles with different permissions:

  1. Owner — Full system access, can manage tenants
  2. Admin — Manage parking lots, users, and reports
  3. Operator — Day-to-day operations, view reports
  4. Attendant — Issue/close tickets, collect payments
  5. Accountant — View-only access to financial reports
const PERMISSIONS = {
  owner: ['*'],
  admin: ['manage_lots', 'manage_users', 'view_reports', 'manage_tickets'],
  operator: ['manage_tickets', 'view_reports'],
  attendant: ['create_ticket', 'close_ticket', 'collect_payment'],
  accountant: ['view_reports', 'view_payments']
};

function hasPermission(user, action) {
  const perms = PERMISSIONS[user.role];
  return perms.includes('*') || perms.includes(action);
}
Enter fullscreen mode Exit fullscreen mode

PWA + Electron = Everywhere

The same React codebase runs as:

  • PWA — installable on any device with a browser
  • Electron app — native desktop experience with system tray, auto-updates
  • Web app — standard browser access

The Electron wrapper is thin:

const { app, BrowserWindow } = require('electron');

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true
    }
  });

  // In production, load the built React app
  win.loadFile('dist/index.html');
}

app.whenReady().then(createWindow);
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Offline-first isn't hard — Dexie.js makes IndexedDB pleasant to work with
  2. Sync conflicts are the real challenge — use timestamps and "last write wins" for simple cases
  3. Multi-tenancy at the data layer — tenant isolation should be enforced at every query
  4. RBAC from day one — retrofitting permissions is painful
  5. React 19 + Vite — blazing fast dev experience with HMR

Try It


If you're building SaaS products that need to work in unreliable network conditions, offline-first architecture is worth the investment. The user experience improvement is dramatic.

What offline-first patterns have you used in your projects? Drop a comment below!

Top comments (0)