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'
});
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);
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');
}
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:
- Owner — Full system access, can manage tenants
- Admin — Manage parking lots, users, and reports
- Operator — Day-to-day operations, view reports
- Attendant — Issue/close tickets, collect payments
- 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);
}
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);
Key Takeaways
- Offline-first isn't hard — Dexie.js makes IndexedDB pleasant to work with
- Sync conflicts are the real challenge — use timestamps and "last write wins" for simple cases
- Multi-tenancy at the data layer — tenant isolation should be enforced at every query
- RBAC from day one — retrofitting permissions is painful
- React 19 + Vite — blazing fast dev experience with HMR
Try It
- Live Demo: park-my-vehicle.vercel.app
- GitHub: github.com/atul0016/park-manager
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)