Hazem Salama
Hazem Salama
Multi-tenant ERP · Saudi Arabia
Mowazana is a multi-tenant SaaS ERP, POS, and ZATCA Phase 2 e-invoicing platform for Saudi businesses. One Laravel 10 API and one React 18 SPA serve every tenant from a shared schema with row-level isolation, handling the full SaaS lifecycle (apply, onboard, bill, auto-lock, auto-wipe) on top of a complete ERP (sales, purchasing, inventory, double-entry accounting, HR) and a government-compliant e-invoicing engine that signs and clears every invoice in real time.
One platform: run the business, clear the invoice with ZATCA.
فاتورة ضريبية
Tax Invoice · UBL 2.1
PIH chained
XAdES-BES · secp256k1
~10K SAR
First month, ~6 paying tenants (reported)
73
Models on one tenant scope
Phase 2
ZATCA clearance + reporting
62
Tables cascaded on tenant wipe
Try it live
A seeded demo tenant on the live system. Real ZATCA sandbox flow, real POS, excluded from every production metric.
Open app.mowazana-pos.comAdmin / ERP login
The POS opens from the top bar inside the ERP, using the same login.
The front door
A public, ZATCA-certified landing page where a business applies in a few steps and tracks the request by reference number. Each application lands in the super-admin queue, where one approval provisions a fully seeded tenant.





The problem
Saudi Arabia mandates electronic invoicing, and Phase 2 raises the bar from “generate a PDF with a QR” to “cryptographically sign every invoice and submit it to a government gateway for clearance or reporting in real time.” That means certificate onboarding, UBL 2.1 XML, XAdES-BES signatures over ECDSA secp256k1, a TLV-encoded 9-tag QR, and a per-seller hash chain where every invoice embeds the hash of the previous one.
Most Saudi SMBs cannot build that. They need it bundled into the software they already use to run the business. But multi-tenant ERP is itself hard: dozens of interrelated modules, sales, purchasing, inventory, double-entry accounting, HR, all have to be isolated per tenant so one business can never see another’s data, while still sharing one deployment.
And it is a SaaS, so the unglamorous lifecycle matters as much as the features. Tenants apply, get onboarded, get billed, and when they stop paying must be warned, locked, and eventually wiped, automatically and safely, with no human in the loop.
The signature problem · ZATCA Phase 2
Eight dedicated services turn a sale into a cryptographically signed, government-cleared document. The chain is serialised per tenant so it can never fork under concurrency.
Onboard
CSR + ECDSA secp256k1 key, CCSID then PCSID
Lock chain
lockForUpdate on the credential row
Build UBL 2.1
ICV + PIH + UUID into the XML
Sign
XAdES-BES over secp256k1
TLV QR
Phase 2, 9 tags
Submit
clearance/single (B2B) or reporting/single (B2C)
Cleared
pending → reported | cleared | failed
The hardest correctness problem: each invoice embeds the previous one’s hash (PIH). A lockForUpdate on the tenant credential row inside a transaction serialises ICV/PIH assignment, so two simultaneous invoices can never read the same previous-hash and split the chain.
One SPA · five surfaces
A single React 18 SPA renders five distinct surfaces over one Laravel API. Pick one.

The full ERP: sales, POS entry, purchasing, inventory, double-entry accounting, HR, settings, and ZATCA onboarding, in a hand-built navy/lime StitchShell.
Inside the ERP
Every tenant gets the same complete suite: sales and POS, purchasing, inventory, a real double-entry ledger, ZATCA and VAT, HR, and reporting. A few screens from each, captured on the demo tenant.
Quotes to cleared invoices, a touch POS, returns, customers, and collections.


Suppliers and accounts payable, purchase invoices, returns, and payments.


Product master with cost, price, VAT, stock levels, categories, and reorder.


A real ledger: chart of accounts, balanced journal entries, and derived statements.


The compliance core: ZATCA onboarding, VAT rates baked into the QR, and the VAT return.


Staff and the {action}-{resource} permission model, plus the tenant’s own billing.


The SaaS control plane
A dedicated super-admin portal at /superadminportal: onboard tenants, approve applications, set plans, bill, and watch the system. Every business metric excludes the demo tenant at the query level.


The automated lifecycle
systemd-scheduled commands run the dunning ladder daily. Every step is logged and announced by Arabic email, and the wipe is ordered child-to-parent so foreign keys stay intact.
Day 0
Invoice issued
Day 3
Auto-lock (read-only)
Day 30 → 150
Escalating Arabic dunning
Day 180
Wipe across 62 tables + S3
Signature features
Per invoice: build UBL 2.1, XAdES-BES sign over secp256k1, emit the TLV 9-tag QR, then route B2B to clearance/single and B2C to reporting/single. Eight dedicated services under app/Services/Zatca/.
Each invoice embeds the previous invoice’s hash. A lockForUpdate on the tenant credential row inside a transaction serialises ICV/PIH assignment, so two concurrent invoices can never read the same previous-hash and split the chain.
Every transaction posts balanced debit/credit against sub-accounts. Trial balance, income statement, and balance sheet are derived from the ledger, not stored, so the books always reconcile.
One TenantScope global scope plus a JWT-bound tenant id makes isolation the default for every query, so forgetting a where clause cannot leak another business’s financials. Backed by two-tenant isolation tests.
systemd-scheduled commands escalate Arabic reminders (days 3 / 30 / 45 / … / 150), auto-lock at day 3 overdue, and wipe across 62 ordered tables plus S3 at day 180, child-to-parent for foreign-key safety.
A separate throttled cashier login, shift open/close, and thermal plus A4 Blade receipt templates, each carrying the ZATCA QR.
Hosted-page checkout with IPN signature verification. TenantBillingService generates and finalises the subscription invoices that drive the whole lifecycle.
Arabic-primary with a full English mirror, Tajawal/Cairo plus Poppins, navy/lime, on a hand-built StitchShell rather than an off-the-shelf admin theme.
Arabic Blade emails for invoice created/paid, reminders, lock, wipe, and one-time tenant credentials, all sent off the queue so the request stays fast.
The ERP underneath
Sales
Quotes, invoices, returns, customers, per-tenant numbering
POS
Cashier auth, cart, shifts, thermal + A4 receipts
Inventory
Products, variants, categories, UoM, stock + adjustments
Accounting
Double-entry ledger, COA, derived statements
Hard parts solved
Problem · Two concurrent invoices reading the same previous-hash break the per-seller chain, and ZATCA then rejects every later invoice from that tenant.
Solution · Serialise per tenant with a lockForUpdate on zatca_credentials inside a transaction; assign ICV/PIH/UUID atomically before signing (ZatcaService.php).
Problem · A synchronous clearance call would stall the POS while it waits on the government gateway.
Solution · Signing and submission run as queued jobs on a dedicated, worker-prioritised zatca queue with retry/backoff (30/120/600s); the invoice carries a pending → reported/cleared/failed state.
Problem · A single missed where clause anywhere in the codebase leaks another business’s financials.
Solution · One TenantScope applied at the model layer plus a JWT-bound tenant id, so isolation is the default and forgetting it per-query is impossible. Backed by isolation tests.
Problem · Auto-locking and wiping lapsed tenants risks destroying real data with no human in the loop.
Solution · A staged, logged, email-announced cascade: warn, lock at day 3, escalate to day 150, wipe at day 180, with the wipe ordered child-to-parent across 62 tables to respect foreign keys.
By the filesystem
Two repositories, one author. Every number below was counted from the source on 2026-06-15.
73
Eloquent models
0106
Migrations
177
Controllers
278
Route files
314
Services (8 ZATCA)
4107
Backend tests
5646
Frontend source files
650
Redux feature modules
7Reported (not code-derived): roughly six paying tenants and ~10K SAR in the first month.
Let's build the next one