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

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.

◇ PENDIENTEIntended user and the problem Noctis targets — you provide.
SERVES
◇ PENDIENTE

SRI E-INVOICING
Scope: In progress

MODEL
Multi-tenant SaaS
POS · tablet1280 × 800 · drop image
Point of sale — counter checkout
Invoicingdrop image
SRI electronic invoice
Inventorydrop image
Inventory — real-time stock

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.

apps/ · 4 deployable + 3 e2e
apicommercebackofficeweb+3 e2e
libs/ · 12 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 filters each row before it leaves the database. Deny-by-default means 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.

1,600+
Automated tests
Across 259 spec files — domain, REST and e2e layers, run in CI on every commit.
54
Versioned migrations
Schema evolves forward-only; every change is reproducible.
19
Decision records
Non-obvious choices written down with their trade-offs.
19
REST controllers
The HTTP surface, split by bounded context.
7
RBAC roles
Access is role-scoped, enforced from gateway to row.
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.