Sécurité des données
Cette page dit comment protéger les lignes de votre base contre ceux qui tiennent la clé publiable : les politiques de sécurité au niveau des lignes (RLS), l'épreuve d'une politique sous l'identité d'un utilisateur, le classement des colonnes personnelles que les agents ne lisent pas, et l'analyse quotidienne de la posture de sécurité.
Le principe
La clé publiable est dans le code de votre navigateur : n'importe qui peut la lire. Ce qui protège vos données, ce sont donc les politiques que la base applique à chaque requête, pas l'application.
- Une table n'est servie par l'API que si elle porte la sécurité au niveau des lignes ET au moins une politique. Les droits (
select,insert,update,deletepour les rôles anonyme et authentifié) suivent d'office, et tombent dès que la table perd l'une ou l'autre (LIM-11). Supabase, lui, accorde toutes les tables depublic. - Les rôles portent le nom de la base en préfixe. Un
grant … to authenticatedou uncreate policy … to authenticatedécrit tel quel est refusé (rôle inconnu,42704). Écrivez la politique sansTO: elle vaut pour tous les rôles, etauth.uid()distingue l'utilisateur connecté.auth.role()etauth.jwt()rendent, eux, les noms de Supabase. - **Écrivez
(select auth.uid()), pasauth.uid()nu.** La forme en sous-requête est évaluée une fois par requête, au lieu d'une fois par ligne. La plateforme refuse la forme nue et rend sa réécriture.
Les politiques proposées
Pour chaque table du schéma public, la plateforme engendre une proposition, sans modèle d'IA, d'après la structure de la table. Trois régimes, essayés dans cet ordre :
| Régime | Quand | Ce qui est proposé |
|---|---|---|
| Propriétaire | Une colonne uuid rattache la ligne à un utilisateur : une clé étrangère vers auth.users(id), sinon un nom de colonne conventionnel. | Chacun voit et modifie ses lignes. |
| Par le parent | Aucune colonne, mais une clé étrangère mène à une table qui appartient à quelqu'un (lignes de commande, tables de liaison). | La ligne appartient au propriétaire de son parent. |
| Fermée | Ni l'un ni l'autre. | La table est fermée aux utilisateurs et ouverte à la seule clé de service. |
La proposition porte les droits, quatre politiques (une par commande) pour le rôle authentifié, une politique pour la clé de service, et l'index qui sert le prédicat. Le rôle anonyme ne reçoit rien. Ouvrir une table en lecture à tous (USING (true)) n'est jamais proposé : c'est une décision qui vous revient.
L'épreuve avant la validation
Avant de valider une politique, la plateforme l'éprouve sous quatre identités, avec de vrais utilisateurs de la base : un visiteur anonyme, le propriétaire d'une ligne, un autre utilisateur, et la clé de service. Une ligne lisible par qui ne la possède pas est un défaut : la politique ne s'applique alors qu'après la confirmation d'une personne, et le rapport cite la ligne.
Depuis la console
Ouvrez Politiques (depuis Données). Les tables sont à gauche, avec leur état ; la table choisie montre au centre sa politique, sa forme et la proposition ; à droite, de quoi l'éprouver sous l'identité d'un utilisateur.
- Choisissez une table et relisez la proposition, ou écrivez votre propre prédicat.
- Appliquez : le rapport des quatre identités s'affiche.
- Si une ligne est vue sans être possédée, confirmez ou corrigez.
Désactiver la sécurité au niveau des lignes d'une table est un autre geste, du rang admin : il se confirme en deux temps, en retapant le nom de la table, et l'ouvrier de la plateforme l'exécute (database.rls.configure). Réactiver ne se confirme pas. Ce geste est fermé aux agents.
Par un agent, la ligne de commande ou l'API
| Geste | Outil MCP | Ligne de commande |
|---|---|---|
| Lire les propositions | database_policy_list | bastide database policy list |
| Appliquer la proposition d'une table | database_policy_write_proposed | bastide database policy write_proposed |
| Appliquer votre prédicat | database_policy_write | bastide database policy write |
| Activer ou désactiver la RLS | aucun | bastide database rls configure |
Appliquer un prédicat d'appartenance (rang maintainer, portée db:write) :
{
"projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
"table": "commandes",
"policy": "proprietaire_id = (select auth.uid())",
"verifyAs": ["anon", "authenticated", "service_role"],
"idempotencyKey": "politique-commandes-2026-09-14"
}
Si l'épreuve trouve une ligne indûment lisible, l'appel est refusé sous confirmation_requise avec un nonce ; une personne le rejoue avec confirmationNonce. La réponse porte applied, rewrite (la forme optimisée, si la vôtre a été refusée), verifications, report et indexCreated. Une politique se remplace par un nouvel appel, qui retire la précédente avant de poser la sienne.
Éprouver une politique sous l'identité d'un utilisateur
L'usurpation montre ce qu'un utilisateur précis de votre application voit vraiment.
- Ouvrir une session pour un utilisateur de
auth.users, désigné par son identifiant (database_impersonation_create). Elle dure trente minutes, sans prolongation. L'audit garde qui l'a ouverte et pour quel utilisateur. - Lire sous son identité (
database_impersonation_read) : une instruction, sous le rôle authentifié, en transaction lecture seule, cent lignes au plus.denieddit si la base a refusé la lecture. - Écrire sous son identité (
database_impersonation_write) : une écriture réelle, qui ne part qu'après la confirmation d'une personne en deux temps. Les politiques s'y appliquent comme pour l'utilisateur.
Dans la console, le panneau de droite de Politiques fait les mêmes gestes : choisissez l'utilisateur, ouvrez la session, puis lisez. Le bouton d'écriture ne l'exécute pas directement : il ouvre une fenêtre qui dit ce qui va se passer avant que vous ne validiez.
{
"projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
"impersonationId": "usu_01JQ8ZK4M7NP2RSTVWXYZ34567",
"sql": "select id, total from commandes order by id"
}
Les colonnes personnelles
Vous pouvez classer des colonnes « donnée personnelle » : adresse, téléphone, date de naissance. Une colonne classée ne se lit par aucun agent : toute instruction d'un agent dont le plan la lit, l'écrit ou la passe à une fonction du projet est refusée sous colonne_personnelle_refusee, dans l'éditeur SQL comme sous usurpation. Une personne la lit toujours depuis la console.
- La liste vit au plan de contrôle : la base n'est pas touchée, et une colonne peut être classée avant d'exister.
- Elle se remplace entière : ce qui n'y est plus est déclassé. Cinq cents colonnes au plus.
- Seule une personne la règle (rang
maintainer) : déclasser une colonne l'ouvrirait aux agents, et l'opérationdatabase.personal_columns.configureleur est donc fermée. - Un projet créé par un agent et pas encore revendiqué ne reçoit aucune donnée personnelle (
projet_non_revendique).
Dans la console : Données, section « Sécurité », carte des colonnes personnelles. Par la ligne de commande : bastide database personal_columns configure. Par l'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"
}
La posture de sécurité
Chaque jour, la plateforme analyse votre base et relève cinq genres de défauts, du plus grave au moins grave :
| Constat | Enjeu | Remède |
|---|---|---|
| Table exposée sans politique | Quiconque tient la clé publiable lit ou écrit toutes ses lignes. | Appliquer la politique proposée. |
| Colonne personnelle lisible de la clé publiable | Une colonne classée est lue par le rôle anonyme. | Retirer le droit de lecture au rôle anonyme. |
| Clé secrète vue d'un navigateur | Son secret est dans le code d'une page. | Frapper une clé neuve côté serveur, puis révoquer celle-ci. |
| Politique toujours vraie | Elle ouvre la table aux rôles de l'API sans condition. | La remplacer par un prédicat d'appartenance. |
| Politique sans index | Chaque ligne réévalue le prédicat. | Créer l'index proposé. |
Chaque constat porte sa correction, et la posture les réunit en une migration que vous appliquez (voir Branches et migrations). Ce que l'analyse n'a pas pu mesurer est dit comme tel (notMeasured), jamais comme une base saine.
- Console : Données, section « Sécurité » ; la colonne de droite de la section « Tables » en donne le résumé.
- Agent :
database_posture_read;analyzedAtnul veut dire qu'aucune analyse n'a encore eu lieu. - Correction en demande de fusion : activée, chaque correction nouvelle part sur une branche
bastide/…de votre dépôt GitHub, en demande de fusion ; rien n'est poussé sur votre branche. Désactivée par défaut, elle se règle depuis la console ou pardatabase.posture.configure, fermée aux agents.
Erreurs fréquentes
- Une table renvoie zéro ligne par l'API : elle n'a pas de politique, ou sa politique n'accorde rien à ce rôle. Lisez Politiques.
- **
42704** : vous avez nomméauthenticatedouanon; retirez leTO. - La politique est refusée pour sa forme : reprenez la réécriture rendue dans
rewrite. - **
confirmation_requise** : l'épreuve a trouvé une ligne indûment lisible ; lisez le rapport avant de confirmer. - **
usurpation_expiree** : la session a passé trente minutes ; ouvrez-en une autre. - **
colonne_personnelle_refusee** : la colonne est classée ; une personne la lit depuis la console.