All case studies

Case study · Enterprise SaaS · Multi-tenant architecture

Designing a modular, multi-tenant enterprise platform

AxisOne Cloud · Enterprise SaaS · Multi-industry

How AxisOne Cloud was designed as one data model, twelve modules and seven roles: licences activated per tenant, isolation by row-level security on every table, and an integration hub built on hashed API keys and signed webhooks.

  • 12 modules — 7 core, 5 add-ons
  • 7 roles with their own navigation
  • 37 tables isolated per tenant

Status In production Website: axisonecloud.com

Context

AxisOne Cloud started from a question I kept meeting in project-driven businesses: why does a company that runs projects, subcontractors and bank instruments need five tools that do not talk to each other? The first brief targeted construction — bills of quantities, subcontracting, letters of guarantee and letters of credit are its daily instruments. Within the same working session I widened it to any industry: the core — accounts, projects, invoices, tasks, documents, approvals — is the same everywhere; only the add-ons are sector-specific. That distinction between a universal core and licensed add-ons became the founding decision of the platform, and the reason it is called modular.

Challenge

Build one product that behaves like a different product for each customer, without forking the code. Three things had to hold at once. Isolation: several companies share one database, and no query may ever cross a tenant boundary — not in the application, not in file storage, not through the API. Licensing: a module that a tenant has not activated must disappear from its routes, menus and dashboard, and the same catalogue must drive the platform console that activates it. Roles: seven roles with different jobs — from platform owner to external partner — each needing its own navigation and its own data rights. And the whole thing had to be open to the outside, because a platform like this rarely replaces an ERP; it sits next to one.

My role

Founder, architect and designer. I wrote the specification of the platform as structured briefs: the tenant, module and tenant-module model; the licensing rule — core always on, add-ons per tenant; the named roles and what each one sees; the layered separation between authentication, tenant context, route guards and data hooks; and the integration hub — connectors, mapping profiles, jobs, event queue, webhooks, API keys, audit. I made the technical calls along the way: moving the database to our own project early, closing the sign-up, treating every security alert as blocking, splitting the marketing site from the application on two subdomains. My team at SOFT M POWER CANADA carried the implementation and operates the platform.

Architecture / approach

Three tables carry the whole licensing model: a module catalogue with a core flag, the tenants, and the activation of a module for a tenant. A tenant context loads the company and its active modules once; a route guard and a role-based navigation hook read from it, so a non-licensed module is neither reachable nor visible. Isolation is not left to the application: every business table carries the tenant identifier, row-level security is enabled everywhere, and the tenant and role checks are definer functions with a fixed search path, reused by every policy. Child tables are protected through their parent; file storage is partitioned by tenant folder. Roles live in their own table, apart from user profiles, so a user can never elevate their own rights by editing their profile.

Technology

A React and TypeScript single-page application with a typed query layer and generic hooks for pagination and tenant-scoped data; a PostgreSQL backend with built-in authentication, private object storage and server-side functions written in TypeScript. Thirty-seven tables, four exposed SQL functions, three trigger functions — one of them a generic audit trigger that snapshots the row before and after each change, reused on six entities. The integration hub runs on an outbound event queue with pending, sent, failed and dead-letter states, a dispatcher that signs each delivery with HMAC-SHA256 and retries three times, and inbound and outbound REST functions authenticated by hashed API keys with a visible prefix. Fifteen applied migrations describe the entire schema.

Execution

The first release of the application and of its public site was specified and built in December 2025, in a specification-driven loop: each capability was written as a brief, implemented by my team with AI-assisted development tooling, then reviewed against one rule — nothing counts as delivered until it is visible in the interface. Two coverage audits against the original specification were run before the site was started. The public site follows the same core-versus-add-on structure, with a five-page product presentation and a sales assistant that qualifies enquiries. The last steps were closing the sign-up behind a company code and configuring the two custom subdomains, one for the site and one for the application.

Constraints

Moving the database from the hosting environment of the initial build to our own PostgreSQL project meant abandoning the first schema and rewriting it; the dead migration is still in the repository as a record. Three security-scanner alerts — customer accounts readable across roles, profile emails exposed, leaked-password protection — were each treated as blocking and closed with hardened row-level policies in two iterations. The transactional email service rejected the runtime’s package import and, in test mode, any recipient other than the account owner: both were solved by calling its API directly and routing to an internal mailbox. An interface regression introduced by a refactor was caught and reverted the same day.

Business impact

What the platform provides today: one multi-tenant data model with 37 isolated tables; a catalogue of 12 modules — 7 core, 5 add-ons — activated per company; seven roles with their own navigation; CRM, projects, tasks, billing, payments, documents and an approval queue; construction and trade-finance add-ons for bills of quantities, tenders, subcontracts, letters of guarantee and letters of credit; a reporting dashboard; and an integration hub with REST import and export, hashed API keys, signed webhooks and an audit journal. The marketing site presents the same core-and-add-on structure to prospects. The architecture is the one I now reuse when a client needs a platform that serves several organizations from a single codebase.

Key takeaways

  1. Decide what is core and what is an add-on before the first table: that split is the product, and data model, licences, navigation and pricing all follow from it.
  2. Isolation belongs in the database, not in the application: row-level security with definer functions, reused by every policy, is cheaper to prove than a hundred filtered queries.
  3. Own your database from day one; migrating it later costs a schema.
  4. A security scanner is a free reviewer: treat every alert as blocking and close it with a migration, not a patch.
  5. Nothing is delivered until it is visible in the interface — that rule keeps a specification-driven build honest.