DevOS: how we built a multi-tenant SaaS for real estate developers

Real estate firms were running on spreadsheets, email chains, and disconnected legacy tools. DevOS replaced four to six systems with one platform — at 95% of the safety of full database isolation and 20% of the cost. Here's how the architecture works.

Most real estate development firms across Africa run on a stack that wasn’t designed — it accumulated. Project finance in spreadsheets. HR on a legacy system bought a decade ago. Governance documents in email threads. Operations split across whatever tools each department picked first.

This works fine when you’re managing one project. It collapses around the third one, when you can’t reconcile cash position across active developments without spending a week chasing PDFs.

DevOS is the platform we built to fix that. It’s a multi-tenant SaaS — one codebase, many developers, strict data isolation between them — covering the full real estate workflow from land acquisition through project delivery. Here’s how it works architecturally, and why each decision was made the way it was.

Context: Names of the client and specific deployment details are redacted under NDA. The architecture and approach are accurate.


The original problem, framed properly

“Real estate developers need better software” is a useless framing — every developer would agree, and none would buy software based on it. The actual problem, articulated by the people who’d use the system day-to-day:

  • Land acquisition lives in WhatsApp threads, scanned title documents, and a finance spreadsheet
  • Project financing sits in another spreadsheet that gets emailed around for sign-offs
  • HR runs on a tool from 2014 that nobody enjoys using but no one has time to replace
  • Governance — approvals, board minutes, audit trails — exists almost entirely in email
  • Operations is whatever WhatsApp group is most active that week

Every developer we spoke to had built their own ad-hoc version of this. None of them were happy with it. None could afford to pay enterprise SaaS prices ($50,000+/year for multi-developer real estate platforms in mature markets). Most importantly: none wanted their financial data sitting in a system alongside their competitors’ financial data, even with technical “isolation” assurances.

That last constraint shaped everything else.


Decision 1: Modular monolith, not microservices

The temptation with a multi-tenant SaaS covering this many domains (finance, HR, governance, operations) is to break it into services from day one. We didn’t.

A modular monolith with strict bounded contexts per domain gives you:

  • All the architectural discipline of microservices (each module has its own data model, its own internal API, no shared mutable state across module boundaries)
  • None of the operational complexity (one deployment, one database connection pool, no cross-service network calls in the hot path, no distributed-transaction nightmares)
  • An easy escape hatch — when a module legitimately needs to be split out later, the bounded context makes extraction surgical

We use this pattern because distributed systems are an answer to a problem most companies don’t have yet. Splitting too early costs you months of plumbing work and gives you back complexity instead of speed. For a multi-tenant platform serving 5-50 developer firms each, the modular monolith was clearly the right call.

The four core modules:

  • Finance — project costing, cash flow, payments, audit-grade ledger
  • HR — payroll, staff records, leave management
  • Governance — approvals, board minutes, document control with audit trails
  • Operations — project tracking, contractor management, site updates

Each module is its own bounded context with its own internal language. The Finance module doesn’t know what a “site visit” is. The Operations module doesn’t know what a “general ledger” is. They coordinate through events.


Decision 2: Schema-per-tenant Postgres, not row-level security alone

This is the architecture decision we spent the most time on. Multi-tenant SaaS isolation comes in three flavors:

  1. Row-level isolation — one shared database, one shared schema, every row tagged with tenant_id. Cheapest to operate. Highest risk of leak. A single buggy WHERE clause anywhere in the codebase can expose tenant A’s data to tenant B.

  2. Schema-per-tenant — one shared database, each tenant gets its own Postgres schema. Strong isolation at the SQL level. Same database server, same backups, same operational overhead. Costs are still pooled.

  3. Database-per-tenant — each tenant gets its own database, often its own server. Maximum isolation. Maximum cost. Maximum complexity.

For DevOS we chose schema-per-tenant with row-level security as a fallback. The rationale:

Why not row-level only: in real estate, the cost of leaking a competitor’s project financing data is enormous. A single Junior developer writing a query that forgets the tenant filter could cost the business its entire customer base. The technical risk doesn’t justify the operational savings.

Why not database-per-tenant: the operational overhead — backup orchestration, schema migrations across N databases, connection pool management — would have killed the price point. Real estate developers won’t pay $50K/year for software. They’ll pay $5-15K/year. That budget doesn’t support per-database infrastructure.

Why schema-per-tenant: strong isolation (you’d have to explicitly cross schemas with a malicious query, which doesn’t happen accidentally), reasonable operational overhead (Postgres handles thousands of schemas in one database without issue), and the cost profile fits the customer.

This is what we mean when we say “95% of the safety at 20% of the cost.” The math isn’t precise, but the trade-off is.


Decision 3: API-first, but no public API yet

DevOS is built API-first internally. Every UI action goes through the same REST endpoints that an external integration would. The front-end has no special privileges or backdoors.

But the public API isn’t exposed yet. Why?

Public APIs are a permanent commitment. The moment external customers start integrating with your API, you can’t change the contract without breaking them. That’s fine when you’ve stabilized the domain model — but in a young product still discovering what real estate developers actually need, locking down a public API contract prematurely is expensive.

The internal API exists. The contract is just versioned and consumed only by us. When customer integration demand crystallizes around specific use cases (most likely accounting-system sync first), we’ll expose those endpoints publicly with proper SLA and versioning policies.

This is a deliberate “build the right thing, expose it later” pattern. The architecture is API-first; the business model isn’t API-first yet.


Decision 4: Event-driven cross-domain coordination

Modules talk to each other through events, not direct calls. Concretely:

  • When a project closes in Operations, an event fires
  • Finance subscribes to that event, reconciles costs, and closes its own books for the project
  • Governance subscribes too, generating the project closure approval artifact
  • HR subscribes if any contractor reassignments are needed

The Operations module doesn’t know who consumes its events. The consumers don’t know about Operations’ internals. This loose coupling is what makes the bounded contexts work — you can change Finance’s data model without touching Operations.

Implementation-wise, this is in-process eventing (Postgres LISTEN/NOTIFY for some, simple pub/sub for others) — not Kafka or RabbitMQ. We don’t need durability across services because there’s only one service. We don’t need cross-data-center delivery guarantees because there’s one data center. The complexity matches the actual requirement, not the architecture diagram’s aesthetic.


Decision 5: Centralized API gateway with per-tenant rate limiting

A single API gateway sits in front of the modular monolith. It handles:

  • JWT authentication — tokens are stamped with tenant_id and user_role on issue
  • Per-tenant rate limiting — one developer firm with a runaway script can’t starve the others
  • Request logging with tenant context — every log line knows which tenant it belongs to, making support and incident response surgical
  • Schema selection — the gateway translates tenant_id from the JWT into the Postgres schema search path used for the request

This means the application code is mostly tenant-unaware. It writes queries against an abstract current_tenant schema, and the gateway/connection layer routes them to the actual schema for that tenant. Adding a new tenant becomes a schema creation + migration run, not a code change.


Decision 6: Tenant-branded dashboards with role-aware navigation

Each developer firm sees DevOS as their platform. Their logo, their primary color, their team’s names in dropdowns — not “DevOS” as the chrome.

This isn’t cosmetic. It addresses the same trust concern as schema-per-tenant isolation: developers feel like they’re using internal software, not a SaaS where they’re one tenant among many.

Role-aware navigation goes further. A finance lead sees finance-first views. A project manager sees operations-first views. The CEO sees a unified roll-up. The system doesn’t show modules a user doesn’t have permission for — they don’t exist in the user’s experience.

This is a progressive disclosure design philosophy: complexity reveals itself based on role and need, instead of overwhelming everyone with the full feature surface immediately.


What we got right, in retrospect

The schema-per-tenant decision. Every time we add a new tenant, we get a clean isolation story we can demonstrate to a security-conscious developer firm in 30 seconds. That’s worth a lot.

Starting with a modular monolith. Six months in, we could ship features in days. A microservices-from-day-one version would have spent that time on plumbing.

Bounded contexts that mirror the user’s mental model. Finance, HR, governance, operations — these are how the customer’s organization is structured. The software structure matches it. Users don’t have to translate between system concepts and business concepts.

Saying no to the public API on day one. We’ve talked to customers six months in who want integrations. We can now design those integrations against real demand instead of guessing.


What we’d do differently

Earlier investment in observability. The first time we needed to debug a tenant-specific issue, our logs didn’t reliably include tenant_id everywhere. Took a sprint to retrofit. Build observability with tenant context from week one.

More aggressive feature flagging. Some tenants want experimental features. Some want stability. We initially shipped one version to everyone. Adding a feature-flag layer per-tenant should have been week-one infrastructure, not month-four.

Different defaults for governance approvals. Our initial default was “two-step approval for any spend over X.” Some firms wanted three steps. Some wanted one. We hardcoded the defaults instead of making it a per-tenant config. Cost us two weeks to refactor.


Where DevOS goes next

The architecture supports several expansions that aren’t built yet:

  • Public API for accounting system sync (QuickBooks, Sage, Odoo) — high demand from customers running parallel finance tools
  • Mobile-first field updates for construction teams — currently web-responsive but not optimized for thumb-driven workflows
  • Inter-tenant collaboration for joint ventures between developers — explicitly architected for, never enabled, because no customer asked yet

The pattern across all of these: build what’s been asked for, not what’s interesting to build.


If you’re thinking about a multi-tenant SaaS, an internal platform that needs to scale to multiple business units, or just an honest opinion on whether your “we need a custom platform” instinct is the right call — start a conversation. You can also see our full case studies including E5 Solstice (solar ERP) and SwiftRoute (logistics platform), or read our framework for choosing custom vs. SaaS before you commit either way.

Ready to fix what's broken?

Tell us where the friction is. In a 30-minute strategy session, we'll diagnose the highest-leverage fix.

Book a Strategy Session