← Back to portfolio
Commerce and inventory ERP, multi-tenant SaaS product

SaraSoft

A commerce and inventory ERP, built for merchants whose network drops for days at a time, then reframed as an international SaaS product. Three deployments over a single database: Django/DRF backend, management application, online storefront. Roughly 60,000 lines of backend, 48,000 of management, 10,000 of storefront, 176 migrations and 107 test files.

Project type
Multi-tenant SaaS ERP, in active development
Scope
Django/DRF backend, management application, online storefront — three deployments, one database
Volume
~60,000 lines of backend · ~48,000 of management · ~10,000 of storefront · 176 migrations · 107 test files
Stack
Django 5, DRF, Next.js 16 / React 19, PostgreSQL (Neon), Tigris, WebAuthn, IndexedDB, service worker, Lengo Pay, Fly.io
SaraSoft

Nothing to configure

The intended users are uneasy with a phone, sometimes unable to read: everything is inferred from the data rather than set up. A single-site customer never sees the word “site”. And no individual customer decision is frozen into the code — currency, timezone, VAT and numbering are attributes whose defaults reproduce the existing behaviour.

Functional scope

Twelve Django applications: catalogue and packaging, multi-site inventory with batches, transfers and stock counts, the full sales chain (quote → order → delivery note → invoice → payment → credit note), procurement, customers, expenses, treasury, OHADA accounting (fiscal years, journals, entries, reconciliation), and the product’s own subscription with Orange Money / MTN mobile payment.

Three axes of access, never conflated

Am I allowed? Which records concern me? Has the company paid? Three distinct questions, three mechanisms, three answers: 403 for a denied permission, 404 or a filtered list for scope, 402 for a module outside the plan. Answering 403 for an unsubscribed module would send the administrator ticking boxes that change nothing. Scope applies to writes as much as to reads, and an empty scope grants nothing.

Four locks on tenant isolation

A manager that filters by company, a refusal to write outside the company, row-level security applied through a PostgreSQL migration, and a surface test that walks from every route to the model it serves and demands the company key. The fourth exists because a model lacking that key gave the first three nothing to hold on to — and none of them objected.

Offline without asking anything

The data key is drawn at 256 bits, never derived from a password, and wrapped by a non-extractable device key: a copied browser profile yields nothing but ciphertext. Twenty-two resources are replicated encrypted on the device, and the user is asked for nothing to work offline — the protection must cost them nothing. The fingerprint is a gateway, not an identifier.

A queue of intents, not states

The sync queue carries what the user meant to do: creating then deleting a record offline cancels itself out, the document number acts as the idempotency key, a rejection waits for a human decision, and network silence is never read as a refusal.

A storefront without a cart

The storefront handles neither cart nor order: it connects buyer and seller through a pre-filled WhatsApp link, and the transaction is settled in the conversation. The buyer’s number therefore never reaches the server — no buyer personal data exists in this product. What gets counted is the gesture: viewed, then contacted. A connection is not a sale, and no screen suggests otherwise.

Django 5DRFNext.js 16React 19PostgreSQL / NeonRLSRBACOHADA accountingWebAuthnIndexedDBService workerTigrisLengo PayFly.io
SaraSoft — diagramme / capture