Hazem Salama
All work

MowazanaموازنةSole Engineer & Architect2026 · solo, both repos · Saudi Arabia

A multi-tenant cloud ERP and POS for Saudi businesses that signs every invoice and clears it with ZATCA.

Stack: React 18, Laravel 10 and MySQL 8. Arabic and English with full RTL, deployed on AWS with CI/CD.

Log into the demo tenant

Try it live

A seeded demo tenant on the live system, with the ZATCA sandbox flow and the POS. It is excluded from every production metric.

Admin / ERP login
Admin / ERP loginapp.mowazana-pos.com(opens in a new tab)
Useradmin-demo-showcase-co
PasswordMyiORKS7
POSThe POS opens from the top bar inside the ERP, using the same login.

Compliance the software has to carry

The problem

Saudi Arabia mandates electronic invoicing. Phase 2 moves it from a PDF with a QR code to signing every invoice and submitting it to a government gateway in real time: certificate onboarding, UBL 2.1 XML, XAdES-BES signatures over ECDSA secp256k1, a TLV-encoded QR, and a per-seller chain where each invoice embeds the hash of the one before it.

Most Saudi small businesses cannot build that, so it has to come inside the software they already run the business on. That software is a multi-tenant ERP: sales, purchasing, inventory, double-entry accounting and HR, shared by every tenant on one deployment, where no business may ever see another’s data and tenants who stop paying have to be handled without a person in the loop.

Three decisions

What was chosen, and what it cost

The path one invoice takes

  1. 1Onboard certificate
  2. 2Lock chain
  3. 3Build UBL 2.1
  4. 4Sign (XAdES-BES)
  5. 5TLV QR
  6. 6Submit
  7. 7Cleared or reported
A drawn invoice, not a real one. Step 2, the chain lock, is the subject of the first decision.

Serialise the invoice hash chain per tenant, and submit off the request

ChoseZATCA Phase 2 chains each seller’s invoices: every invoice embeds the hash of the one before it. A lockForUpdate on the tenant’s credential row, inside a transaction, assigns the counter, previous hash and UUID atomically before signing. Signing and submission then run as queued jobs on a dedicated zatca queue, with retry and backoff at 30, 120 and 600 seconds.
OverAssigning chain values without a lock and calling the gateway synchronously from checkout. Two concurrent invoices could then read the same previous hash and fork the chain, after which ZATCA rejects every later invoice from that tenant, and the POS would wait on the government gateway for each sale.
CostA tenant’s invoices take their place in the chain one at a time, however many cashiers are selling. An invoice is saved as pending before it is reported or cleared, so it can fail after the sale is done and has to be retried.

One shared schema, isolated by a single global scope

ChoseAll tenants share one MySQL schema and one deployment. A single TenantScope applied at the model layer, plus a tenant id bound into the JWT, filters every query by default, so a forgotten where clause cannot leak another business’s financials. Two-tenant isolation tests back it.
OverA database per tenant, which isolates physically but means provisioning and migrating a database for every signup. Or tenant filters written by hand in each query, where one omission is a leak.
CostIsolation is logical, not physical. Any query that goes around the model layer also goes around the scope, and removing a tenant means deleting its rows table by table, in child-to-parent order, instead of dropping a database.

Lock and wipe lapsed tenants on a schedule, with no operator

Chosesystemd-scheduled commands run the ladder daily. A tenant is locked to read-only at day 3 overdue, Arabic reminder emails escalate up to day 150, and at day 180 the tenant is wiped from the database and S3. Every step is logged and announced by email, and the wipe deletes child tables before their parents so foreign keys hold.
OverA person reviewing overdue accounts and locking or deleting tenants by hand. That keeps a human check in front of anything destructive, but the platform has one engineer and that work would fall to him.
CostThe wipe is irreversible and nobody approves it. The safeguards are the long gap between lock and wipe, the repeated emails and the logs, so a wrong billing state would lock a paying customer before anyone looked.

6 paying tenants in the first month

Result

Mowazana is in production. Every tenant gets the same suite, with sales and POS, purchasing, inventory, accounting, HR and ZATCA invoicing served by one Laravel API and one React SPA from a shared schema.

It shows that one engineer can carry Phase 2 compliance, tenant isolation and the billing lifecycle in a single codebase, provided the risky parts are decided once, at the chain lock, the tenant scope and the scheduler, and not screen by screen.

Screens

Captured on the demo tenant

ERP

Dashboard, double-entry accounting, inventory, purchasing and HR.

Admin dashboard: sales, receivables, purchases, and a sales-performance chart
Dashboard
Journal entry: debit Accounts Receivable, credit VAT Payable, with audit details
Journal entry (debit = credit)
Income statement: revenue, expenses, net profit
Income statement, derived from the ledger
Products master: SKUs, stock value, low and out-of-stock
Products

POS and invoicing

Checkout, the sale invoice, ZATCA onboarding and the VAT return.

Point of Sale: cart, customer, VAT, and payment
Point of Sale
Sale invoice detail with products, payment, profit and VAT
Sale invoice
ZATCA e-invoicing onboarding: OTP, CCSID/PCSID certificates, sandbox to production
ZATCA onboarding
VAT return: purchases, input VAT, and net VAT due to ZATCA
VAT return (net due)

Super-admin control plane

Applications, tenants, plans and subscription billing for the platform itself.

Super-admin dashboard: revenue, active and locked tenants, recent invoices and tenants
Control-plane dashboard
Tenant detail drawer: profile, tax docs (CR/VAT), national address, tenant login credentials
Tenant detail
Registration applications queue with approve, reject, and view actions
Registration applications
Subscription invoices with outstanding, collected, and paid totals and register-payment
Subscription invoices

By the numbers

Counted from the source

Code-verified counts
Eloquent modelsnote 273
Migrations106
Controllers77
Route files78
Services (8 ZATCA)14
Backend tests107
Frontend source files646
Redux feature modules50

Notes

  1. 1Reported, not code-derived: 6 paying tenants and about 10K SAR in the first month.
  2. 2Verified: two repositories, one author, counted from the source on 2026-06-15.