SesaHub
An internal multi-tenant ERP — companies, sites, people, stock and a general ledger in one Laravel and React app, scoped so one company cannot see another.
2025
- Client
- Nyande ERP Solutions (internal)
- Type
- Multi-tenant ERP
- Stack
- Laravel 11, React, TypeScript, Inertia.js, Tailwind, PostgreSQL
- Team
- Dominic Nyande, business analyst; Amy Nyande, data analyst









Beyond another imported ERP
The work was one login across companies, sites and books — not a Western suite with the extra modules left on.
SesaHub is Nyande ERP Solutions, an internal family product. There is no external client. TechCorp Solutions Ltd in the general ledger shot is seeded test data, not a customer.
Initial proposal
Buy or rent an existing ERP and live with modules written for a different market, or keep companies, stock, people and books in separate tools.
Discovery
Multi-company isolation had to be the permission model, not a filter on a shared table. Sites needed codes. Employees belonged to sites. Stock was per site. Currency had to convert inside the same app. The older public write-up listed a purchase-order pipeline — request, approval, shipment, receipt — that is not a finished workflow. Core financials were missing from that write-up and are in the running build: chart of accounts, journals, trial balance, fiscal calendar.
Product response
Laravel 11 with React, TypeScript and Inertia. Company-scoped RBAC and policies. Services for company, site, employee, customer, supplier, product and currency. Docker on Render with PostgreSQL. I designed and built the screens. Dominic Nyande was the business analyst; Amy Nyande was the data analyst.
Mapping the reality.
Enterprise records first, then the books that have to match them.
Built December 2024 to October 2025. Companies, sites, people, customers, suppliers, parts and currency, then core financials. The purchase-order request-to-receipt path was described on an older page; it is not shipped here. Screens are from the running app on seeded test data. There is no companies-list still yet — that module will not borrow another screen.
Problem
A family product for more than one company cannot share a single admin table and hope a dropdown is enough. Permissions, currency and the books have to start from the company, or isolation is theatre.
Solution
A company record with company-scoped RBAC and policies. Creating a company initialises its financial system rather than leaving the ledger as a later bolt-on.
Before
- Separate tools per company, or one database everyone can see
- Permissions as a UI filter
After
- Company-scoped policies
- Cross-company roles for consultants without shared rows
- Financials initialised with the company
Problem
Stock, people and fulfilment are per site. If a site is only a name on a company, inventory and employees have nothing reliable to join to.
Solution
A sites resource on the company. SiteService issues the code. Employees and part inventory hang off that record.
Before
- Location as a note on the company
- Hand-assigned site codes
After
- Site list under the company
- Auto-generated codes
- Employees and inventory join to the site
Problem
HR sitting in a spreadsheet next to the ERP means site assignment, access and the employee record drift apart.
Solution
An employees resource with site assignment, generated IDs, department and status, tied into the same user and RBAC model as the rest of the app.
Before
- Staff lists off-system
- No join from a person to a site
After
- Employee IDs issued by the service
- Site assignment on the record
- Login and role on the same person
Problem
Inventory and books need counterparties. Without customer and supplier records, orders and purchases have nowhere to attach.
Solution
Customers with addresses and customer orders. Suppliers as their own resource. That is the party layer. It is not a finished purchase-order workflow.
Before
- Counterparties in a sheet or in someone’s head
After
- Customer records, addresses and orders
- Supplier records
- Purchase-order steps still a later module
Problem
A quantity on the company hides which site can fulfil. Categories and identifiers have to survive more than one location.
Solution
Parts, product categories and part inventory. Stock levels live on the site. SKU and barcode sit on the part.
Before
- One stock number for the whole company
- Categories remembered, not stored
After
- Part records with SKU and barcode
- Category taxonomy
- Inventory rows per site
Problem
Multi-company work across Ghana and other markets cannot price and book in a single hard-coded currency.
Solution
A currencies resource plus convert and update-rates. Overrides stay available per company and transaction rather than only as a global default.
Before
- One currency assumed for every company
- Rates looked up off-system
After
- Currency records in the app
- Conversion and rate-update endpoints
- Company and transaction overrides
Problem
If the books live in another product, the ERP is a set of lists. Journal entries and a fiscal calendar have to belong to the company that owns the stock.
Solution
Core financials as a first-class route group: accounts, ledger, journals, trial balance, account analysis, fiscal calendar. Double-entry in the same tenant as the enterprise records. The screenshot is the TechCorp test company with zeroed balances.
Before
- Books in a separate tool, or not at all
- No fiscal period on the company
After
- Chart of accounts and general ledger
- Journal entries and trial balance
- Fiscal calendar on the same login
Problem
The public write-up listed an end-to-end purchase workflow. Shipping that copy as a finished module would have been a gallery of empty laptop frames.
Solution
Supplier records and customer orders are in the build. The request → approval → purchase → shipment → receipt path is not. It stays marked in progress.
Before
- A written PO pipeline with no screen to show
After
- Supplier and order foundations in place
- PO workflow not presented as shipped
04 / Deep dives
Three problems that were not a dashboard skin
The older page talked about African enterprises and empty MacBooks. The work that mattered was company-scoped access, a ledger in the same app, and not publishing a purchase-order story the routes do not support.
From a shared table to company-scoped access
Problem
An ERP for more than one company fails the moment a consultant can see another company’s employees or books. A company dropdown on a global query is not isolation.
Contribution
I built companies as the tenant. Policies and RBAC are company-scoped. Consultants can hold roles across companies without sharing rows. Creating a company starts its financial system rather than leaving a blank ledger for later.
Constraints
- No external client brief to hide behind — this is an internal product.
- Dominic Nyande was the business analyst; Amy Nyande was the data analyst.
- Data isolation had to hold for employees, sites, stock and the ledger, not only the company list.
Scope by policy, not by a filter in the UI
CompanyRBACService and policies sit on the records. A missed where clause should not be the only thing keeping tenants apart.
Initialise the books with the company
A company without a chart of accounts is a list. Financial setup runs when the company is created.
Employees list. Company is a column on the record — TechCorp and East Africa Trading are seed data, not a shared staff table.
Outcome
Shipped. Companies, sites, people and the ledger share the same tenant rules.
05 / Design to code
Where building protected the design
The information architecture is the tenant: company, then sites, people, parties, parts and books. Laravel, Inertia and React are how those records stay on one login. TypeScript pages under Enterprise and Core Financials are the screens. Docker on Render is how it shipped.
Policies on the company, not a global admin flag
RBAC and company policies are the isolation. A consultant’s cross-company role is still a role on a company, not a pass to every table.
Let services issue the codes
Site codes and employee IDs come from the service layer so lists stay joinable when someone forgets a naming convention.
Keep conversion in the app
Currency convert and update-rates are endpoints, not a spreadsheet beside the ERP.
- Laravel 11
- PHP
- React
- TypeScript
- Inertia.js
- Tailwind CSS
- PostgreSQL
- Docker
- Render
06 / Outcome and status
Where the product stood
SesaHub shipped on Render as Nyande ERP Solutions, October 2025, after work that started in December 2024. The running app is seeded with test data. There is no external client and no named quote. The live URL is not on this page until there is one to give. Purchase-order request-to-receipt is not claimed as shipped.
Evidence
- Deployed on Render with Docker, nginx, PHP-FPM and PostgreSQL.
- Company-scoped RBAC across companies, sites, employees, customers, suppliers, products, currencies and core financials.
- General ledger, chart of accounts, journal entries and fiscal calendar are in the running app. The screenshot is TechCorp test data. Purchase-order request-to-receipt is not.
What's next
Finish the purchase-order workflow. Capture module screenshots from the running app. Add a live link if a shareable URL exists. The mobile app, voice and AI items on the older page are later work, not this release.
Reflection
The honest version of this project is an internal ERP, built in order: tenant, then sites and people, then stock and currency, then the books. I would still start with isolation. I would not publish a purchase-order gallery I cannot show, and I would not describe MemberPress as this stack.
