Test your Supabase RLS. Get a signed report.

RowAttest runs real attempts to reach other users' and other organizations' data in your Supabase app, and signs what it finds.

Free: one Supabase project, 10 runs a month. A signed PDF report with a public verify link is $49 per run. No card is needed to start.

Why this exists

One missing policy is the whole breach.

State 1 of 3: Enforced.

With row-level security enforced, Tenant A's request for Tenant B's rows is refused by the database itself.

This is not rare. UpGuard Research checked about 300,000 domains that showed signs of Supabase use and found 16,326 databases with tables readable with the app's public key. Over half of those databases showed signs of personal data.

Sources: UpGuard Research, “Everything Everywhere: Systemic Data Exposure in Supabase Apps” (published 2026-10-14) · TechCrunch, “Some Supabase customers are publicly exposing reams of people’s data to the web” (2026-09-25)

RowAttest runs these attempts for real and signs the result.

  • Tenant enclosure
  • Rows
  • Request
  • Policy gate
  • Signed report

Watch us break in

A cross-tenant attack, one request at a time.

A seeded attacker signs in to one tenant and reaches for another tenant's data on Tasklio — a demo fixture built by RowAttest, not a customer. Every request below is a real shape the engine sends. Scroll to run it.

  1. Read Initech's tasks

    GET /rest/v1/tasks?org_id=eq.<initech>&select=*

    DENIED row-level security returned zero rows

  2. Read Initech's projects

    GET /rest/v1/projects?org_id=eq.<initech>&select=*

    DENIED the SELECT policy is scoped to the caller's own org

  3. Plant a row inside Initech

    POST /rest/v1/tasks { "org_id": "<initech>", "title": "…" }

    DENIED the INSERT policy's WITH CHECK refused the foreign org_id

  4. Overwrite one of Initech's tasks

    PATCH /rest/v1/tasks?id=eq.<row> { "title": "changed-by-b" }

    LEAK HTTP 200 — the row changed. The UPDATE policy had no WITH CHECK.

One missing policy is all it takes. Get a signed answer for your app.

Create a free account

The taxonomy

The 8 ways row-level security breaks.

RowAttest plants each of these against the Tasklio demo fixture one at a time, then measures whether the engine catches it. Tap a card for the minimal change that opens the gap.

Each change is a real reversible operator from the engine's mutation suite (apps/tasklio/mutants). Six of the eight are caught; the two that are not are reported honestly, not hidden.

Nobody else publishes this list because nobody else runs it against your app.

Create a free account

Sample attestation

What a report looks like, and how to check it hasn't been altered.

A sanitized excerpt for a fictional app. The signature underneath is real: your browser checks it against the demo public key. Edit the report and it stops verifying.

Sample · synthetic data RA-SAMPLE0001

RowAttest Attestation Report

RA-SAMPLE0001 · SAMPLE

Sample. Synthetic data throughout. “Tasklio” is a fictional demo application built by RowAttest to show the report format. Names and identifiers are placeholders; the signature in the block below is a real Ed25519 signature over this excerpt's canonical JSON, made with a demo key.

1 · Header

Project ref
tasklio-demo (fictional)
Environment
declared as staging by the customer
Run window (UTC)
2026-09-18 14:02:11 → 14:09:47
Engine
v0.1.0-sample
Issued by
RW Digital Ventures LLC (RowAttest)

2 · Executive summary

1 of 93 evaluated boundaries failed: a seeded attacker in one tenant could reach another tenant's data. Details in the findings section.

  • 92

    Pass

  • 1

    Fail

  • 12

    Not evaluated

  • 0

    Error

92 checks passed.

Coverage: 7 of 8 tables evaluated (84 cells) plus 1 storage bucket (9 cells); 1 table not evaluated (12 cells, unknown tenancy).

5 · Finding F-1 · cross-tenant UPDATE on tasks

What was accessed
A task row belonging to tenant A (synthetic row rowattest_synthetic_task_a1) was modified by a user in tenant B
As whom
Seeded user attacker-b1@rowattest-seed.example (tenant B, role: member)
Reproduction
PATCH /rest/v1/tasks?id=eq.<synthetic-id> with tenant-B access token, header Prefer: return=representation; body {"title": "changed-by-b"}
Evidence
HTTP 200; response body contains the modified row (1 row returned, synthetic row only)
Detected by
Layer A (API) and Layer B (policy simulation), both agree
Fix direction
The UPDATE policy on tasks checks membership in USING but has no WITH CHECK, and the USING clause does not compare org_id. Add an org_id membership check to both clauses.

8 · Signature block

Report SHA-256 (canonical JSON)
0d0b8385f6fa968afaba9aa7e2ac396717d5605bfafea3d395aea76e0ccfbf97differs from the signed hash
Ed25519 signature
08bf197e72e038ed797cb60f39b593768abe8edeecac409ba7412ee935e6b60cd676dcf9609c0d355deedf92ba87fbe5a7b2d8935e6c224500d388108e8bc90b
Demo public key (Ed25519)
0a751ebe666571509fda8763a81bb54b74fcf333b3b86d61d0b2df47f3a086c5
Key fingerprint (SHA-256)
c7e4b86705e7ded0a2456f623345fb8c099eb95cdea3dfa6e760b206509e76a7

Production reports are signed with a separate key and carry a public verify link; no account is needed to check one. This sample is signed with a demo key, and you can check it right here.

Not yet verified

This checks the report's signature — it proves the document is unaltered, not that your app is safe. Verify a real signed report from our Tasklio reference fixture →

The evidence

Every cell is a probe with a verdict.

The isolation matrix from the Tasklio demo fixture: principals down the side of the real thing, operations across. Tap a cell for the verdict in the engine's own words.

Table · operationanonattacker · tenant B2nd user · tenant A
projects · SELECT
tasks · SELECT
tasks · UPDATE
documents · INSERT
bucket files · read
analytics_events · all

Tap any cell to read the engine's verdict for that probe.

  • PASS — both layers agree
  • LEAK — confirmed finding
  • Limitation — one layer only
  • Not evaluated

Spot the leak

One of these three policies is broken.

Which policy lets a tenant reach another tenant's data? Tap your guess.

How it works

Three steps, one artifact.

  1. 01

    Connect a staging project

    Point RowAttest at a staging Supabase project you are authorized to test. Production is out of scope.

  2. 02

    The eight-stage engine runs

    It reads the security surface and the access model, seeds its own test users, runs the matrix of reads and writes across users and organizations, keeps the evidence and tears everything down.

    • preflight
    • static-introspection
    • tenancy-inference
    • attacker-seeding
    • matrix-generation
    • execution
    • evidence-capture
    • teardown
  3. 03

    Signed attestation, public verify link

    The report is hashed and signed with Ed25519. Anyone you hand it to can check it without an account.

    • report-signing

API and MCP

Run it from the dashboard, your scripts, or your AI assistant.

Every account gets the same three doors. Runs started from any of them count against the same monthly allowance, and they return the same results.

01 · Dashboard

Sign in, connect, run.

Sign in with GitHub or with email. Connect a staging Supabase project, press Run test, and read the isolation matrix on the run page. Buy the signed report for a run when you need evidence you can hand to someone else.

Create a free account →

02 · REST API

Keys start runs and read results.

Create a key on the API keys page. A key can list your projects, start runs, follow them, and read findings and the signed report. It cannot connect projects, change settings, or pay for anything.

curl https://app.rowattest.com/api/v1/projects \
  -H "Authorization: Bearer rak_your_key"
  • GET /api/v1/projects
  • POST /api/v1/runs
  • GET /api/v1/runs/{id}
  • GET /api/v1/runs/{id}/findings
  • GET /api/v1/runs/{id}/report
OpenAPI description →

03 · MCP

Six tools for your assistant.

Point your assistant at the MCP endpoint with the same key. It gets six tools: list projects, start a run, check status, wait for a run, read findings, fetch the signed report. Tested with Claude Code; the API keys page shows ready-to-paste settings for Claude Code, Cursor and mcp-remote.

claude mcp add --transport http rowattest https://app.rowattest.com/mcp --header "Authorization: Bearer rak_your_key"
Get a key → Server docs →
Credentials
Keys and agents never hold your project's credentials. Projects are connected on the website only; a key starts runs and reads results.
Allowance
One allowance per account: 10 runs a month on the free tier, counted the same whether a run starts from the dashboard, the API or MCP. Failed runs do not count.
Unsigned and signed
Free runs show their results on the run page and through the API, labelled as unverified. Buying the signed report for a run adds the signed PDF, a public verify link, and the signed JSON through the API.
GitHub
RowAttest is on GitHub at github.com/rowattest, and you can sign in to the app with your GitHub account.

Status

Where this stands.

Status
Alpha, open. You can create an account and run today. Expect rough edges, and tell us about them: info@rowattest.com.
Capacity
One founder runs RowAttest. Support is by email.
Pricing
Free: one project, 10 runs a month. Signed report: $49 per run, one-time.
Scope
Testing runs only against staging environments, with written authorization from the project owner.
This page
The report above is a sample for a fictional app. Nothing on this page is a customer result.

Start with a staging project.

Create a free account, connect a staging Supabase project you are authorized to test, and run. Buy the signed report only when you need to prove the result to someone else.

Sign in with GitHub or with email and password. Questions: info@rowattest.com.