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.

ROLE
Solo project — architecture & build
STACK
NestJS · Next.js 14 · PostgreSQL · Nx
REPOSITORY
Private — architecture shown here
THE APP ITSELF

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.

SERVES
Retail of any size — from SMBs to multi-company, multi-branch groups.

SRI E-INVOICING
Scope: In progress

MODEL
Multi-tenant SaaS
POS · tablet
Total$ ——
Charge
Point of sale — counter checkout
Invoicing
$ ——
SRI ✓
SRI electronic invoice
Inventory
128
34
6
0
Inventory — real-time stock

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.

apps/ · 4 deployable + 3 e2e
apicommercebackofficeweb+3 e2e
libs/ · 13 libraries · 4 bounded contexts
identity
Domain · users, tenants
Application · permissions, org
Infrastructure · Prisma, RLS
auth
Domain · sessions, policy
Application · JWT, argon2id
Infrastructure · guards
commerce
Domain · products, sales
Application · POS, SRI, stock
Infrastructure · repos, outbox
hr
Domain · employees
Application · labor params
Infrastructure · repos
PostgreSQLRow-Level Security · deny-by-default

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.

request
JWT → RequestContext
set_config
app.current_tenant · per tx
tenant_isolation
deny-by-default
-- 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.

~2,000
Automated test cases
Unit tests per layer, API E2E against a real PostgreSQL under the restricted runtime role, and Playwright UI E2E — all running in CI.
37
RLS policies
Deny-by-default Row-Level Security over 36 tables, verified by an E2E suite that runs under the same restricted role.
86
REST endpoints
Exposed by 21 controllers, split by bounded context — each behind a permission guard.
75
Atomic permissions
Fail-closed RBAC across 14 modules — 7 role templates copied per tenant, with resolution cached in Redis.
19
Decision records
Non-obvious choices written down with their trade-offs.
Current state of a project in active development — a snapshot of the codebase, not a final result.

05 — DECISION RECORD

ADR-002
Tenant isolation — Row-Level Security + RequestContext
Accepted · amended 2026-06-10
DECISION

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().

CONTEXT

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.

CONSEQUENCE

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.