Skip to content
Case study

A Lovable client portal where every tenant could read every other tenant

A document portal for accounting practices launched with generated Supabase policies that matched any authenticated user.Composite engagement. Details anonymised; figures are representative.

Headline result

11 → 0

tables readable across tenants

Platform
Lovable
Industry
B2B SaaS · Professional services
Duration
3 weeks
Findings
2 critical, 2 high, 2 medium
The situation

What we walked into

A solo founder built the portal in Lovable over about six weeks and launched it to sixty accounting practices.

The engagement started because of a security questionnaire. A larger practice wanted to move its whole client base onto the portal and asked, in one line, how tenant isolation was enforced.

Every table in the project had row-level security enabled, and the Supabase dashboard showed all of them green.

Engagement

Every engagement opens with a free consultation and a scoped audit. This one drew on the following:

Composite engagement. Details anonymised; figures are representative.

Production check

What we found

Severity is about consequence, not effort. A critical finding is one where a stranger can read data, move money, or destroy state.

2 Critical2 High2 Medium
  • Critical

    Policies that matched every authenticated user

    All fourteen tables had row-level security enabled with a single policy of the shape USING (auth.role() = 'authenticated').

  • Critical

    Service-role key shipped in the browser bundle

    A settings screen called an admin function directly from the client, and the service-role key was inlined into the built JavaScript through a VITE_ prefixed variable.

  • High

    Tax documents in a public storage bucket

    Uploads went to a public bucket under paths of the form uploads/<client-id>/<original-filename>.

  • High

    One Supabase project for development and production

    The local build, the preview builds, and the live app all pointed at the same project.

  • Medium

    No index on the tenant column

    The documents and activity_log tables were scanned in full on every dashboard load.

  • Medium

    The client list fetched the whole table

    The list view selected every column of every row and filtered in JavaScript, shipping 6.8 MB of JSON to render twenty-five visible rows.

Production ready

The work we did

Delivered over 3 weeks, on branches, alongside a founder who kept shipping product the whole time.

  1. 01

    Rewrote all fourteen row-level security policies around a workspace claim resolved from a membership table, with separate policies per operation instead of one permissive rule covering everything.

  2. 02

    Built a policy test suite that runs each table as three synthetic roles - workspace owner, member of a different workspace, and anonymous - and fails the build if any of them reads a row it should not.

  3. 03

    Rotated the service-role key, moved the four privileged calls into edge functions with their own authorization checks, and purged the key from the repository history.

  4. 04

    Moved documents into a private bucket behind short-lived signed URLs, with the storage policy keyed to workspace membership rather than to the path.

  5. 05

    Split the project into development, staging, and production, seeded staging with anonymised data, and enabled point-in-time recovery on production.

  6. 06

    Ran a full restore drill end to end and wrote it up as a runbook: fourteen minutes from decision to a verified copy of the database.

  7. 07

    Added composite indexes on the two hot tables and moved the client list to server-side pagination with explicit column selection.

  8. 08

    Stood up CI with per-branch preview deploys, the policy tests as a required check, error tracking, and a one-command rollback.

Measured

Before and after

The same metrics, measured the same way on both sides of the work. Where a number got worse, it is in the table.

  • Tables readable across tenants

    11 of 140 of 14

    Verified by an authorization test suite that runs on every pull request.

  • Dashboard p95 load

    4.2 s780 ms

    Same data volume. Indexes and pagination account for most of the change.

  • Client list payload

    6.8 MB94 KB

    Server-side pagination plus selecting the eight columns the view renders.

  • Secrets in the client bundle

    20

    Service-role key and a third-party API key, both rotated and scoped per environment.

  • Verified database restore

    Never attempted14 min

    Rehearsed end to end, then written up so the founder can run it alone.

  • Monthly infrastructure cost

    $210/mo$240/mo

    Three separate projects and point-in-time recovery cost more than one project with neither.

Composite engagement. Details anonymised; figures are representative. Figures are rounded from what we measured across the engagements this study is drawn from.

What it runs on now

The builder stayed in the picture where it earned its place. Nothing here was rewritten for the sake of rewriting it.

  • Lovable
  • React + Vite
  • Supabase Postgres
  • Supabase Auth
  • Supabase Storage
  • Edge Functions
  • GitHub Actions
  • Sentry
  • Vercel
The part that stayed with me is that the dashboard said row-level security was on. It was on. It just did not mean what I assumed it meant.
Founder, B2B SaaSParaphrased from a composite engagement and attributed by role only. No individual or company is identified.

Same builder, same symptoms?

Send us a repo link and a paragraph about what it does. The audit is free and the report is yours either way.

Book a Free Call