RENXW CRM: a multi-tenant agency CRM with permissions enforced in PostgreSQL

How I built the RENXW multi-tenant agency CRM with React, Supabase and PostgreSQL RLS: 86 tables, 88 permissions in the database, 834 automated tests.

Client
RENXW (my own agency)
Role
Co-founder and technical lead
When
Ongoing
Stack
React, Vite, Supabase, PostgreSQL, Row-level security, Netlify, GitHub

Visit the live site

RENXW multi-tenant CRM, screenshot
86database tables
88permissions enforced in the database
15system roles
834automated tests
In short

RENXW CRM is a multi-tenant agency CRM I built as co-founder and technical lead of RENXW. It runs on React, Vite and Supabase, with 15 system roles and 88 permissions enforced in PostgreSQL row-level security. It covers leads, pipeline, projects, a client portal and an encrypted credential vault, backed by 834 automated tests.

RENXW CRM is a multi-tenant agency CRM that I designed and built as co-founder and technical lead of RENXW. It runs on React, Vite and Supabase, and every one of its 88 permissions is enforced in PostgreSQL row-level security rather than in the interface, with 834 automated tests checking that it stays that way.

The problem

An agency runs on a chain of steps: a lead comes in, someone calls, follow-ups get scheduled, a deal moves through the pipeline, the client is onboarded, a project starts, a website gets built, and the client wants to see progress. Spread across spreadsheets and separate tools, that chain breaks at every handover.

We also wanted the system to serve more than one organisation. That makes it multi-tenant, and multi-tenant changes the stakes: one bug in access control means one client seeing another client's data, including credentials.

Constraints

  • Many tenants, one database. Data for different organisations sits side by side and must never leak.
  • Fine-grained roles. Agencies have owners, sales, developers, clients and more. We ended up with 15 system roles and 88 permissions.
  • Sensitive data. Client logins and keys live in the system, so the vault had to be encrypted.
  • Small team. The stack had to be something a small team can deploy and maintain: React and Vite on Netlify, Supabase for the database and auth, GitHub for code.

What I built

Multi-tenant data model

The schema has 86 tables across 32 migrations. Every tenant-owned row carries its tenant, and the row-level security policies use it. Migrations are versioned so the schema can be rebuilt from scratch in any environment.

Permissions in the database

Roles and permissions are data, and RLS policies check them on every read and write. The interface asks the same rules so it can hide what a user cannot do, but the interface is a convenience, not the guard.

Agency workflow

Across 16 screens, the CRM covers leads, calls, follow-ups, pipeline, onboarding, projects, website-build tracking, a client portal and reporting. It also has API integrations and automation hooks so other tools can react to events.

Encrypted credential vault

Client credentials are stored encrypted, behind the same tenant and role rules as everything else.

Test suite

There are 834 automated tests, including 425 tenant-isolation probes that try to cross tenant and role boundaries and fail the build if they succeed.

How it works

  1. A user signs in through Supabase auth and gets a session tied to their tenant and role.
  2. The React app requests data directly from Supabase.
  3. PostgreSQL applies row-level security to every query, checking tenant and permission.
  4. Rows the user is not allowed to see are never returned, and writes they are not allowed to make are rejected.
  5. Automation hooks and integrations fire on events such as a new lead or a stage change.
  6. On every change, the test suite and isolation probes run before anything ships.

Decisions and trade-offs

RLS in the database versus checks in the UI. UI checks are quick to write and easy to read, but they only control what a user sees, not what they can request. With Supabase the client talks to the database directly, so any rule that lives only in React is a suggestion. Putting the rules in RLS means a forged request gets the same answer as the real UI. The cost is that policies are harder to write and debug, and a slow policy slows every query. I accepted that, and wrote about the pattern in Supabase RLS for multi-tenant SaaS.

Permissions as data, not code. Storing 88 permissions in tables means changing a role does not need a deploy. The trade-off is more joins inside policies, which I keep in mind when indexing.

Probes, not just unit tests. Ordinary tests check that features work. Isolation probes check that the wrong person cannot make them work. For a multi-tenant system, the second kind matters more, so 425 of the 834 tests are probes.

Supabase over a custom backend. A hand-written API server would give more control but double the surface to secure. Supabase plus RLS keeps the security model in one place.

Outcome

The CRM is built and running for RENXW with 86 tables, 32 migrations, 15 system roles, 88 permissions enforced in the database, 16 screens and 834 automated tests including 425 tenant-isolation probes. It handles the agency chain from lead to client portal in one system.

What I would do next

Phase 2 is planned in 5 waves: a unified inbox, payments, AI agents, a website builder and white-label support. The white-label wave depends most on the tenant model being right, which is why the isolation work came first. For the other half of RENXW's engineering, the website side, see the RENXW WordPress theme case study.

If you need a CRM or SaaS product built this way, see my custom web app development service or get in touch.

Questions people ask

What is the RENXW CRM?

It is a multi-tenant CRM for running an agency: leads, calls, follow-ups, pipeline, onboarding, projects, website-build tracking, a client portal, reporting and an encrypted credential vault. It is built with React, Vite and Supabase on PostgreSQL, with 86 tables, 15 system roles and 88 permissions enforced in the database.

Why enforce permissions with row-level security instead of in the UI?

UI checks only hide buttons. Anyone with a valid session can still call the API directly. Row-level security puts the rule on the data itself, so a query from the wrong tenant or role returns nothing, whichever client sent it. With 88 permissions and many tenants, that is the only place I trust to hold the line.

How do you know tenants cannot see each other's data?

The CRM has 834 automated tests, including 425 tenant-isolation probes. Those probes act as one tenant or role and try to read or change data that belongs to another. If any probe succeeds, the test fails. Isolation is checked on every change rather than assumed.

Can you build a custom CRM for my business?

Yes. I build custom CRMs and SaaS products with React, Supabase and PostgreSQL, with security rules in the database and automated tests from the start. Scope drives time and cost, so we begin with the workflows you actually use. See my custom web app development service for how I work.

Need something like this?

Want results like these?

Tell me what you are building. I reply within one working day.

Start a project