Context#
Organisations with several branches end up running on a collection of tools that do not talk to each other: one spreadsheet for people, another for accounts, a WhatsApp group to coordinate, and the memory of whoever has been there longest for everything else.
The problem is not a lack of software. It is that every piece of data lives somewhere different and nobody knows which one is right. When someone asks how many people are active, or how much came in last month, the answer depends on who you ask.
SIGEDReino exists so there is a single source of truth, with this domain's particularities: several independent organisations on the same platform, internal hierarchies that determine who sees what, and money involved, which does not tolerate approximations.
Constraints#
- Multi-tenant from day one. Not a future feature: each organisation must be incapable of seeing another's data, and that shapes both the data model and the security model from the first line.
- Three clients, one domain. Admin panel, public portal, and mobile app. Maintaining three definitions of the same concept guaranteed they would diverge.
- Small team. Decisions requiring continuous operations — databases to administer, infrastructure to watch — are paid for in time not spent on product.
- A domain still being understood. Several modules changed shape during construction, so the architecture had to tolerate the business changing its mind.
Architecture#
A monorepo with pnpm and Turborepo. A NestJS backend with hexagonal architecture per module, three client applications, and shared packages carrying the contract and the domain primitives.
Every backend module has the same structure, held together by a single rule:
domain/ imports nothing, application/ imports domain/, infrastructure/
imports both. Never the other way round.
Execution#
Twenty domain modules — people, memberships, treasury, governance, events, notifications, billing, among others — each with its own entities, use cases, and error codes.
Use cases return a Result rather than throwing. A broken business rule is not a
program error, it is a possible outcome; treating it as an exception forces you
to wrap half the codebase in try/catch just to tell "the expense was not
pending" apart from "the database went down".
All three clients consume the same api-contracts, so renaming a field breaks
their builds in the same commit. With one trap that took a while to find: the
backend externalises that package when bundling, so it can import its types
but not its values at runtime. The enums it needs live duplicated in its
domain, with a test that fails if the two copies drift.
The interface comes from an in-house design system: one for web on Radix and Tailwind, another native on NativeWind. They share no components — they cannot — but they do share tokens, type scale, and variant names.
Outcome#
The system is in active development, with the identity, people, membership, and treasury modules running in a test environment.
What can be stated already is structural: twenty modules with tool-enforced boundaries, three applications sharing a domain without duplicating it, and three languages whose parity is validated by a test on every run. Usage figures will come when the system reaches production, and this case will be updated then with real data rather than projections.