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.
Noctis is a personal project — a multi-tenant SaaS for SMB commerce that brings point of sale, SRI electronic invoicing and inventory into one platform, with each business isolated at the database. It is in active development, not a shipped product.
02 — ARCHITECTURE
Clean Architecture in an Nx monorepo
Four deployable apps and twelve libraries, split across four bounded contexts — identity, auth, commerce and hr. Dependencies point inward: the domain knows nothing about the framework.
03 — TENANT ISOLATION
Isolation at the database, not the application.
Every request binds its tenant per transaction; PostgreSQL RLS filters each row before it leaves the database. Deny-by-default means 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.