Start here

Arrows to move, enter to open, escape to close.

Ship/Now

Ce qui est branché · 4 min

Base de données

Quatre tables, Row Level Security sur toutes, et comment prouver que les policies tiennent au lieu de l'espérer.

Le schéma tient dans un fichier, supabase/schema.sql, appliqué avec npm run db:push. Il est écrit pour être rejoué : chaque create est en if not exists et chaque policy est supprimée avant d'être créée.

Les tables

TableContientÉcrite par
profilesTout ce qui n'est pas de l'authentificationUn trigger, à l'inscription
plansLa grille tarifaireVous, dans le seed en bas du schéma
ordersUne ligne par paiement aboutiLe webhook Stripe uniquement
subscriptionsL'état courant d'un abonnementLe webhook Stripe uniquement

Interdire par défaut

Row Level Security est activé sur chaque table, et les seules policies sont celles du schéma. Cela inverse le mode de défaillance habituel. Oubliez une policy et la requête ne renvoie rien, ce que vous remarquez en dix secondes. Oubliez un contrôle d'autorisation dans le code et la requête renvoie tout, ce que vous remarquez quand quelqu'un vous le signale.

alter table public.orders enable row level security;

create policy "orders are self readable"
  on public.orders for select using (auth.uid() = user_id);

Notez ce qui est absent : il n'y a pas de policy d'insertion sur orders. Un utilisateur ne peut pas créer sa propre commande au prix qu'il veut depuis une console de navigateur, parce que la seule chose qui écrit des commandes est le webhook, avec la clé de service, qui contourne RLS par construction.

La récursion des policies

Dès que vous ajouterez des équipes, vous écrirez ceci :

create policy "members read the org" on organisations for select
  using (exists (select 1 from memberships where org_id = id and user_id = auth.uid()));

create policy "members read memberships" on memberships for select
  using (exists (select 1 from organisations where id = org_id));

Postgres refusera les deux avec une erreur de récursion infinie, parce que chaque policy interroge une table dont la policy interroge la première. La sortie est une fonction security definer, qui s'exécute avec les droits du propriétaire et ne rentre donc pas à nouveau dans la policy :

create or replace function public.is_member(target uuid)
returns boolean language sql security definer stable
set search_path = public as $$
  select exists (
    select 1 from memberships
    where org_id = target and user_id = auth.uid()
  );
$$;

Les deux policies appellent ensuite is_member(...). Fixez toujours le search_path sur une fonction security definer : sans lui, quelqu'un qui peut créer un schéma peut masquer un nom de table et votre fonction s'exécute contre la sienne.

Le prouver

Une policy que vous n'avez pas testée est une policy en laquelle vous croyez. Lisez avec la clé publiable, qui ne porte aucune session, et vérifiez que vous n'obtenez rien :

const res = await fetch(`${SUPABASE_URL}/rest/v1/orders?select=id`, {
  headers: { apikey: PUBLISHABLE_KEY },
});
console.assert((await res.json()).length === 0, "les commandes fuient");

Ce contrôle a sa place dans votre suite de tests, pas dans votre mémoire. Voir Tests.

Modifier le schéma

Éditez supabase/schema.sql, relancez npm run db:push. Pour une colonne qui doit atterrir sur une base qui a déjà la table, utilisez la forme additive afin que le fichier reste rejouable :

alter table public.profiles add column if not exists paypal_email text;

Il n'y a pas de dossier de migrations ni d'historique. C'est un arbitrage assumé : un fichier lisible qui décrit toujours la forme actuelle, plutôt que quarante fichiers numérotés à rejouer de tête. Si vous le dépassez, les migrations du CLI Supabase cohabitent très bien.

Une erreur ou un manque sur cette page ? Dites-le-nous.