ECA Suite
From desktop POS to multi-tenant SaaS
How I rebuilt a desktop POS and inventory suite used by 40+ businesses as a multi-tenant cloud platform: a .NET modular monolith with Clean Architecture and DDD, a Next.js dashboard and POS, and a lean production setup.
The problem
ECA started as two desktop applications, an inventory system and a touch POS, built in C# with SQL Server and DevExpress. They are used by 40+ businesses, but every install is local: updates are manual, data lives on one machine, and an owner can't check the store from anywhere else.
The goal of the cloud edition: one platform where an organization manages its locations, its team and its sales from the browser, while each business's data stays isolated from every other.
The approach
I chose a modular monolith over microservices. It ships as a single deployable, which keeps operations simple for a small team, but each module owns its domain, its database schema and its public contracts, so it can be split out later without a rewrite.
Inside each module the code follows Clean Architecture: Domain, Application, Infrastructure and Endpoints. Use cases are commands and queries dispatched through MediatR, validated with FluentValidation, and they return explicit results instead of throwing for expected errors.
Architecture
Identity
Organizations, members, roles, permissions and invitations.
Catalog
Products, recipes, nested categories and units of measure.
Commerce
POS, cash registers, shifts, sales, purchases, returns and customers.
Inventory
Locations, stock per location, movements, transfers and shrinkage.
Media
Image uploads stored on S3 or disk.
Notifications
In-app inbox and email.
Key decisions
Tenant isolation is explicit
Every handler filters by organization on purpose, instead of relying on a global query filter that is easy to forget. Missing a filter shows up in review, and an isolation audit before v0.3.1 tightened the remaining endpoints.
Modules talk through contracts
Each module exposes a Contracts project with queries and integration events. No module reaches into another's internals or tables, which keeps the boundaries real.
One typed contract, backend to frontend
The TypeScript API client is generated from the .NET OpenAPI spec and shared across the monorepo, so a breaking API change fails the frontend build instead of production.
Optimistic concurrency on sales
Sales use PostgreSQL's xmin column as a concurrency token, so two concurrent edits to the same sale fail loudly instead of silently overwriting each other.
Business days follow the organization
Reports, cash register closes and reference numbers cut the day in the organization's time zone, not UTC. Before this, evening sales in Havana landed on the next day.
Infrastructure
- API in Docker on a VPS behind Coolify and Traefik, with automatic TLS
- PostgreSQL 16 with a daily backup sidecar (14-day retention) plus full-server snapshots
- Dashboard deployed on Vercel
- Clerk for authentication, Resend for email, S3 for media
Tech stack
What's next
- Manager authorization for sensitive POS actions
- Automated test coverage across modules
- A local-first desktop POS with Tauri that keeps selling offline
- Accounting
Updates & changelog
Tenant-isolation hardening
- Tightened organization checks on cash register and image endpoints
- The sidebar shows the organization's logo
Organization logo and time zones
- Organization logo on receipts, report headers and the POS
- Per-organization time zone that defines the business day for reports, closes and reference numbers
- Daily database backups with 14-day retention
- Image library and roles scoped to the current organization
Purchase returns and a permission-aware UI
- Purchase returns, with or without a linked purchase
- The dashboard hides actions the user's role can't perform
- Cash register management from the dashboard
- Retries for transient database connection failures
MVP in production
- Catalog, inventory, purchases, sales and POS
- Multi-tenant organizations with roles, permissions and invitations
- Reports, notifications and image uploads
Cloud rewrite kickoff
- Started the cloud edition as a Turborepo monorepo with a .NET API and Next.js apps