SaaS platform · 2026
A multi-tenant retail ERP and POS SaaS for small shops
One codebase, many shops, zero data leakage: tenant isolation enforced at the ORM layer, permissions instead of roles, tokens that can't be forged into another store.
- Client
- Confidential (Middle East retail)
- Industry
- Retail / point of sale
- My role
- Architect and lead engineer, API and web app
- Year
- 2026
Overview
A multi-tenant ERP and point-of-sale SaaS for small and medium shops: products, stock, sales, staff and reporting per organisation, with an owner / manager / cashier permission model, all on a NestJS 11 + Prisma 7 + PostgreSQL 16 API and a Next.js 16 web app.
The problem
Multi-tenancy is where SaaS products quietly leak data. A single missed `where organizationId = …` in one of hundreds of queries and one shop can read another's sales. The system had to make that class of bug impossible rather than merely unlikely, while staying fast enough for a cashier at a till.
What I built
- Tenant identity carried only in the access token. organizationId is never read from a request body, header or query string.
- A `TenantGuard` that re-resolves the membership from the database on every request, so a deactivated employee or a suspended shop loses access immediately, not at token expiry.
- Permission-based guards (not role-based), backed by a permission catalogue, so roles can change without touching code.
- A Prisma client extension that refuses any query against a tenant-owned table that does not filter by organizationId, so a forgotten filter fails loudly in development instead of leaking in production.
- Argon2id passwords; refresh tokens stored as SHA-256 digests, rotated on use, with reuse revoking the whole token family.
- One response envelope, stable error codes, Swagger in non-production, rate limiting, scheduled jobs, S3 uploads, Redis caching, e2e tests against a dedicated database.
Outcome
0
Ways to query a tenant table without a tenant filter
3
Seeded roles (owner, manager, cashier) on one permission catalogue
e2e
Tests against a real Postgres, not mocks
6
Architecture, database, API, permissions, security and local-dev docs
Engineering decisions
- Tenant isolation as a compile-and-runtime invariant, not a convention
- Permissions over roles
- Refresh-token family revocation
- Six-document architecture and security spec, including what is deliberately not claimed