<Joan Marcos />
Back to Case Studies
Live
Case Study

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.

Founder & Lead Engineer2026 – Present
40+Businesses on the desktop edition
6Backend modules
Aug 2026In production
v0.3.1Current version

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

Next.js dashboard & POS
Typed API client (OpenAPI)
ASP.NET Core API
Domain modules
PostgreSQL (schema per module)

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

.NET 8
ASP.NET Core
Entity Framework Core
MediatR
FluentValidation
PostgreSQL 16
Next.js
TypeScript
Turborepo
Docker
Clerk
Resend
AWS S3

What's next

  1. Manager authorization for sensitive POS actions
  2. Automated test coverage across modules
  3. A local-first desktop POS with Tauri that keeps selling offline
  4. Accounting

Updates & changelog

September 24, 2026
v0.3.1

Tenant-isolation hardening

  • Tightened organization checks on cash register and image endpoints
  • The sidebar shows the organization's logo
September 24, 2026
v0.3.0

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
September 23, 2026
v0.2.0

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
August 28, 2026
v0.1.0

MVP in production

  • Catalog, inventory, purchases, sales and POS
  • Multi-tenant organizations with roles, permissions and invitations
  • Reports, notifications and image uploads
April 20, 2026

Cloud rewrite kickoff

  • Started the cloud edition as a Turborepo monorepo with a .NET API and Next.js apps