Aller au contenu
Bastide Connexion Créer un compte
Press / to search

Data security

FrançaisMarkdownv0.3.0

This page explains how to protect the rows of your database from anyone holding the publishable key: row-level security (RLS) policies, testing a policy as a given user, classifying personal columns that agents do not read, and the daily security posture analysis.

The principle

The publishable key is in your browser code: anyone can read it. What protects your data is therefore the policies the database applies to every request, not the application.

  • A table is served by the API only if it has row-level security AND at least one policy. The grants (select, insert, update, delete for the anonymous and authenticated roles) follow automatically, and are withdrawn as soon as the table loses either (LIM-11). Supabase, by contrast, grants every table in public.
  • Roles carry the database name as a prefix. A grant … to authenticated or a create policy … to authenticated written as is gets refused (unknown role, 42704). Write the policy without TO: it applies to every role, and auth.uid() tells the signed-in user apart. auth.role() and auth.jwt(), for their part, return Supabase's names.
  • **Write (select auth.uid()), not a bare auth.uid().** The sub-query form is evaluated once per query instead of once per row. The platform refuses the bare form and returns its rewrite.

Proposed policies

For each table in the public schema, the platform generates a proposal, with no AI model, from the table's structure. Three regimes, tried in this order:

RegimeWhenWhat is proposed
OwnerA uuid column ties the row to a user: a foreign key to auth.users(id), otherwise a conventional column name.Each user sees and changes their own rows.
Through the parentNo such column, but a foreign key leads to a table that belongs to someone (order lines, join tables).The row belongs to its parent's owner.
ClosedNeither.The table is closed to users and open only to the service key.

The proposal carries the grants, four policies (one per command) for the authenticated role, one policy for the service key, and the index that serves the predicate. The anonymous role gets nothing. Opening a table for reading to everyone (USING (true)) is never proposed: that decision is yours.

The test before committing

Before committing a policy, the platform tests it under four identities, with real users of the database: an anonymous visitor, a row's owner, another user, and the service key. A row readable by someone who does not own it is a defect: the policy is then applied only after a person confirms it, and the report cites the row.

From the console

Open Politiques (Policies), from Données (Data). Tables are on the left, with their state; the chosen table shows its policy, its form and the proposal in the centre; on the right, the tools to test it as a given user.

  1. Pick a table and read the proposal, or write your own predicate.
  2. Apply: the four-identity report is shown.
  3. If a row is visible to a non-owner, confirm or correct.

Disabling row-level security on a table is a separate action, at the admin rank: it is confirmed in two steps, by retyping the table name, and the platform's worker carries it out (database.rls.configure). Re-enabling needs no confirmation. This action is closed to agents.

Through an agent, the command line or the API

ActionMCP toolCommand line
Read the proposalsdatabase_policy_listbastide database policy list
Apply a table's proposaldatabase_policy_write_proposedbastide database policy write_proposed
Apply your own predicatedatabase_policy_writebastide database policy write
Enable or disable RLSnonebastide database rls configure

Apply an ownership predicate (rank maintainer, scope db:write):

{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "table": "commandes",
  "policy": "proprietaire_id = (select auth.uid())",
  "verifyAs": ["anon", "authenticated", "service_role"],
  "idempotencyKey": "politique-commandes-2026-09-14"
}

If the test finds a row that is wrongly readable, the call is refused with confirmation_requise and a nonce; a person replays it with confirmationNonce. The response carries applied, rewrite (the optimised form, if yours was refused), verifications, report and indexCreated. A policy is replaced by a new call, which removes the previous one before setting its own.

Testing a policy as a user

Impersonation shows what a specific user of your application actually sees.

  1. Open a session for a user of auth.users, given by their identifier (database_impersonation_create). It lasts thirty minutes, with no extension. The audit log keeps who opened it and for which user.
  2. Read as that user (database_impersonation_read): one statement, under the authenticated role, in a read-only transaction, at most one hundred rows. denied says whether the database refused the read.
  3. Write as that user (database_impersonation_write): a real write, which only goes out after a person confirms it in two steps. Policies apply as they would for the user.

In the console, the right-hand panel of Politiques does the same: pick the user, open the session, then read. The write button does not run the write directly: it opens a dialog that says what will happen before you approve.

{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "impersonationId": "usu_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "sql": "select id, total from commandes order by id"
}

Personal columns

You can classify columns as "personal data": address, phone number, date of birth. A classified column is read by no agent: any agent statement whose plan reads it, writes it or passes it to a project function is refused with colonne_personnelle_refusee, in the SQL editor as under impersonation. A person can still read it from the console.

  • The list lives in the control plane: the database is not touched, and a column can be classified before it exists.
  • It is replaced as a whole: whatever is no longer in it is declassified. Five hundred columns at most.
  • Only a person sets it (rank maintainer): declassifying a column would open it to agents, so the database.personal_columns.configure operation is closed to them.
  • A project created by an agent and not yet claimed receives no personal data (projet_non_revendique).

In the console: Données, section « Sécurité » (security), personal columns card. On the command line: bastide database personal_columns configure. Over the API: PUT /v1/projects/{projectId}/database/personal-columns.

{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "columns": [
    { "schema": "public", "table": "clients", "column": "courriel" },
    { "schema": "public", "table": "clients", "column": "telephone" }
  ],
  "idempotencyKey": "classement-clients-2026-10-13"
}

Security posture

Every day, the platform analyses your database and reports five kinds of defect, from most to least severe:

FindingStakeRemedy
Table exposed without a policyAnyone holding the publishable key reads or writes all its rows.Apply the proposed policy.
Personal column readable with the publishable keyA classified column is read by the anonymous role.Revoke the anonymous role's read grant.
Secret key seen from a browserIts secret is in a page's code.Issue a new key server side, then revoke this one.
Always-true policyIt opens the table to the API roles unconditionally.Replace it with an ownership predicate.
Policy without an indexEvery row re-evaluates the predicate.Create the proposed index.

Each finding carries its correction, and the posture gathers them into one migration that you apply (see Branches and migrations). What the analysis could not measure is reported as such (notMeasured), never as a healthy database.

  • Console: Données, section « Sécurité »; the right-hand column of the « Tables » section shows a summary.
  • Agent: database_posture_read; a null analyzedAt means no analysis has run yet.
  • Correction as a pull request: when enabled, each new correction goes out on a bastide/… branch of your GitHub repository, as a pull request; nothing is pushed to your branch. Off by default, it is set from the console or with database.posture.configure, which is closed to agents.

Common errors

  • A table returns zero rows through the API: it has no policy, or its policy grants nothing to that role. Check Politiques.
  • **42704**: you named authenticated or anon; remove the TO.
  • The policy is refused for its form: use the rewrite returned in rewrite.
  • **confirmation_requise**: the test found a wrongly readable row; read the report before confirming.
  • **usurpation_expiree**: the session passed thirty minutes; open a new one.
  • **colonne_personnelle_refusee**: the column is classified; a person reads it from the console.

See also

Report a mistake on this page