MERX Systems, an Iano product · 2026, actively developed
MERX
Self-hosted, multi-tenant ERP platform with pluggable Accounting, Inventory, and Supply modules.
Built + maintained by Iano
Visit live site ↗Summary
MERX is a self-hosted, multi-tenant ERP platform built as a Turborepo monorepo on Next.js 15 and Postgres. It ships three first-party business modules (Accounting, Inventory, and Supply) on top of a pluggable module system, so new capabilities self-register into the shell without touching the core. Tenant isolation, role-based permissions, an audit log, and a branded PDF document engine are built into the platform layer.
Context
Businesses needed an ERP they could run themselves, covering accounting, inventory, and supply, without SaaS lock-in, and extensible enough to add their own modules rather than being boxed into a fixed feature set.
Role & team
Iano, a two-person marketing agency I co-founded. Both founders engineered the build.
Stack
- Next.js 15 (App Router, RSC, Server Actions)
- TypeScript (strict)
- pnpm + Turborepo
- PostgreSQL 16
- Drizzle ORM
- Redis 7 + BullMQ
- MinIO (S3-compatible)
- Auth.js v5
- Tailwind + shadcn/ui + Radix
- react-hook-form + Zod
- @react-pdf/renderer + Puppeteer
- Nodemailer + React Email
- Caddy
- Vitest + Playwright
Key decisions
- Chose a pluggable module registry over a monolith: each module ships a manifest ({ key, nav, permissions, templates, hooks }) and the Next.js shell auto-mounts navigation, routes, permissions, and PDF templates at boot, so adding a module is `cp` + manifest, not core surgery.
- Chose AsyncLocalStorage-based tenant isolation over ad-hoc filtering: a TenantContext is set per request and every query is scoped by org_id via requireOrgId(), making isolation a platform guarantee rather than a per-query discipline.
- Chose Drizzle ORM over a heavier ORM for type-safe, migration-first schema work with drizzle-kit.
- Chose self-hosted Docker + Caddy over a managed SaaS deploy to deliver the “run it yourself, auto-HTTPS on your own domain” promise.
Architecture
A single Next.js shell (`apps/web`) hosts all modules; shared packages provide the database (`db`), auth/RBAC/audit (`auth`, `core`), UI primitives (`ui`), and the PDF engine (`docs`). Tenant context flows through AsyncLocalStorage; RBAC is permission-string based (e.g. `accounting.write`) resolved per membership, throwing ForbiddenError when missing. Documents are React-PDF templates registered by key and rendered with shared brand styling. Background work runs on Redis + BullMQ; files live in MinIO; email goes through Nodemailer/React Email; Caddy fronts the production stack with automatic HTTPS.
Challenges
- Guaranteeing tenant isolation: Across every query, without leaking data, this is solved with a request-scoped TenantContext and mandatory org_id scoping enforced in @merx/core.
- Making the platform genuinely extensible: Solved with self-registering module manifests that the shell auto-mounts at boot, so modules contribute nav, routes, permissions, and document templates declaratively.
- A reusable, on-brand document pipeline: Solved with a keyed React-PDF template registry (with a Puppeteer fallback) shared across modules.