PERSONAL PROJECT·IN ACTIVE DEVELOPMENT·2026
Noctis Commerce
A multi-tenant SaaS I'm building for SMB commerce — point of sale, SRI electronic invoicing and inventory, designed so one codebase can serve many isolated businesses.
01 — CONTEXT & PRODUCT
One platform for sales, invoicing and inventory.
A multi-tenant SaaS for commerce, built so one codebase runs many isolated businesses — each with its own companies, branches and warehouses. It unifies catalog, point of sale, valued multi-warehouse inventory, purchasing and customer/supplier ledgers; SRI e-invoicing and IESS payroll are modeled and come next. In active development, not a shipped product.
For the owner-operator — and their cashiers, warehouse and back-office staff — who today split the operation across a POS, spreadsheets and separate SRI filings.
02 — ARCHITECTURE
Clean Architecture in an Nx monorepo
Four deployable apps and thirteen libraries, split across four bounded contexts — identity, auth, commerce and hr. Dependencies point inward: the domain layer knows nothing about the framework. The data model spans 49 Prisma models and 56 forward-only migrations.
03 — TENANT ISOLATION
Isolation at the database, not the application.
Every request binds its tenant per transaction; PostgreSQL RLS — 37 deny-by-default policies across 36 tables — filters each row before it leaves the database. An unset tenant returns nothing, not another tenant's data.
-- bind the tenant per transaction (tx-local, parameter-bound) SELECT set_config('app.current_tenant', $tenant, true); ALTER TABLE commerce.products ENABLE ROW LEVEL SECURITY; ALTER TABLE commerce.products FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON commerce.products USING (public.is_platform_admin() OR tenant_id = public.current_tenant_id()); -- unset GUC → current_tenant_id() IS NULL → deny by default
04 — EVIDENCE
The numbers, with what they mean.
05 — DECISION RECORD
Adopt PostgreSQL Row-Level Security in force + deny-by-default. Each request binds app.current_tenant per transaction via set_config; every operational-table policy filters with is_platform_admin() OR tenant_id = current_tenant_id().
Every operational row carries a tenant_id. Filtering by convention alone was one forgotten WHERE — or one raw-SQL / psql query — away from a cross-tenant leak, the highest-severity risk for a shared-database SaaS.
Isolation holds at the database even when application code forgets its filter; an unset tenant yields zero rows, not another tenant's data. Cost: a per-transaction GUC and RLS-aware migrations, compatible with RDS Proxy transaction pooling — accepted.