Limites connues — un projet Supabase qui passe sur Bastide
Page publique, exigée par
EX-DATA-090(TR-083). Un client remplace l'URL et la clé de son projet Supabase par celles de Bastide ; ce qui ne se comporte pas pareil est ici, chiffré, avec ce qu'il faut faire à la place.Servie au public à https://bastide.app/limites-supabase par le site vitrine, qui en garde une copie mot pour mot :
pnpm vitrinerefuse une copie qui diffère de ce fichier.Chaque limite est reproduite par un test. L'épreuve
services/api/src/donnees/limites-connues.epreuve.tsjoue chacune contre la pile réelle — l'edge, l'hôte des services, PostgREST, le mutualiseur — et lit ses chiffres dans CETTE page : une limite qui change sans que la page change fait rougir l'épreuve.pnpm limitesrefuse, lui, une limite de la page qu'aucun test ne nomme, et un test qui nomme une limite absente de la page.
Les limites
| Réf | Limite | Chiffre | Ce qu'il faut faire à la place |
|---|---|---|---|
| LIM-01 | Une réponse de l'API REST porte au plus mille lignes. Au-delà, elle est tronquée sans erreur, et l'en-tête Content-Range dit la tranche rendue. | 1000 | Paginer : l'en-tête Range, ou limit et offset. Prefer: count=exact rend le total. |
| LIM-02 | Une ressource ne s'embarque dans une autre (select=*,autre(*)) que le long d'une clé étrangère déclarée. Sans elle, la requête est refusée, code PGRST200. | 400 | Déclarer la clé étrangère, ou exposer une vue qui fait la jointure. |
| LIM-03 | Mode transaction du mutualiseur — celui des connexions directes, par défaut : une notification (NOTIFY) n'atteint pas un client qui l'écoute (LISTEN). | 0 | Écouter depuis une connexion en mode session. |
| LIM-04 | Mode transaction : un verrou consultatif de session (pg_advisory_lock) n'exclut pas une autre connexion cliente du même projet — elle l'obtient aussi. | — | pg_advisory_xact_lock, dans une transaction ; ou le mode session. |
| LIM-05 | Mode transaction : l'état de session n'est pas isolé. Un SET, une table temporaire, un curseur WITH HOLD posés par une connexion cliente se retrouvent dans la suivante, même après sa fermeture. | — | SET LOCAL ; une table temporaire ON COMMIT DROP ; un curseur sans WITH HOLD ; ou le mode session. |
| LIM-06 | Trois gestes de l'authentification tierce ne sont pas servis : la connexion d'entreprise SAML (signInWithSSO, /auth/v1/sso), la liaison d'une identité à un compte déjà connecté (linkIdentity, /auth/v1/user/identities/authorize) et la description des réglages (/auth/v1/settings) reçoivent un 404, code chemin_auth_inconnu. Les douze fournisseurs de signInWithOAuth — Apple, Microsoft Entra ID (azure), Bitbucket, Discord, Facebook, GitHub, GitLab, Google, Keycloak, LinkedIn (linkedin_oidc), Slack (slack_oidc), Twitch —, en flux implicite comme en PKCE, et la double authentification de supabase.auth.mfa, eux, sont servis. | 404 | Une connexion d'entreprise passe par Keycloak, en OpenID Connect. Une identité se lie en se connectant par le fournisseur avec une adresse VÉRIFIÉE : elle rejoint alors le compte de cette adresse. |
| LIM-07 | Le point compatible S3 (/storage/v1/s3) n'est pas servi : une requête y reçoit un 404, code chemin_inconnu. Le stockage de storage-js, lui, est servi à l'adresse du projet : seaux, objets, URL publiques et signées, téléversement repris (tus), et les transformations d'images (/storage/v1/render/image, EX-DATA-340) depuis TR-096. | 404 | Les opérations de storage-js sous /storage/v1/bucket et /storage/v1/object. — Le point S3 viendra avec EX-DATA-370 (lot L2). |
| LIM-08 | Le temps réel sert la diffusion et la présence, pas l'écoute des changements de la base : un abonnement postgres_changes est refusé à la jonction du canal (statut error), et le refus nomme la diffusion depuis la base. Aucune réplication logique n'est exposée. Le temps réel s'active par projet (backend.realtime.configure) ; inactif, /realtime/v1 rend 403, code temps_reel_inactif. | 0 | La diffusion depuis la base : select realtime.diffuser_les_changements('public.ma_table') pose le déclencheur, et le canal public:ma_table reçoit chaque insertion, modification et suppression en diffusion. Un canal privé se garde par les politiques de realtime.messages. |
| LIM-09 | Les clés historiques anon et service_role — des jetons JWT — ne sont acceptées que pour un projet IMPORTÉ de Supabase, et pour ses deux clés d'origine seulement : douze mois à compter de l'import, chaque réponse portant Deprecation: true et Sunset avec la date d'échéance. Toute autre — d'un projet qui n'a pas été importé, frappée par le client avec le secret de son ancien projet, ou échue — reçoit 401, code cle_refusee. | 401 | La clé publiable (sb_publishable_…) et une clé secrète (sb_secret_…), que la console et backend.key.create délivrent. |
| LIM-10 | updateUser ne change que le mot de passe. Une adresse ({ email }) ou des métadonnées ({ data }) postées sur PUT /auth/v1/user sont refusées : 400, codes changement_d_adresse_ailleurs et metadonnees_non_rangees. Supabase change l'adresse sur la foi de la seule session ; nous exigeons le mot de passe en plus des deux confirmations. | 400 | L'adresse : POST /auth/v1/user/email, avec l'adresse et le mot de passe, puis les deux confirmations. Les métadonnées : une table du projet, où les politiques du projet les gardent. |
Ce qui est tenu
- Les chemins de Supabase :
https://<projet>.<domaine>/rest/v1/…,/auth/v1/…et/realtime/v1/…, sans réécriture. - Le temps réel de
supabase-js(TR-098) : la diffusion — client à client, par REST et depuis la base — et la présence, sur la WebSocket/realtime/v1/websocket, protocoles 1.0.0 et 2.0.0. Les canaux privés sont jugés par les politiques que le projet écrit surrealtime.messages, avecrealtime.topic()etauth.uid(). Les quotas du palier : 100 messages par seconde et 200 connexions simultanées en Découverte, 500 et 2 000 en Standard, 2 500 et 10 000 en Pro ; 100 canaux par connexion, 3 000 Ko de charge utile, 5 appels de présence par client par 30 secondes. Un dépassement de connexions rend 429 avant la poignée de main. - L'authentification à l'adresse du projet : inscription, confirmation d'adresse, connexion par mot de passe, lien magique et code à 6 chiffres, rotation du jeton de rafraîchissement, déconnexion globale. Le jeton d'accès est un JWT du projet, signé avec son secret, que son API REST vérifie sans consulter la plateforme ; une déconnexion globale le rend inutilisable à la requête SUIVANTE.
- L'appelant reconnu par son JETON :
GET /auth/v1/userrend son compte,PUT /auth/v1/userchange son mot de passe, etPOST /auth/v1/logoutferme ses sessions sans qu'il présente d'identifiants. Les trois portées de?scope=sont servies —global,local,others—, et une portée absente ou illisible ferme TOUT : se tromper vers la déconnexion coûte une reconnexion, se tromper vers le maintien laisse une session ouverte qu'on croyait fermée. - Les noms que
supabase-jsemploie, quand ils diffèrent des nôtres :POST /auth/v1/otpsert le lien magique, etPOST /auth/v1/token?grant_type=refresh_tokensert la rotation. Nos chemins canoniques —/magiclinket/token/refresh— restent servis : les deux noms mènent au même gestionnaire. - Un corps vide sur un chemin de données est lu comme un corps absent, et non refusé : c'est ce que
supabase-jsenvoie sursignOut(). Le refus vaut toujours pour les autres routes de la plateforme. - La connexion par un fournisseur tiers (TR-091,
EX-DATA-140) :signInWithOAuthvers douze fournisseurs, activés et réglés depuis la console ou parbackend.auth_provider.configure, effectifs dès l'appel. Le retour esthttps://<projet>.<domaine>/auth/v1/callback, à déclarer chez le fournisseur. La redirection n'est suivie que sous l'adresse du site ou une redirection admise (SITE_URL,URI_ALLOW_LIST), sinon la personne revient à l'adresse du site. Une adresse que le fournisseur ne dit pas vérifiée — jamais pour Entra ID ni Twitch — ne rejoint pas un compte existant. - La double authentification (TR-091,
EX-DATA-150) :supabase.auth.mfa— TOTP et clé WebAuthn —, les revendicationsaaletamrdans le jeton d'accès, la rotation qui garde le niveau, et des politiques RLS qui écrivent(select auth.jwt() ->> 'aal') = 'aal2'. - Les limites de débit de l'authentification, réglables par projet : 30 inscriptions par heure et par adresse IP, 4 courriels par heure et par adresse, 5 essais par jeton, 30 renouvellements par heure et par session. Un dépassement rend 429, code
debit_depasse, avec l'en-têteRetry-After. - Les en-têtes de
supabase-js: la clé dansapikey, un jeton de session dansAuthorization. - Les formats de clé
sb_publishable_etsb_secret_, plusieurs clés secrètes révocables une à une ; une clé secrète présentée par un navigateur est refusée (401,cle_secrete_depuis_un_navigateur), et l'analyse de posture du projet la nomme jusqu'à sa révocation. - Les instructions préparées, en mode transaction comme en mode session : mesuré, trente requêtes préparées de suite sur une même connexion cliente passent dans les deux modes.
Ce que le vrai client supabase-js obtient
Mesuré avec @supabase/supabase-js 2.117.2 — le paquet publié, tel qu'un client l'installe — contre un projet Bastide approvisionné, par services/api/src/donnees/supabase-js.epreuve.ts. Le périmètre est le PARCOURS ORDINAIRE d'une application : s'inscrire, confirmer, se connecter, savoir qui est là, renouveler, changer son mot de passe, se connecter sans mot de passe, demander une réinitialisation, se déconnecter. Les fournisseurs tiers et la double authentification sont éprouvés à part (auth/connexion-tierce.epreuve.ts, auth/mfa-supabase-js.epreuve.ts) ; le web3 n'est pas servi.
100 % des gestes du parcours ordinaire sont tenus : dix sur dix, mesuré le 01/10/2026. Ils ne l'étaient pas le matin — cinq sur dix —, et aucun des cinq écarts n'était une absence de service : les cinq fonctions existaient, à un autre chemin ou sous une autre forme de corps que celle que le client emploie. TR-083 les a rapprochées.
Ce tableau n'a pas de numéro LIM-NN et c'est volontaire : il est gardé des DEUX SENS par supabase-js.epreuve.ts, qui compare chaque verdict au verdict mesuré. Un geste réparé sans mise à jour de cette page fait rougir son cas, exactement comme un geste qui cesse de marcher. Les LIM-NN existent pour les limites qu'aucune épreuve de client ne peut constater seule.
Geste de supabase-js | Requête réelle | Verdict |
|---|---|---|
signUp | POST /auth/v1/signup | tenu |
verifyOtp | POST /auth/v1/verify | tenu |
signInWithPassword | POST /auth/v1/token?grant_type=password | tenu |
getSession | — (local au client) | tenu |
getUser | GET /auth/v1/user | tenu |
refreshSession | POST /auth/v1/token?grant_type=refresh_token | tenu |
updateUser | PUT /auth/v1/user | tenu — le mot de passe seul ; voir LIM-10 |
signInWithOtp | POST /auth/v1/otp | tenu |
resetPasswordForEmail | POST /auth/v1/recover | tenu |
signOut | POST /auth/v1/logout?scope=global | tenu — les trois portées, global, local et others |
CE QUE CE 100 % NE DIT PAS. Il porte sur les dix gestes ci-dessus, et sur eux seuls. La double authentification et la connexion tierce sont servies et éprouvées hors de ce tableau ; ne sont ni mesurés ni servis : les codes de secours, le web3, le SSO SAML et la liaison d'identité (LIM-06), et le changement d'adresse par le client (LIM-10). Un geste non mesuré n'est pas un geste tenu — et la page le dit ici plutôt que de laisser le chiffre parler pour lui.
**L'UTILISATEUR RENDU NE PORTE PAS last_sign_in_at.** Nous ne gardons pas la date de dernière connexion. Le champ est ABSENT, et non nul : un null se lirait « jamais connecté » de quelqu'un qui vient de se connecter. Une application qui l'affiche verra undefined.
L'écart qui n'est pas encore mesuré
EX-DATA-090 demande que la SUITE D'INTÉGRATION OFFICIELLE de supabase-js réussisse à 95 % au moins. Elle n'est pas jouée, et ce qui l'en empêche est désormais mesuré plutôt que supposé. La suite amont — packages/core/auth-js/test du dépôt supabase/supabase-js, 258 cas — exige trois choses que nous ne pouvons pas lui donner aujourd'hui :
- Elle ne lit aucune variable d'environnement. Son serveur et ses clés sont des constantes de
test/lib/clients.ts:http://127.0.0.1:54321/auth/v1, celui du CLI Supabase. Il n'y a donc rien à « remplacer » au sens de la clause sans modifier la suite elle-même. - Elle se forge ses clés en RS256 depuis un fichier de signature que le dépôt amont ne publie pas, et les présente en
apikeyavec leurkeyid; le serveur du CLI les vérifie par un point JWKS. Ce n'est PAS le format historique de LIM-09, qui est un JWT HS256 du secret du projet : accepter celui-là n'accepterait pas celles-ci. - Elle exige le CLI Supabase et Docker, un serveur de courriel d'essai, et un
config.toml.
La mesure à portée est donc celle du tableau ci-dessus : le paquet publié, contre notre surface.
CE QUE LA SUITE OFFICIELLE DEMANDERAIT VRAIMENT : la vérification de clés asymétriques avec un point JWKS, plus de lui fournir sa propre clé de signature. L'arbitrage A-6 du cahier programme cette bascule au lot L2 — « émettre en HMAC par projet en L1 pour la compatibilité, basculer en asymétrique avec JWKS en L2 ». Le taux de la suite officielle attend donc ce lot, et non la levée de LIM-09. Le taux global attend en outre le point S3 du stockage (LIM-07), et l'écoute des changements de la base que le temps réel ne sert pas (LIM-08).
CE QUI N'EST PAS DIT ICI EST AUSSI UN FAIT. Le tableau ci-dessus ne couvre ni la double authentification ni la connexion tierce, éprouvées à part ; les codes de secours, le web3 et le changement d'adresse par le client ne sont pas mesurés, et un geste non mesuré n'est pas un geste tenu.