For AI agents

Check the RLS proof before your agent connects.

RowAttest tests a Supabase project's Row Level Security (RLS) and signs what it finds: the attestation. Your agent can verify it before it connects to a Supabase-backed app or MCP server, with no key and no account, or hand the check to the MCP tool verify_attestation.

GET /.well-known/rowattest.json → signed summary → Ed25519 verify → subject binding → verdict

verify_attestation · example values
// input
{ "origin": "https://example.com",
  "project_ref": "abcdefghijklmnopqrst" }

// result
{ "valid": true,
  "failed_check": null,
  "report_id":
    "RA-01M4DWC8VPVD6078NGF5T6F1D5",
  "issued_at": "2026-10-17T13:48:53Z",
  "subject_match": true,
  "findings_present": false,
  "evidence_class": "executed-api",
  "platform": "supabase",
  "label": null }

How an agent checks an attestation

example values
Your agent claude · cursor · cli any MCP client 1 GET /.well-known/rowattest.json ✓ 200 OK · application/json The app's origin https://example.com /.well-known/rowattest.json 2 attestations[0] report_id RA-01M4DWC8VPVD6078… ✓ summary_url · summary_sha256 Signed summary app.rowattest.com /api/reports/{id}/summary 3 sha256(bytes) ✓ == summary_sha256 4 RFC 8785 → Ed25519 RowAttest keys keys.json · key_id 27499e4b ✓ key-mismatch · canonicalization · signature · report-sha256 · verify-url valid · bound · no findings 5 sha256(project_ref) == subject_sha256 ✓ leak 0 · disagreement 0 RowAttest · handshake v1
  1. 1 · Ask the originGET https://example.com/.well-known/rowattest.json200 OK · application/jsonThe file is self published. It only says where to look.
  2. 2 · Read the first attestationattestations[0].report_id RA-01M4DWC8VPVD6078NGF5T6F1D5summary_url · summary_sha256 · keys_urlNewest first. Your agent reads the first entry and may read the rest.
  3. 3 · Fetch the summary, hash the bytesGET app.rowattest.com/api/reports/RA-01M4DW…/summarysha256(bytes) == summary_sha256A different hash means the pointer is stale. Stop.
  4. 4 · Verify the signature offlinekeys.json → key_id 27499e4bRFC 8785 canonical JSON → Ed25519 signatureSame five checks as rowattest.com/verify: key-mismatch, canonicalization, signature, report-sha256, verify-url.
  5. 5 · Check the binding, read the verdictsha256(project_ref) == subject_sha256totals.leak 0 · totals.disagreement 0Bound to this database, signed by RowAttest, no findings in that run. Your agent decides what to do with it.

·

01

What an attestation is

An attestation is a RowAttest signed summary published by the owner of an app or MCP server, plus a small pointer file at a fixed path on their origin. The summary is JSON signed with Ed25519. It carries the totals of one run, the schema fingerprint of the database at that time, a SHA‑256 of the project ref it is about, an optional label and the report id. It never carries table names, policies or credentials. Anyone can verify it against the public keys at rowattest.com/keys.json. Trust comes from the signature and the binding, not from the file that points to it.

02

The handshake, step by step

Six steps. Steps 1 to 5 are what verify_attestation does for you. Step 6 is yours.

  1. GET {origin}/.well-known/rowattest.json. Missing or invalid means there is no attestation; your agent follows its own policy.
  2. Read attestations[0], then GET its summary_url. SHA‑256 the bytes as served and compare with summary_sha256. A mismatch means the pointer is stale: stop.
  3. Verify the signature offline. Canonicalize the signed object with RFC 8785, fetch keys_url (cache it), pick the key whose key_id matches, verify the Ed25519 signature. The five checks are the ones rowattest.com/verify runs: key-mismatch, canonicalization, signature, report-sha256, verify-url. Any failure means INVALID: stop.
  4. Read totals. If leak or disagreement is above zero, the attestation says findings present, not clean. Your agent decides; the file cannot hide this.
  5. Check the subject binding. Take the project ref your agent is about to use (the first label of the Supabase host), SHA‑256 it, compare with subject_sha256. A mismatch, or no subject_sha256, means the proof is not bound to this database: stop.
  6. Freshness. If your agent can read the target's catalog, recompute schema_fingerprint with the published algorithm and compare. Otherwise use issued_at and your own staleness rule.

The MCP shortcut

Do not want to write it? With any RowAttest API key, the MCP tool verify_attestation runs steps 1 to 5 and returns valid, failed_check, report_id, issued_at, totals, subject_match, findings_present, evidence_class, platform and label. Step 6 comes back as not checked unless you pass a fingerprint. Input: origin, project_ref (optional), fingerprint (optional). Server docs: /mcp.

verify_attestation
{ "origin": "https://example.com",
  "project_ref": "abcdefghijklmnopqrst" }

03

The well-known file

Serve this at /.well-known/rowattest.json on the origin your agent will use: the app's public origin, or the MCP server's origin. Content type application/json. Schema v1:

{
  "rowattest": "1",
  "subject": { "origin": "https://example.com", "kind": "app" },
  "attestations": [
    {
      "report_id": "RA-01M4DWC8VPVD6078NGF5T6F1D5",
      "summary_url": "https://app.rowattest.com/api/reports/RA-01M4DWC8VPVD6078NGF5T6F1D5/summary",
      "verify_url": "https://rowattest.com/verify/RA-01M4DWC8VPVD6078NGF5T6F1D5",
      "summary_sha256": "<64 hex of the summary bytes as served>",
      "issued_at": "2026-10-17T13:48:53Z"
    }
  ],
  "keys_url": "https://rowattest.com/keys.json"
}
  • kind is app or mcp-server.
  • attestations is newest first.
  • summary_url is the public summary route, GET /api/reports/{id}/summary, which serves the stored signed summary bytes.
  • summary_sha256 pins those bytes; a different hash means stale.
  • verify_url is the human page for the same report.
  • keys_url points at RowAttest's trusted key list.
  • The file is self published. Trust does not come from the file; it comes from the signature and the subject binding.

Publish yours

  1. Run RowAttest against your project and get the signed report (Pro includes signed proofs; a signed report for one run is bought on the run page).
  2. Take the report id from the run page and build summary_url and verify_url from it.
  3. Compute summary_sha256 from the bytes as served: curl -s SUMMARY_URL | shasum -a 256.
  4. Serve the file at /.well-known/rowattest.json as application/json.
  5. Put the newest run first when you add another.

Our signing keys

https://rowattest.com/keys.json. One key today, key_id 27499e4b. key_id is the first 8 hex of the SHA‑256 of the raw public key. Rotation adds a key and never removes one.

key_id
27499e4b
public_key_b64
uxkQB8obyQW7s9G34caDDu+LzRa5bfFqbol0Iz9qr1U=
added_at
2026-09-21
purpose
production reports

04

Labels are public

A label is recorded in the signed summary exactly as sent and is public once the summary is. Do not put names, tickets or secrets in a label. If you label a run with a migration set, send a hash of it, not the file names. 1 to 128 printable ASCII characters.

05

Error codes

Every error from the REST API and the MCP server is one JSON object: error with code, message and docs_url, plus the extras a code carries. docs_url is this page followed by # and the code. The list is closed; each code below is an anchor.

{ "error": { "code": "not_found",
             "message": "…",
             "docs_url": "https://rowattest.com/agents#not_found" } }

The key is missing, malformed or unknown. Header: WWW-Authenticate: Bearer. The handshake and GET /api/v1/verify/{report_id} need no key.

This request needs a valid RowAttest API key in the Authorization header.

No project or run with that id for this key. Another account's project is not found too; the API never says whether it exists.

No project or run with that id was found for this key.

No signed report with that id, or for that project and label (GET /api/v1/attestations, GET /api/v1/verify/{report_id}).

No signed report was found with that id, or for that project and label.

The project already has a run waiting to start.

This project already has a run waiting to start. Wait for it, then try again.

No fit check has completed.

RowAttest has not checked which tables it can test in this project; run a fit check on your dashboard, then try again.

The fit check is still running.

RowAttest has not finished checking which tables it can test in this project; try again when the fit check on your dashboard is complete.

The project needs attention.

This project needs attention on your dashboard before it can run.

RowAttest cannot test this project yet because its latest fit check found no table RowAttest can test; the dashboard lists each table and the reason.

This run has not finished yet. Try again when its status is complete.

Four cases, all with a Retry-After header. More than 120 requests with one key in a minute:

Too many requests with this key. Wait a minute, then try again.

More than 30 requests without a valid key from one address in 10 minutes:

Too many requests without a valid API key from this address. Try again after the time in the Retry-After header.

More than 10 run starts for one project in an hour:

Too many run starts for this project in the last hour. Try again after the time in the Retry-After header.

More than 10 fit checks for one project in an hour, API and dashboard together:

This project was checked 10 times in the last hour. Wait up to an hour, then check again.

The month's run allowance is used up. Extras: allowance, used, resets_at.

Free tier includes 10 runs per calendar month. You have used 10. Your allowance resets at the start of next month (UTC). A signed report for a complete run is bought on that run's page at app.rowattest.com, never through the API.

Pro includes 200 runs per calendar month. You have used 200. Your allowance resets at the start of next month (UTC).

No valid project_id:

The request needs a JSON body with a valid project_id.

Bad wait:

The wait value must be a whole number from 1 to 60.

Bad label:

label must be 1 to 128 printable ASCII characters.

Extra: poll_after_seconds.

The signed report for this run is being prepared. Try again in a few seconds.

A database read failed; nothing was changed.

RowAttest could not complete this request just now. Try again in a moment.

The run was not started because the account's plan does not include signed proofs. Signed proofs are included with RowAttest Pro. A run started without attest runs as usual and returns the unverified result.

This account's plan does not include signed proofs.

06

What a PASS means

A PASS applies only to the checks in the full report, at the time of that run. It does not certify that the application is secure.

For an agent, a clean attestation means three things: the signature verifies, the subject binding matches the database it is about to use, and leak and disagreement are both zero in that run. It says nothing about tables that were not in the run and nothing about changes made after it.