When we started designing REVOLT's data model, we had a decision to make. Do we build for one store and figure out multi-store later? Or do we architect for multi-tenancy from day one?
We chose day one. Here's why and how.
The hierarchy
REVOLT's data model has three levels: Company, Store, Register.
A Company is the top-level tenant. It owns the subscription, the users, and the global settings. Poco Loco Supermercado is a company. A future chain of five convenience stores would also be a company.
A Store belongs to a company. It has its own address, its own operating hours, its own employees, and critically its own product catalog and pricing. The same item might be $3.49 at one location and $3.99 at another. The store is where business logic lives.
A Register belongs to a store. It's a physical terminal running the Electron POS app. Each register has its own API key, its own transaction history, its own local database. When a register syncs, it only pulls data relevant to its store.
Why this matters for small retailers
You might think multi-tenancy is an enterprise concern. It's not. A lot of independent grocery store owners have more than one location. My uncles run one store, but their friends in the same business often run two or three. These aren't chains with corporate IT departments — they're families running multiple storefronts.
Without multi-tenancy, each store needs its own completely separate system. The owner logs into three different dashboards to see how business is doing. Products get managed independently even when most of the catalog is shared. Employee information lives in separate silos.
With REVOLT's architecture, the owner logs in once and sees all their stores. They can set a product's base price at the company level and override it per store when needed. They see consolidated reports or drill down by location.
Prisma and row-level isolation
We use Prisma as our ORM, backed by PostgreSQL. Every row that belongs to a tenant has a companyId and — where applicable — a storeId. Every query is scoped to the authenticated user's tenant context automatically.
The API extracts the tenant context from the JWT on every request using an AsyncLocalStorage-based request context service. Controllers and services never need to manually pass company or store IDs around — they're available globally within the request lifecycle. This makes it hard to accidentally query across tenant boundaries.
Company (Poco Loco Supermercado)
├── Store (Main St Location)
│ ├── Register 1
│ └── Register 2
└── Store (Oak Ave Location)
└── Register 1
The tradeoffs
Multi-tenancy from day one adds complexity to every migration, every query, every seed script. It slows you down in the early days when you only have one customer. We accepted that tradeoff because retrofitting multi-tenancy into an existing system is significantly harder and riskier than building with it from the start.
The schema enforces the hierarchy with foreign keys and cascading deletes. You can't create a store without a company. You can't create a register without a store. The database won't let you break the model even if the application code has a bug.
What's next
We're currently building out store-level isolation for employee management and time tracking — making sure a cashier at one location can't accidentally clock in at another. The hierarchy gives us the foundation; the feature work is applying it consistently across every module.
One codebase, one deployment, many stores. That's the goal. REVOLT scales down to a single register at a neighborhood grocery and up to a multi-location operation without changing the architecture.