---
title: MVP no-code en production : les accès que l'interface ne protège pas
description: Un bouton masqué ne refuse pas une requête. Les scénarios que je teste avant d'ouvrir un MVP no-code à de vrais utilisateurs.
category: Sécurité
format: article
pillar: reprise-et-maintenance
tags: mvp, lovable, supabase, rls, securite
author: Charlie Dallier-Wood
publishedAt: 2026-11-05
updatedAt: 2026-11-05
canonical: https://sycode.fr/blog/securite-mvp-no-code
---
Un MVP no-code est prêt à recevoir de vrais utilisateurs quand le serveur ou la base refuse ce que l'interface se contente de masquer. Un bouton caché n'arrête aucune requête et un scan vert ne prouve rien là-dessus : ce qui le prouve, c'est un compte donné, une donnée visée et un refus constaté. Voici les scénarios que je rejoue avant d'ouvrir, sur GoPronos et sur mes propres produits.

Le 22 mai 2026, je reprenais le prototype Lovable de [GoPronos](/case-studies/gopronos). Pour savoir si l'application pouvait recevoir des joueurs, je n'ai pas commencé par regarder le bouton « pronostiquer ». J'ai regardé ce que la base autorisait après le coup d'envoi, puis ce qu'un joueur pouvait lire des choix des autres. Le calcul des points était déjà en SQL et il est resté ; c'étaient les règles d'accès autour de lui qu'il fallait éprouver.

Petite précision avant de commencer : la page sécurité de Lovable et la documentation RLS de Supabase que je cite ici, je les ai relues le 5 novembre 2026. Si tu lis ça dans six mois, revérifie.

Ce jour-là, sur GoPronos, la question n'était pas de savoir si l'écran des pronostics se fermait à l'heure du match, mais ce que la base répondait à la même demande envoyée autrement. C'est cette réponse-là, obtenue avec les droits d'un vrai joueur sur un pronostic qui ne lui appartient pas, qui décide si le produit peut ouvrir.

> Un bouton caché exprime une intention de parcours, une policy décide.

## Que doit refuser la base après le coup d'envoi ?

Dans un jeu de pronostics, l'heure du match est une limite métier, et elle porte tout le reste. Si un pronostic peut encore bouger après le coup d'envoi, le classement est faux et les joueurs qui ont respecté les règles le voient au tableau des scores. Sur GoPronos, les fonctions SQL et les politiques d'accès ont servi à verrouiller les pronostics au coup d'envoi, à contrôler les rôles avec `has_role` en RPC et à différer la visibilité des choix des autres joueurs.

L'[étude de cas](/case-studies/gopronos) raconte la reprise du produit. Ici, je garde seulement la question qui sert à tous les MVP : **quel refus obtenir avant d'ouvrir** ?

Le premier test que j'écris ressemble à une phrase métier, pas à un outil de sécurité : « après le coup d'envoi, ce joueur ne peut plus modifier ce pronostic ». Le deuxième couvre la lecture : « avant la révélation, ce joueur ne voit pas les choix d'un autre ». Si la base refuse ces deux opérations sous le rôle réellement utilisé par l'application, le comportement ne dépend plus d'un écran particulier.

## Une colonne « organisation » isole-t-elle les comptes ?

Non, et j'ai rencontré l'écart sur mon propre produit, Deviso.pro, l'ancêtre d'[asap.cool](/case-studies/asap-cool). Le schéma avait des organisations. Ça donnait l'impression que les comptes étaient séparés, alors que certaines requêtes métier ne tenaient pas systématiquement compte de l'organisation du demandeur. Le modèle de données disait une chose ; le chemin d'accès pouvait en faire une autre.

Quand j'ai reconstruit le produit sous le nom d'asap.cool, j'ai choisi de forcer l'isolation en base et de tester les accès avec les rôles utilisés en production. Aujourd'hui, toutes les tables qui portent des données d'organisation sont en `FORCE ROW LEVEL SECURITY`. Ce n'est pas parce que la RLS rend une application magiquement sûre : c'est parce qu'une règle appliquée à chaque accès est plus difficile à oublier qu'un filtre que chaque nouvelle route doit penser à rajouter. C'est le genre d'arbitrage que je détaille dans l'article sur [choisir son architecture technique](/blog/choisir-architecture-technique-saas).

La distinction compte si tu portes un MVP. Voir `organizationId` ou `userId` dans une table est une bonne nouvelle : on sait à qui rattacher les données. Ce n'est pas encore la preuve que le compte A ne peut pas lire la facture du compte B. Pour obtenir cette preuve, il faut faire la demande sous le rôle du compte A, viser une donnée de B et constater un refus. Puis essayer l'écriture, pas seulement la lecture.

Et même une règle posée en base a demandé des tests sur plusieurs connexions. Le 19 mai 2026, jour 1 d'asap.cool, le contexte d'organisation posé par `set_config` revenait à chaîne vide en fin de transaction : sur une connexion réutilisée par le pool, l'inscription suivante partait en 500. Des inscriptions échouaient, sans rien dans l'interface pour l'expliquer. C'était une erreur de disponibilité, pas une fuite de données, mais elle m'a appris la même discipline : je ne m'arrête pas à la présence d'une policy dans une migration, je regarde le comportement sous les conditions réelles d'exécution.

## Dans Supabase, une policy ne retire pas un privilège déjà donné

Si ton prototype utilise Supabase, la [documentation sur la RLS](https://supabase.com/docs/guides/database/postgres/row-level-security) décrit deux contrôles distincts. Les privilèges du rôle déterminent s'il peut tenter une lecture ou une écriture. Les politiques déterminent quelles lignes cette opération peut toucher. Le truc, c'est qu'ajouter une policy ne retire pas automatiquement un privilège déjà accordé, et l'inverse est vrai aussi : un rôle sans droit de contournement ne voit rien de ce qu'aucune policy ne lui ouvre.

Ce deuxième cas, je l'ai eu sur asap.cool, le 27 mai 2026. Mon webhook Stripe tournait sous un rôle `NOBYPASSRLS`, sans contexte d'organisation et sans policy pour ce mode-là. Il lisait donc zéro ligne, répondait 200 à Stripe, et n'enregistrait jamais l'abonnement.

Du coup, un client qui aurait payé n'aurait pas eu son accès, et côté Stripe tout avait l'air en règle puisque la réponse était 200. J'ai corrigé avec une policy « système » minuscule plutôt qu'un bypass, et j'ai réutilisé ce motif trois fois depuis. C'est aussi pour ça que je traite la réception d'un webhook comme une étape à vérifier en base, pas au code de réponse HTTP, comme je le raconte dans l'article sur [intégrer Stripe en production](/blog/integrer-stripe-en-production).

Côté rôles, `anon` représente les requêtes non connectées et `authenticated` celles d'un utilisateur connecté. `service_role` contourne la RLS et doit rester côté serveur : cette clé n'a rien à faire dans le code livré au navigateur. Une clé publique côté navigateur, en revanche, n'est pas à elle seule le diagnostic d'une fuite — ce sont les privilèges, les politiques et les données renvoyées qui décident ce qu'elle peut atteindre.

Sur GoPronos, je devais distinguer la lecture des matchs, qui sert à tout le monde, de l'écriture d'un pronostic, qui dépend du joueur et de l'heure. Sur asap.cool, il fallait isoler les organisations. Les deux produits utilisent une base, mais leurs refus attendus ne sont pas les mêmes. C'est pour ça que je pars des actions du produit avant d'ouvrir l'éditeur SQL : les profils, les commandes, les paiements, les fichiers, les actions réservées à l'admin.

## Le scan Lovable suffit-il pour ouvrir ?

Non, et Lovable ne le prétend pas. Il a deux contrôles intégrés. Son [Quick scan](https://docs.lovable.dev/features/security) se lance à la publication et repère notamment des règles de base trop ouvertes. Le Deep scan examine davantage la logique, les permissions et les endpoints ; il ne tourne pas automatiquement dans le cas général. Lovable dit lui-même que ces contrôles ne garantissent pas une sécurité complète.

Alors je les utilise comme des signaux, pas comme la réponse finale à « puis-je recevoir mes premiers clients ? ». Sur GoPronos, la réponse dépendait de l'heure du match et de l'identité du joueur. Un scanner général peut attirer l'attention sur une table trop ouverte ; la règle « ce joueur ne voit pas encore ce pronostic » doit être formulée par quelqu'un qui connaît le jeu, puis vérifiée comme telle.

Ça vaut aussi pour un produit repris d'un autre prestataire. Sur [SimplyJury](/case-studies/simplyjury), j'ai commencé par lire l'existant et ordonner les correctifs avant la mise en ligne, tout en conservant la stack et les parties utiles, puis j'ai ramené progressivement les accès PostgREST sur Drizzle. Une application peut contenir des protections déclarées et demander malgré tout une recette précise sur les comptes et les parcours. La présence d'un fichier de migration n'est pas un résultat de test.

La question utile à poser à celui qui audite ton produit est donc : « quelle opération as-tu essayé de faire refuser, avec quel compte, et qu'a répondu l'application ? »

Elle produit une preuve que tu peux relire après une modification. « Le scan est vert » ne t'apprend rien sur le compte qui lit la facture d'un autre.

## Les scénarios de refus que je rejoue avant d'ouvrir

La documentation Supabase demande des tests d'autorisation et de refus pour `select`, `insert`, `update` et `delete` sous les rôles réels. Sur mes reprises, je note d'abord les demandes qui doivent revenir en refus. Pour GoPronos, c'était l'écriture après le coup d'envoi, qui fausse le classement, et la lecture prématurée des choix des autres, qui change la nature du jeu. Pour un SaaS comme asap.cool, c'est le passage d'une organisation à une autre, où la donnée visible est un devis client. En gros, le produit change, mais le format du test reste le même : un rôle, une donnée, une action, un résultat attendu.

| Situation à vérifier | Résultat attendu |
|---|---|
| Visiteur non connecté demandant une donnée privée | Refus ou absence de donnée |
| Compte A lisant ou modifiant une donnée du compte B | Refus |
| Utilisateur ordinaire lançant une action d'administration | Refus |
| Joueur modifiant son pronostic après la limite | Refus |
| Utilisateur autorisé accomplissant la même action au bon moment | Succès |

La dernière ligne est aussi importante que les refus. Une règle trop serrée qui empêche tes clients de travailler n'est pas une réussite de sécurité : le signup cassé d'asap.cool en mai n'exposait aucune donnée, il empêchait simplement des inscriptions d'aboutir. Un test d'accès doit donc aussi exercer un parcours normal après plusieurs opérations, pas uniquement une requête isolée qui passe une fois.

Je fais ces vérifications sur le produit dont j'ai la charge, avec des comptes de test et des données prévues pour ça. Le résultat sert à décider quoi corriger en premier. Si une règle critique manque, elle passe avant un nouvel écran ou une nouvelle intégration. Si le serveur refuse déjà correctement, je garde cette partie et j'avance. C'est la même logique que dans le guide sur la [reprise d'un MVP Lovable](/blog/reprendre-un-mvp-no-code) : lire ce qui tient avant d'engager une reconstruction.

Moi, je ne valide pas une mise en production parce que les boutons ont les bonnes couleurs ou que le scan n'affiche plus d'alerte. Je veux voir les refus qui protègent les utilisateurs et les succès qui leur permettent de travailler. Si ton MVP arrive à ce point de bascule, c'est ce que je vérifie dans un [audit de reprise](/services/reprise-et-maintenance), avant de décider avec toi ce qu'on garde et ce qu'on corrige.
