Module 14 · Les données

Supabase : le backend

Jusqu'ici, on a surtout regardé ce qui se passe dans le téléphone : des composants, de l'état, de la navigation. Mais une app de musculation qui oublierait tes entraînements dès que tu changes de téléphone ne servirait à rien. Il faut un endroit, ailleurs, qui se souvient. Cet endroit, c'est le backend, et dans Halterofit il s'appelle Supabase.

💡 Le fil rouge du module

On va répondre à trois questions, dans l'ordre. (1) Pourquoi une app a besoin d'un serveur du tout. (2) Ce qu'est Supabase et quelles briques il offre. (3) Ce que Halterofit en utilise vraiment — et, tout aussi instructif, ce qu'il n'utilise pas. À la fin, tu sauras lire la couche « données distantes » du projet sans te perdre.

1. Qu'est-ce qu'un « backend », et pourquoi en faut-il un ?

Quand tu lances Halterofit, le code qui s'exécute sur ton téléphone — l'interface, les écrans, la logique d'un entraînement — c'est ce qu'on appelle le front-end (« le devant », ce que l'utilisateur voit et touche). Le back-end (« l'arrière »), c'est tout ce qui tourne ailleurs, sur des ordinateurs que tu ne vois jamais : des serveurs, dans un centre de données, quelque part dans le cloud. L'app de ton téléphone leur parle par Internet.

Pourquoi déporter du travail loin du téléphone ? Parce qu'un téléphone, tout seul, a trois grandes faiblesses, et chacune justifie l'existence d'un serveur :

  • Il peut être perdu, cassé ou remplacé. Si tes 200 séances ne vivent que dans la mémoire du téléphone, le jour où tu le laisses tomber dans une flaque, tu perds tout. Un serveur garde une copie au chaud, dans le cloud.
  • Il est seul. Tu installes l'app sur une tablette, ou tu changes pour un nouveau téléphone : comment retrouver tes données ? Elles doivent être stockées à un endroit central que n'importe lequel de tes appareils peut interroger après t'être identifié.
  • Il n'a aucune notion de « qui es-tu ». Pour que tes données soient les tiennes — et pas mélangées à celles des autres utilisateurs — il faut un endroit qui sache vérifier ton identité (ton compte, ton mot de passe) et qui distribue les bonnes données à la bonne personne.
💡 Le modèle client ↔ serveur

C'est le schéma fondamental de presque toute application connectée. Le client (ici : ton app mobile) demande ; le serveur (ici : Supabase) répond. Le client dit « connecte-moi avec cet email et ce mot de passe », « donne-moi mes entraînements », « enregistre cette nouvelle série ». Le serveur, lui, est l'autorité : c'est lui qui décide si la demande est légitime, qui détient la vérité officielle des données, et qui les protège. Retiens cette asymétrie : le client demande, le serveur tranche. On y reviendra sans cesse, surtout pour la sécurité.

Une analogie : ton téléphone est comme un guichet de banque local, pratique et rapide ; mais l'argent réel n'est pas dans le guichet, il est dans le coffre central de la banque. Le guichet demande au coffre, le coffre vérifie ton identité et autorise (ou non) l'opération. Sans le coffre, un guichet n'est qu'une jolie boîte vide.

2. Qu'est-ce que Supabase ?

Construire un backend complet à la main — un serveur, une base de données, un système de comptes sécurisé, du stockage de fichiers — c'est énorme. Supabase est un produit qui te livre tout cet attirail déjà monté et géré, prêt à l'emploi. On le présente souvent comme « l'alternative open-source à Firebase » (le service équivalent de Google). « Open-source » signifie que son code est public : tu peux l'auto-héberger si tu veux, tu n'es pas prisonnier.

Le cœur de Supabase, c'est PostgreSQL — une base de données relationnelle très réputée, vieille de plusieurs décennies, solide et standard. « Relationnelle » veut dire que les données vivent dans des tables (comme des feuilles de calcul) qui se référencent les unes les autres. Autour de ce noyau Postgres, Supabase ajoute plusieurs briques :

  • Auth — la gestion des comptes : inscription, connexion, mots de passe, sessions, vérification d'email. C'est lui qui sait « qui tu es ».
  • Postgres — la base de données elle-même : tes tables, tes lignes, les relations entre elles.
  • Storage — un espace pour stocker des fichiers volumineux (photos, vidéos) qui n'ont pas leur place dans des tables.
  • Realtime — un canal « temps réel » qui pousse les changements vers les clients abonnés, via des websockets (une connexion permanente ouverte).
  • Edge Functions — de petites fonctions de code que toi tu écris et qui s'exécutent sur les serveurs de Supabase, pour la logique sur-mesure.

Le point génial : tout ça est accessible depuis ton app via une seule bibliothèque JavaScript, @supabase/supabase-js. Tu écris du code JavaScript familier (supabase.auth.signIn(...), supabase.from('workouts').select()) et la bibliothèque traduit ça en requêtes réseau vers les serveurs. Tu n'as pas à apprendre un protocole réseau de bas niveau ; tu appelles des méthodes, comme pour n'importe quel objet.

🧭 Bon à savoir : « managé »

On dit que Supabase est un service managé (de l'anglais managed) : tu ne t'occupes pas d'installer Postgres, de le mettre à jour, de le sauvegarder, de surveiller le serveur. Supabase fait tout ce travail d'exploitation à ta place. Pour un développeur solo comme celui de Halterofit, c'est ce qui rend faisable d'avoir un vrai backend sans une équipe d'infrastructure derrière.

3. Ce que Halterofit utilise vraiment (et ce qu'il évite)

Supabase offre beaucoup de briques, mais une app n'est pas obligée de toutes les prendre. Lire le projet, c'est aussi savoir quelles briques sont branchées. Voici la carte de Halterofit :

  • Auth — oui, en plein : comptes, sessions, jetons (JWT). C'est le sujet du flux d'authentification, plus loin.
  • Postgres — oui : sept tables synchronisées (utilisateurs, entraînements, exercices d'une séance, séries, plans, etc.) qui reflètent les données locales du téléphone.
  • RLS (Row Level Security) — oui, et c'est central : des règles de sécurité dans la base qui garantissent que chacun ne voit que ses propres lignes.
  • Edge Functions — oui, mais une seule : delete-account, pour supprimer définitivement un compte.
  • RPC de synchro — oui : deux fonctions Postgres, pull_changes et push_changes, qui implémentent le protocole de synchronisation (on les détaille dans le module sur le traçage et la synchro).
  • Realtimenon. Volontairement. Voir l'encadré ci-dessous.
  • Storage — non : l'app ne stocke pas de fichiers/photos côté serveur pour l'instant.
🏋️ Dans Halterofit : pourquoi PAS de Realtime ?

Le Realtime (websockets) sert à recevoir les changements en direct : utile pour un chat, un document collaboratif, un tableau partagé. Halterofit n'a aucun de ces besoins. C'est une app offline-first et mono-utilisateur : tes données sont d'abord stockées sur le téléphone (dans une base locale), et la synchro avec le serveur se fait par interrogation (on appelle pull_changes/push_changes à des moments choisis), pas par un flux live permanent. Comme tu es seul à modifier tes propres séances, il n'y a personne d'autre dont il faudrait recevoir les changements en temps réel. Ouvrir une websocket permanente coûterait de la batterie et de la complexité pour zéro bénéfice. Choisir de ne pas utiliser une brique est une décision d'architecture aussi importante que d'en utiliser une.

4. Le client Supabase : le point d'entrée unique

Tout dialogue de l'app avec Supabase passe par un seul objet, le client. On le crée une fois, au démarrage, et tout le reste du code l'importe. Voici le vrai fichier qui le construit, dans Halterofit :

apps/mobile/src/services/supabase/client.ts
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
import { mmkvStorage } from '@/services/storage/mmkvStorage';

// Les deux ingrédients viennent des variables d'environnement (section 5).
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL || '';
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || '';

// Si l'URL OU la clé manque, on n'essaie même pas : le client vaut null.
// C'est le "repli hors-ligne" — l'app démarre quand même, sans auth/sync.
export const supabase: SupabaseClient | null =
  supabaseUrl && supabaseAnonKey
    ? createClient(supabaseUrl, supabaseAnonKey, {
        auth: {
          // Où ranger la session ? Dans MMKV (le stockage local rapide du tél.),
          // pour rester connecté entre deux ouvertures de l'app.
          storage: {
            getItem: (key) => mmkvStorage.get(key) ?? null,
            setItem: (key, value) => mmkvStorage.set(key, value),
            removeItem: (key) => mmkvStorage.delete(key),
          },
          autoRefreshToken: true,   // renouvelle tout seul le jeton qui expire
          persistSession: true,     // se souvient de la session au redémarrage
          detectSessionInUrl: false, // pas de session via l'URL (app mobile, pas web)
        },
      })
    : null;

if (!supabase) {
  console.warn('Supabase not configured — auth/sync unavailable.');
}

createClient(url, clé, options) fabrique l'objet par lequel tout passe. Remarque que la variable exportée a le type SupabaseClient | null : elle peut légitimement être null.

Trois choses méritent qu'on s'y arrête, parce qu'elles racontent des décisions d'ingénierie réelles.

  • La session est rangée dans MMKV. Une « session » est la preuve que tu es connecté (concrètement, un jeton). En la persistant dans MMKV — le petit stockage local rapide du téléphone, vu dans le module sur la structure du projet — l'app se souvient de toi : tu n'as pas à te reconnecter à chaque ouverture. Au démarrage, Supabase relit cette session depuis MMKV.
  • autoRefreshToken: true. Le jeton d'accès expire vite (souvent une heure), pour la sécurité. Plutôt que de te déconnecter brutalement, le client va, en coulisses, échanger un « jeton de rafraîchissement » contre un jeton neuf. Tu ne vois rien ; tu restes connecté. C'est autoRefreshToken qui orchestre ça automatiquement.
  • Le repli null. Si les identifiants de connexion à Supabase manquent (par exemple sur une build de prévisualisation sans secrets configurés), le client vaut null au lieu de planter. L'app peut alors fonctionner en mode purement local. C'est pour ça que, dans tout le code d'auth, chaque opération commence par vérifier que le client existe.
🧭 Bon à savoir : le garde-fou requireSupabase()

Puisque supabase peut être null, le service d'authentification de Halterofit ne l'utilise jamais directement à l'aveugle. Il passe par un petit gardien :

function requireSupabase() {
  if (!supabase) {
    // Pas de client → on lève une erreur claire plutôt que de planter
    // mystérieusement plus loin sur "supabase is null".
    throw new AuthError('Authentication is not available', '...', 'AUTH_UNAVAILABLE');
  }
  return supabase; // ici, TypeScript sait que ce n'est plus null
}

C'est aussi une jolie illustration du module sur TypeScript : après le if (!supabase), le type passe de SupabaseClient | null à SupabaseClient tout court. Le narrowing (rétrécissement de type) en action.

5. Les clés et les variables d'environnement

Pour parler à ton projet Supabase (et pas à celui du voisin), le client a besoin de deux informations : l'URL de ton projet et une clé d'accès. Dans le code, on les lit ainsi :

const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL || '';
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || '';

process.env.XXX lit une variable d'environnement : une valeur définie en dehors du code (dans un fichier de config, dans les réglages de build), pas écrite en dur dans les sources. Pourquoi ? Pour ne pas figer des valeurs sensibles ou changeantes dans le code source, et pour pouvoir pointer vers un projet différent en développement et en production sans toucher une ligne. Le préfixe EXPO_PUBLIC_ est une convention d'Expo : il dit « cette variable a le droit d'être embarquée dans l'app livrée ».

Et c'est exactement là qu'il faut être très attentif, parce qu'il existe deux sortes de clés et qu'on ne les traite pas pareil :

CléOù elle vitCe qu'elle peut faire
Clé publiable (« anon ») Dans l'app mobile (côté client), embarquée Accès limité : tout passe ensuite par la sécurité RLS (section 6). Sans connexion valide, elle ne donne presque rien.
Clé secrète (« service role ») Uniquement sur le serveur (Edge Functions), JAMAIS dans l'app Accès total : elle contourne toute la sécurité RLS. Une clé maîtresse.
⚠️ Piège fréquent : la clé secrète dans l'app

La règle d'or, gravée dans la pierre : la clé secrète (service role) ne va JAMAIS dans l'app mobile. Jamais. Une app installée sur un téléphone peut être décompilée par n'importe qui : tout ce qu'elle embarque est, en pratique, public. Or la clé secrète contourne toute la sécurité RLS — quiconque la récupère peut lire et effacer les données de tous les utilisateurs. La clé publiable, elle, est conçue pour être publique : seule, sans session valide, elle ne donne accès à presque rien, car RLS verrouille tout derrière. Tu verras d'ailleurs en section 9 que la clé secrète n'apparaît que dans une Edge Function, côté serveur, là où personne ne peut la décompiler.

🧭 Bon à savoir : un détail de nommage

Dans le vrai code, la variable s'appelle EXPO_PUBLIC_SUPABASE_ANON_KEY et un commentaire précise qu'elle contient en fait la clé publiable moderne (sb_publishable_...). « anon » est l'ancien nom historique, conservé parce que c'est la convention de fait chez Expo et Supabase. Anon ou publiable : même rôle, celui de la clé publique côté client.

6. RLS (Row Level Security) — la sécurité au niveau de la ligne

Voici, sans doute, le concept le plus important de tout ce module. Imagine la table workouts : elle contient tous les entraînements de tous les utilisateurs, ligne après ligne, chacune marquée d'un user_id. Quand ton app demande « donne-moi les entraînements », qu'est-ce qui empêche qu'on te renvoie aussi ceux des autres ?

La réponse est RLS : des règles écrites directement dans la base de données qui filtrent, ligne par ligne, ce que chaque utilisateur a le droit de lire ou d'écrire. « Row Level » = « au niveau de la ligne » : la permission ne se décide pas table par table, mais ligne par ligne, selon qui demande. Voici une vraie politique de Halterofit :

supabase/migrations/…optimize_rls_policies.sql
-- D'abord, on ACTIVE la sécurité au niveau ligne sur la table.
-- (sans ça, RLS ne s'applique pas du tout)
ALTER TABLE public.workouts ENABLE ROW LEVEL SECURITY;

-- Puis on écrit la RÈGLE. "FOR ALL" = pour lire, insérer, modifier, supprimer.
CREATE POLICY "Users see own workouts" ON public.workouts FOR ALL
  -- La condition magique : ne laisse passer QUE les lignes
  -- dont le user_id est égal à l'identité de celui qui demande.
  USING ((select auth.uid()) = user_id);

auth.uid() renvoie l'identifiant de l'utilisateur connecté, déduit de son jeton. Le motif auth.uid() = user_id est le cœur de toute la sécurité : « cette ligne t'appartient-elle ? ».

Lis bien le mécanisme. Quand ton app fait une requête, Postgres ajoute automatiquement la condition WHERE (select auth.uid()) = user_id à chaque accès à la table. Tu ne peux donc physiquement pas récupérer une ligne qui n'est pas la tienne : la base les filtre avant même que les données ne sortent. Et ce, quoi que fasse l'app. Même un client malveillant qui forgerait des requêtes ne contournerait rien, parce que le filtre vit dans la base, pas dans l'app.

Pour les tables « filles » (les exercices d'une séance, les séries d'un exercice), la propriété est héritée : une série t'appartient si elle appartient à un exercice qui appartient à un entraînement dont le user_id est le tien. La politique vérifie alors la chaîne avec un EXISTS, mais l'idée reste identique : remonter jusqu'au propriétaire et comparer à auth.uid().

💡 Pourquoi la sécurité DOIT vivre côté serveur

On pourrait être tenté de filtrer côté app : « je n'affiche que mes données, donc c'est bon ». C'est une illusion dangereuse. Le code de l'app s'exécute sur l'appareil de l'utilisateur — un environnement que tu ne contrôles pas et qu'un attaquant contrôle totalement. Toute vérification côté client peut être contournée, désactivée, modifiée. La seule sécurité qui tient est celle que le serveur impose, parce que lui, tu le contrôles. RLS est exactement ça : la base elle-même refuse de servir les données qui ne sont pas à toi, peu importe ce que l'app demande. C'est le retour du modèle client ↔ serveur de la section 1 : le client demande, le serveur tranche. Les vérifications côté client sont là pour le confort (afficher une jolie erreur), jamais pour la sécurité.

🧭 Bon à savoir : (select auth.uid())

Tu remarqueras (select auth.uid()) plutôt que auth.uid() tout court. C'est une optimisation : sans le select, Postgres réévalue la fonction pour chaque ligne examinée ; avec, il la calcule une seule fois par requête et réutilise le résultat. Détail de performance recommandé par Supabase — la sécurité, elle, est identique.

7. Le flux d'authentification, bout à bout

On a maintenant toutes les pièces pour comprendre comment un utilisateur entre dans l'app. Le service d'auth de Halterofit centralise ces opérations dans un seul fichier (services/auth/index.ts) : signUp, signIn, signOut, resetPassword, la vérification d'email par code OTP, etc. Suivons le parcours d'une inscription, étape par étape :

  1. Inscription. L'utilisateur saisit email + mot de passe. L'app appelle supabase.auth.signUp(...). Le serveur Auth crée le compte et renvoie un objet utilisateur.
  2. Session et JWT. En échange, Supabase délivre une session, qui contient un JWT (JSON Web Token) — un petit jeton signé qui prouve « je suis cet utilisateur ». Ce jeton est joint automatiquement à chaque requête suivante ; c'est lui que la base lit pour calculer auth.uid() dans les politiques RLS. La session est rangée dans MMKV (section 4) pour survivre aux redémarrages.
  3. Le store d'auth. L'app range l'utilisateur courant dans son store global (useAuthStore.getState().setUser(user)). C'est l'état partagé qu'on a vu dans le module sur l'état global : poser user fait basculer isAuthenticated, et toute l'app réagit.
  4. La garde de navigation. Ce basculement déclenche la garde du module sur la navigation : tant qu'il n'y a pas d'utilisateur, l'app montre les écrans de connexion ; dès qu'il y en a un, elle redirige vers les écrans de l'application. La connexion et la déconnexion ne font, au fond, que basculer un seul booléen, et la navigation suit.

Un fil important relie le serveur et le local : à l'inscription, l'app crée aussi un enregistrement utilisateur dans la base locale du téléphone, et — détail crucial — avec le même identifiant que auth.uid(). C'est ce qui permet à la synchro (pull_changes/push_changes) de faire correspondre la bonne personne des deux côtés. Si les identifiants ne collaient pas, la synchro ne saurait pas à qui appartiennent les données.

🏋️ Dans Halterofit : un écouteur d'état d'auth

Le service installe aussi un écouteur, setupAuthListener(), via supabase.auth.onAuthStateChange(...). À chaque événement (connexion, déconnexion, rafraîchissement du jeton, demande de récupération de mot de passe), il met à jour le store d'auth en conséquence. C'est la colle qui garde l'état de l'app synchronisé avec la vérité de Supabase : si le jeton se renouvelle en arrière-plan, ou si la session expire, l'app le sait et réagit sans que tu aies à interroger quoi que ce soit.

8. Les Edge Functions : du code à toi, sur leurs serveurs

La plupart du temps, l'app parle à Postgres et c'est tout. Mais parfois, il faut faire quelque chose qu'on ne peut pas, et ne doit pas, faire depuis le téléphone. C'est là qu'entrent les Edge Functions : de petites fonctions serverless que tu écris (en Halterofit, en Deno, un environnement JavaScript/TypeScript moderne) et qui s'exécutent sur les serveurs de Supabase, près des utilisateurs (« edge » = en bordure de réseau). « Serverless » ne veut pas dire « sans serveur » : ça veut dire que tu n'as pas à gérer le serveur ; tu déposes ta fonction, elle se réveille à chaque appel.

Halterofit n'en a qu'une, mais elle est exemplaire : delete-account, qui supprime définitivement un compte (une exigence des stores d'applications et du RGPD — le « droit à l'effacement »). Pourquoi cette opération doit vivre côté serveur ?

supabase/functions/delete-account/index.ts
Deno.serve(async (req) => {
  // ... (vérifs de méthode, CORS, en-tête Authorization présent) ...

  const authHeader = req.headers.get('Authorization');

  // Les clés sont INJECTÉES par la plateforme dans l'env de la fonction.
  // La service_role NE vit QUE côté serveur, jamais dans l'app.
  const supabaseUrl = Deno.env.get('SUPABASE_URL');
  const anonKey = Deno.env.get('SUPABASE_ANON_KEY');
  const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');

  // 1. On identifie l'appelant À PARTIR DE SON JWT — jamais d'un id reçu
  //    dans le corps de la requête. Un appelant ne peut viser QUE son compte.
  const userClient = createClient(supabaseUrl, anonKey, {
    global: { headers: { Authorization: authHeader } },
  });
  const { data: { user } } = await userClient.auth.getUser();

  // 2. On supprime POUR DE BON avec la clé service_role (droits admin).
  //    Le ON DELETE CASCADE efface ensuite toutes ses lignes public.*.
  const adminClient = createClient(supabaseUrl, serviceRoleKey);
  await adminClient.auth.admin.deleteUser(user.id);

  return json({ success: true }, 200);
});

Deux clients, deux rôles : un anon scellé au jeton de l'utilisateur pour savoir qui appelle, puis un service_role pour agir avec les pleins pouvoirs.

Décortiquons le « pourquoi côté serveur », car c'est l'illustration parfaite de tout ce module :

  • Supprimer un utilisateur de auth.users est une opération d'administrateur. Elle exige la clé service role, celle qui contourne RLS. Or cette clé ne peut jamais être dans l'app (section 5). La fonction tourne donc là où la clé peut vivre sans risque : sur le serveur.
  • L'identité vient du JWT, jamais du corps de la requête. La fonction ne fait pas confiance à un id que l'app lui enverrait (sinon, on pourrait demander à supprimer le compte de quelqu'un d'autre). Elle déduit l'identité du jeton vérifié de l'appelant. Résultat : tu ne peux supprimer que ton compte. Encore le principe « le serveur tranche ».
  • Le ON DELETE CASCADE fait le ménage. Une fois la ligne auth.users supprimée, les clés étrangères en cascade effacent automatiquement toutes les lignes public.* de cet utilisateur. Pas de nettoyage table par table à écrire : le schéma s'en charge.

Côté app, l'appel est trivial : supabase.functions.invoke('delete-account', ...). Le client attache automatiquement le jeton de l'utilisateur ; si la fonction répond une erreur, l'app la transforme en message lisible. Toute la complexité — et toute la sécurité — est cachée derrière, sur le serveur.

✍️ Exercice de lecture

Voici la politique RLS de la table des entraînements, déjà vue :

CREATE POLICY "Users see own workouts" ON public.workouts FOR ALL
  USING ((select auth.uid()) = user_id);

Questions : (1) Un utilisateur A, connecté, lance une requête qui demande tous les entraînements de la table, sans aucun filtre. Que reçoit-il ? (2) Un développeur propose de « simplifier » en retirant cette politique et en filtrant plutôt dans l'app, côté téléphone. Pourquoi est-ce une mauvaise idée ?

Voir le corrigé

(1) Il ne reçoit que ses entraînements. Même si sa requête ne précise aucun filtre, Postgres ajoute automatiquement la condition de la politique ((select auth.uid()) = user_id) : les lignes des autres utilisateurs sont écartées avant de quitter la base. L'utilisateur ne peut pas voir ce qui n'est pas à lui, même en le demandant explicitement.

(2) Parce que le filtrage côté app n'est pas une sécurité : le code de l'app tourne sur l'appareil de l'utilisateur, qui peut le modifier ou forger ses propres requêtes réseau. Sans RLS, une requête « donne-moi tout » renverrait réellement tout — les données de tous les utilisateurs. La sécurité doit vivre côté serveur (dans la base, via RLS), parce que c'est le seul endroit que l'attaquant ne contrôle pas.

🧠 Quiz éclair

1. En une phrase : pourquoi une app mobile a-t-elle besoin d'un backend ?

Pour stocker les données dans le cloud (survivre à la perte du téléphone), les retrouver sur un autre appareil, et gérer les comptes / l'identité. Un téléphone seul est isolé, fragile et ne sait pas « qui tu es ».

2. Quelles briques de Supabase Halterofit utilise-t-il, et laquelle évite-t-il volontairement ?

Il utilise Auth, Postgres (+ RLS), les RPC de synchro (pull_changes/push_changes) et une Edge Function (delete-account). Il évite le Realtime : l'app est offline-first et mono-utilisateur, la synchro se fait par interrogation, pas en live.

3. Quelle clé peut être embarquée dans l'app, et laquelle ne doit JAMAIS y être ? Pourquoi ?

La clé publiable (anon) peut être dans l'app : seule, elle ne donne presque rien car RLS verrouille tout. La clé secrète (service role) ne doit jamais y être : elle contourne RLS (accès total), et une app peut être décompilée — la clé deviendrait publique.

4. Que garantit le motif auth.uid() = user_id dans une politique RLS ?

Qu'un utilisateur ne peut lire/écrire que les lignes dont il est propriétaire. La base ajoute ce filtre automatiquement à chaque requête, côté serveur — impossible à contourner depuis l'app.

5. Pourquoi delete-account est-il une Edge Function et pas du code dans l'app ?

Parce que supprimer un compte exige la clé service role (droits admin), qui ne peut jamais vivre dans l'app. La fonction tourne côté serveur, identifie l'appelant via son JWT (jamais via un id fourni), et ne supprime donc que le compte de l'appelant.

À retenir

Un backend stocke les données dans le cloud, gère les comptes et arbitre les accès : le client demande, le serveur tranche. Supabase = Postgres managé + Auth + Storage + Realtime + Edge Functions, via un client JS unique. Halterofit en prend Auth, Postgres, RLS, les RPC de synchro et une Edge Function — et pas le Realtime (offline-first, mono-utilisateur). La sécurité tient grâce à RLS (auth.uid() = user_id, dans la base), jamais grâce à des vérifications côté app. La clé publiable va dans l'app ; la clé secrète, jamais.

⚠️ Piège fréquent

Les deux fautes graves, à ne jamais commettre : (1) glisser la clé service role dans l'app « parce que c'est plus simple » — une app est décompilable, la clé devient publique, et elle contourne toute la sécurité. (2) se reposer sur des vérifications côté client pour la sécurité (« j'affiche seulement mes données, donc c'est protégé ») — l'app tourne sur l'appareil de l'utilisateur, qui peut tout contourner. La sécurité réelle vit côté serveur : RLS dans la base, identité dérivée du JWT.

🔄 Transférable

Tout ce module dépasse Supabase. Le modèle client ↔ serveur, l'idée que la sécurité doit vivre côté serveur, la distinction entre clés publiques et secrètes, l'identification par jeton signé (JWT), et le principe « ne jamais faire confiance au client » sont valables pour n'importe quel backend, avec ou sans Supabase. Apprends ces principes une fois ; ils te suivront sur Firebase, sur une API maison, partout.