Store — SaaS ERP
A multi-tenant ERP that lets retailers run sales, purchasing, stock and subscriptions end-to-end — without paperwork.

Context
Many retailers still run their business on notebooks or spreadsheets: rough stock counts, invisible margins, no traceability. Store set out to deliver an accessible, affordable web and mobile ERP covering the full retail cycle — from restocking to checkout — while staying easy to adopt.
It is a SaaS product: each company gets its own isolated space, can run several stores and employees with distinct roles, and subscribes to a plan. I designed and built the entire backend and the system architecture.
Architecture
The backend follows a Domain-Driven Design approach: each of the 17 business modules is a self-contained context, split into four strict layers. This separation keeps the domain core independent from the framework and infrastructure details, and makes every module testable and maintainable in isolation.
Multi-tenant isolation
Isolation relies on an application-level strategy rather than a schema-per-tenant setup. On login, a JWT carries the tenant identity (company and store), role and permissions. Every query is then systematically scoped to that perimeter, guaranteeing a company never reaches another's data.
The perimeter is hierarchical: the company is the root tenant, and each company can run several stores. A single PostgreSQL database holds all data — a deliberate choice that simplifies operations and keeps costs in check at the target scale, while leaving the door open to stronger isolation if needed.
Frontend & full-stack consistency
The frontend (Next.js 16 / React 19 in TypeScript) follows the same discipline as the backend: a feature-based layout mirroring the business modules, and a shared layer split into domain / application / infrastructure / presentation. This front/back symmetry keeps the system predictable — the same boundaries appear across the whole stack.
Server data is handled with TanStack Query (cache, invalidation), client state with Zustand, and forms with React Hook Form validated by Zod. The UI is built with Tailwind CSS and shadcn/ui, is bilingual (next-intl) and installable as a PWA — delivering the web and mobile access promised to retailers.
Key modules
Sales & checkout
Full order cycle, FIFO stock consumption, instalment payments, anonymous walk-in customers and till summaries.
Purchasing & suppliers
Supplier orders from draft to receipt, multiple receptions, traceable batches and split payments.
Stock & inventory
Batch tracking with expiry dates, average-cost valuation, movement journal and physical inventories with profit/loss reports.
SaaS subscriptions
Plans (Trial, Standard, Pro) with limits, coupons and promotions, manual payment validated against proof (Wave, Orange Money).
Security & RBAC
JWT authentication, refresh tokens, data-driven access control: 85+ permissions and roles customisable per company.
Reporting & audit
Dashboards (top products, margins, stock valuation) and an audit log whose scope adapts to the user's role.
Challenges & solutions
- Challenge
Model very different users — an ADMIN running the platform, an owner running their company, employees tied to its stores — each linked to distinct entities, without multiplying authentication or authorization mechanisms.
SolutionSeparate identity from access. A Person → Utilisateur hierarchy (JOINED inheritance) captures who the user is and what they belong to: the owner carries their company, the employee their store. Facing it, a single Account entity holds the credentials and points to a role bearing permissions. The result: admin, owner and employees all authenticate through the same entry point and are authorized by the same RBAC — every endpoint checks a permission, never the user's concrete type. System roles stay non-delegable, and the resolved scope (company, store) is projected into the JWT for uniform authorization.
- Challenge
Guarantee that no company can ever see another's data on a shared database.
SolutionThe tenant (company + store) is carried in the JWT and enforced on every query through the security context. Perimeter scoping is centralised to prevent leaks by omission.
- Challenge
Know the stock value at any time and trace every unit, including perishable goods.
SolutionA FIFO batch stock model (lot number, expiry date, purchase price) with a timestamped movement journal and expiring-batch detection. Valuation uses the average purchase price.
- Challenge
Offer fine-grained, role-specific permissions without recompiling for every new need.
SolutionA data-driven RBAC: permissions and roles live in the database, roles are customisable per company, and each endpoint declares the permission it requires.
- Challenge
Collect subscriptions while local payments go through Wave, Orange Money or transfer, outside the app.
SolutionA manual-payment subscription flow: the customer uploads proof, an admin approves or rejects, and activation is computed (plan, duration, promotion, coupon). The model is ready for an automated gateway when the time comes.
- Challenge
Keep quality high across a broad scope and keep the database under control.
Solution741 tests run against a real PostgreSQL database, a SonarQube quality gate (≥ 80% coverage on new code), a schema versioned by 49 Flyway migrations, all shipped through a GitLab CI pipeline and a multistage Docker image.
Tech stack
- Java
- Spring Boot
- Spring Security
- Spring Data JPA
- JWT
- PostgreSQL
- Flyway
- TypeScript
- React
- Next.js
- Tailwind CSS
- shadcn/ui
- TanStack Query
- Zustand
- React Hook Form
- Zod
- PWA
- JUnit 5
- Mockito
- JaCoCo
- SonarQube
- Vitest
- Docker
- GitLab
- CI/CD
- Railway
- Vercel
Want to know more?
I can walk you through the architecture in detail and answer your technical questions.
Contact me