@clicbase/mcp 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,6 +80,33 @@ projet** (`/dashboard?studio=<id>&section=api`) : « API REST » donne l'URL,
80
80
  « Cle service » la cle. Ce n'est PAS une cle `cbk_` : celles-la servent au
81
81
  serveur tout-en-un et n'apparaissent que pour qui possede un VPS ou un Docker.
82
82
 
83
+ ## Ce que l'assistant sait avant son premier appel
84
+
85
+ Un serveur MCP qui n'expose que des outils laisse le modele deviner les
86
+ conventions de la plateforme. Depuis la 0.1.2, celui-ci remplit le champ
87
+ `instructions` du protocole : le client le pose dans le contexte du modele A LA
88
+ CONNEXION, avant tout appel. On ne peut donc pas oublier de le lire.
89
+
90
+ Six pieges y sont enumeres, et ils ont en commun de repondre **200 OK** :
91
+
92
+ 1. `with check` n'est pas `using`. Sur INSERT et UPDATE, PostgreSQL n'evalue
93
+ QUE `with check`.
94
+ 2. Chaque nouvelle table accorde le CRUD a `authenticated` : un `grant select`
95
+ ne restreint rien, il faut `revoke`.
96
+ 3. Toute DDL se termine par `notify pgrst, 'reload schema';`.
97
+ 4. `service_role` a besoin de `BYPASSRLS`, sinon 200 OK et un tableau vide.
98
+ 5. Une fonction Edge rend `{ status, body }`, jamais `new Response(...)`.
99
+ 6. L'auth du PROJET (schema `auth` de la base, `auth.uid()`) ne se confond pas
100
+ avec celle de la PLATEFORME.
101
+
102
+ L'outil `clicbase_conventions` en rend le detail a la demande : formes exactes
103
+ des policies, ordre `revoke` puis `grant`, operateurs PostgREST. Il est classe
104
+ en LECTURE, donc il survit au mode restreint · c'est ce mode qui en a le plus
105
+ besoin, puisqu'il ne laisse que trois autres outils.
106
+
107
+ Le texte vit dans `savoir.mjs`, sans aucun import, pour rester chargeable par
108
+ les tests du depot.
109
+
83
110
  ## Quand l'utiliser
84
111
 
85
112
  `run_sql` exécute du SQL arbitraire avec les droits du propriétaire de la base,
@@ -12,6 +12,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import { z } from "zod";
14
14
  import { filtre, lectureSeule } from "./portee.mjs";
15
+ import { CONVENTIONS, INSTRUCTIONS } from "./savoir.mjs";
15
16
 
16
17
  const BASE = process.env.CLICBASE_DB_URL ?? process.env.ROROUIRA_DB_URL;
17
18
  const KEY = process.env.CLICBASE_SERVICE_KEY ?? process.env.ROROUIRA_SERVICE_KEY;
@@ -51,7 +52,15 @@ async function api(path, method = "GET", body) {
51
52
  const out = (d) => ({ content: [{ type: "text", text: JSON.stringify(d, null, 2) }] });
52
53
  const fail = (e) => ({ isError: true, content: [{ type: "text", text: String(e?.message ?? e) }] });
53
54
 
54
- const server = new McpServer({ name: "clicbase-db", version: "1.0.0" });
55
+ // ⚠️ `instructions` EST LU A LA CONNEXION, AVANT LE PREMIER APPEL. C'est le
56
+ // seul endroit qu'un assistant ne peut pas oublier de consulter : le client
57
+ // MCP le pose dans le contexte du modele. Sans lui, un assistant recevait
58
+ // TROIS outils et trente mots de description, puis devinait les conventions
59
+ // de la plateforme. Voir savoir.mjs.
60
+ const server = new McpServer(
61
+ { name: "clicbase-db", version: "1.0.0" },
62
+ { instructions: INSTRUCTIONS },
63
+ );
55
64
 
56
65
  // ⚠️ EN LECTURE SEULE, LES OUTILS QUI ECRIVENT NE SONT PAS ENREGISTRES. Voir
57
66
  // portee.mjs : un outil absent n'est jamais demande, un outil present qui
@@ -59,6 +68,13 @@ const server = new McpServer({ name: "clicbase-db", version: "1.0.0" });
59
68
  const SEULEMENT_LECTURE = lectureSeule();
60
69
  const outils = filtre(server, SEULEMENT_LECTURE);
61
70
 
71
+ outils.tool(
72
+ "clicbase_conventions",
73
+ "Les conventions Clicbase : formes exactes des policies RLS, droits par defaut, rechargement du schema, appels REST, fonctions Edge. A lire AVANT d'ecrire du SQL ou une policy.",
74
+ {},
75
+ async () => ({ content: [{ type: "text", text: CONVENTIONS }] }),
76
+ );
77
+
62
78
  outils.tool(
63
79
  "run_sql",
64
80
  "Exécute du SQL (DDL/DML/RLS) sur la base. Après un changement de schéma, termine par: NOTIFY pgrst, 'reload schema';",
package/clicbase-mcp.mjs CHANGED
@@ -21,6 +21,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
21
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
22
  import { z } from "zod";
23
23
  import { filtre, lectureSeule } from "./portee.mjs";
24
+ import { CONVENTIONS, INSTRUCTIONS } from "./savoir.mjs";
24
25
 
25
26
  const API = process.env.CLICBASE_API ?? process.env.ROROUIRA_API ?? "https://clicbase.com/api/v1/databases";
26
27
 
@@ -37,10 +38,112 @@ if (!CLE) {
37
38
  const out = (d) => ({ content: [{ type: "text", text: typeof d === "string" ? d : JSON.stringify(d, null, 2) }] });
38
39
  const fail = (e) => ({ isError: true, content: [{ type: "text", text: String(e?.message ?? e) }] });
39
40
 
41
+ // L'API de LECTURE des projets, derivee de celle de provisioning.
42
+ const API_LECTURE = API.replace(/\/databases\/?$/, "/admin/projects");
43
+
44
+ /**
45
+ * Traduit un statut HTTP en phrase utile.
46
+ *
47
+ * ⚠️ « Clicbase 403 » NE DIT RIEN, ET ON L'A VU COUTER UNE HEURE. Le
48
+ * 2026-09-17, une session a lu ce nombre, conclu que la cle etait morte, et
49
+ * cherche du mauvais cote. Mesure ensuite : la cle etait vivante, jamais
50
+ * revoquee, et cette route ne peut PAS repondre 403 (400 sans en-tete, 401 cle
51
+ * refusee, 429 debit, 500 provisioning). Le nombre seul envoie chercher au
52
+ * hasard.
53
+ */
54
+ /**
55
+ * Lit une reponse en echec SANS PERDRE CE QU'ELLE CONTENAIT.
56
+ *
57
+ * ⚠️ `res.json().catch(() => ({}))` EFFACE LA PREUVE. Le 2026-09-17, une session
58
+ * a rapporte « Clicbase 403: {} » sur trois tentatives. Ce `{}` ne voulait pas
59
+ * dire « corps vide » : il voulait dire « corps ILLISIBLE en JSON », donc pas
60
+ * notre application, qui rend toujours un champ `error`. L'information qui
61
+ * aurait tranche en dix secondes, l'en-tete `cf-ray` et les premiers octets du
62
+ * corps, avait ete jetee par le `catch`.
63
+ *
64
+ * On garde donc : le statut, l'origine declaree par le serveur, l'identifiant
65
+ * Cloudflare s'il existe, et le debut du corps tel quel.
66
+ */
67
+ async function lireEchec(res) {
68
+ const brut = await res.text().catch(() => "");
69
+ let json = null;
70
+ try {
71
+ json = JSON.parse(brut);
72
+ } catch {
73
+ /* pas du JSON : c'est precisement ce qu'on veut savoir */
74
+ }
75
+ const via = res.headers.get("cf-ray")
76
+ ? `Cloudflare (cf-ray ${res.headers.get("cf-ray")})`
77
+ : (res.headers.get("server") ?? "origine inconnue");
78
+ return {
79
+ json,
80
+ // ⚠️ UN CORPS NON-JSON EST LA SIGNATURE D'UN REFUS EN AMONT. On le dit en
81
+ // clair au lieu de rendre un objet vide qui ressemble a une reponse.
82
+ detail: json
83
+ ? JSON.stringify(json)
84
+ : brut.trim()
85
+ ? `reponse NON-JSON de ${via} : ${brut.replace(/\s+/g, " ").slice(0, 180)}`
86
+ : `corps vide, servi par ${via} — la requete n'a probablement pas atteint Clicbase.`,
87
+ };
88
+ }
89
+
90
+ function expliquer(statut) {
91
+ if (statut === 400) return "requete incomplete : le champ `name` est obligatoire.";
92
+ if (statut === 401)
93
+ return "cle refusee : invalide, expiree ou revoquee. Attention, le bouton « Rouler » du tableau de bord REMPLACE le secret : l'ancien meurt aussitot.";
94
+ if (statut === 403)
95
+ return "hors du perimetre de cette cle. Une cle de Docker ne voit que son Docker. Verifie sa portee sur /api/v1/admin/me.";
96
+ if (statut === 429) return "trop de requetes, attends une minute.";
97
+ if (statut === 500)
98
+ return "la plateforme a refuse l'operation. Les creations de projet sont actuellement fermees : un projet qui n'existe pas deja ne peut pas etre cree.";
99
+ return "reponse inattendue.";
100
+ }
101
+
102
+ /**
103
+ * Trouve un projet SANS RIEN CREER.
104
+ *
105
+ * ⚠️ IL ACCEPTE LE NOM **OU** LE SLUG, et c'est le coeur du correctif. Le
106
+ * projet « présidence » a pour slug `presidence` : un assistant qui tape le
107
+ * slug, ou qui perd l'accent en chemin, ne trouvait rien. Ici on cherche dans
108
+ * les deux, puis on rend le nom EXACT tel qu'il est en base.
109
+ */
110
+ async function trouver(name) {
111
+ const res = await fetch(API_LECTURE, {
112
+ headers: { authorization: `Bearer ${CLE}` },
113
+ });
114
+ if (!res.ok) {
115
+ const e = await lireEchec(res);
116
+ throw new Error(`Clicbase ${res.status} — ${expliquer(res.status)} ${e.detail}`);
117
+ }
118
+ const data = await res.json().catch(() => ({}));
119
+ const liste = Array.isArray(data.projects) ? data.projects : [];
120
+ const cible = String(name).trim().toLowerCase();
121
+ const p =
122
+ liste.find((x) => String(x.name ?? "").toLowerCase() === cible) ??
123
+ liste.find((x) => String(x.slug ?? "").toLowerCase() === cible);
124
+ if (!p) {
125
+ const noms = liste.map((x) => x.slug ?? x.name).filter(Boolean).join(", ");
126
+ throw new Error(
127
+ `Aucun projet « ${name} » dans le perimetre de cette cle.` +
128
+ (noms ? ` Projets visibles : ${noms}.` : " Cette cle ne voit aucun projet."),
129
+ );
130
+ }
131
+ return p;
132
+ }
133
+
40
134
  // Provisionne (ou récupère, idempotent) un projet et renvoie ses infos.
135
+ //
136
+ // ⚠️ `creer: false` POUR TOUT OUTIL DE LECTURE. Cet appel est un POST sur
137
+ // l'endpoint de provisioning : idempotent quand le projet existe, CREATEUR
138
+ // sinon. `list_tables` survit au mode lecture seule et passait par ici : la
139
+ // promesse « aucune ecriture » etait donc fausse au niveau du transport, pendant
140
+ // que `create_database` disparaissait de la liste des outils. On verifie
141
+ // l'existence par une LECTURE d'abord, et on ne poste qu'un nom deja connu.
41
142
  const cache = new Map();
42
- async function resolve(name, domain) {
143
+ async function resolve(name, domain, { creer = true } = {}) {
43
144
  if (cache.has(name)) return cache.get(name);
145
+ let nomExact = name;
146
+ if (!creer) nomExact = (await trouver(name)).name ?? name;
44
147
  const res = await fetch(API, {
45
148
  method: "POST",
46
149
  headers: {
@@ -48,10 +151,13 @@ async function resolve(name, domain) {
48
151
  // ⚠️ UNE CLE, PAS UN MOT DE PASSE. Voir l'en-tete du fichier.
49
152
  authorization: `Bearer ${CLE}`,
50
153
  },
51
- body: JSON.stringify({ name, domain }),
154
+ body: JSON.stringify({ name: nomExact, domain }),
52
155
  });
156
+ if (!res.ok) {
157
+ const e = await lireEchec(res);
158
+ throw new Error(`Clicbase ${res.status} — ${expliquer(res.status)} ${e.detail}`);
159
+ }
53
160
  const data = await res.json().catch(() => ({}));
54
- if (!res.ok) throw new Error(`Clicbase ${res.status}: ${JSON.stringify(data)}`);
55
161
  const info = {
56
162
  url: data.restApi?.url,
57
163
  anonKey: data.restApi?.anonKey,
@@ -64,8 +170,8 @@ async function resolve(name, domain) {
64
170
  }
65
171
 
66
172
  // Appel d'un endpoint du projet avec la clé service.
67
- async function call(name, path, method = "POST", body) {
68
- const p = await resolve(name);
173
+ async function call(name, path, method = "POST", body, opts) {
174
+ const p = await resolve(name, undefined, opts);
69
175
  if (!p.url || !p.serviceKey) throw new Error("Projet non résolu (clés manquantes).");
70
176
  const res = await fetch(`${p.url}${path}`, {
71
177
  method,
@@ -77,7 +183,15 @@ async function call(name, path, method = "POST", body) {
77
183
  return data;
78
184
  }
79
185
 
80
- const server = new McpServer({ name: "clicbase", version: "2.0.0" });
186
+ // ⚠️ `instructions` EST LU A LA CONNEXION, AVANT LE PREMIER APPEL. C'est le
187
+ // seul endroit qu'un assistant ne peut pas oublier de consulter : le client
188
+ // MCP le pose dans le contexte du modele. Sans lui, un assistant recevait
189
+ // TROIS outils et trente mots de description, puis devinait les conventions
190
+ // de la plateforme. Voir savoir.mjs.
191
+ const server = new McpServer(
192
+ { name: "clicbase", version: "2.0.0" },
193
+ { instructions: INSTRUCTIONS },
194
+ );
81
195
 
82
196
  // ⚠️ EN LECTURE SEULE, LES OUTILS QUI ECRIVENT NE SONT PAS ENREGISTRES. Voir
83
197
  // portee.mjs : un outil absent n'est jamais demande, un outil present qui
@@ -85,6 +199,13 @@ const server = new McpServer({ name: "clicbase", version: "2.0.0" });
85
199
  const SEULEMENT_LECTURE = lectureSeule();
86
200
  const outils = filtre(server, SEULEMENT_LECTURE);
87
201
 
202
+ outils.tool(
203
+ "clicbase_conventions",
204
+ "Les conventions Clicbase : formes exactes des policies RLS, droits par defaut, rechargement du schema, appels REST, fonctions Edge. A lire AVANT d'ecrire du SQL ou une policy.",
205
+ {},
206
+ async () => ({ content: [{ type: "text", text: CONVENTIONS }] }),
207
+ );
208
+
88
209
  outils.tool(
89
210
  "create_database",
90
211
  "Crée (ou récupère, idempotent) une base Clicbase pour un site. Renvoie la chaîne de connexion PostgreSQL, l'URL de l'API REST et les clés anon/service.",
@@ -113,7 +234,20 @@ outils.tool(
113
234
  "Liste les tables du schéma public d'un projet.",
114
235
  { name: z.string() },
115
236
  async ({ name }) => {
116
- try { return out(await call(name, "/sql", "POST", { query: "select table_name from information_schema.tables where table_schema='public' order by table_name" })); } catch (e) { return fail(e); }
237
+ try {
238
+ // ⚠️ `creer: false` : un outil de lecture ne provisionne jamais.
239
+ return out(
240
+ await call(
241
+ name,
242
+ "/sql",
243
+ "POST",
244
+ { query: "select table_name from information_schema.tables where table_schema='public' order by table_name" },
245
+ { creer: false },
246
+ ),
247
+ );
248
+ } catch (e) {
249
+ return fail(e);
250
+ }
117
251
  },
118
252
  );
119
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicbase/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mcp": "clicbase-mcp.mjs",
@@ -35,6 +35,7 @@
35
35
  "clicbase-mcp.mjs",
36
36
  "clicbase-db-mcp.mjs",
37
37
  "portee.mjs",
38
+ "savoir.mjs",
38
39
  "README.md",
39
40
  "LICENSE"
40
41
  ],
package/portee.mjs CHANGED
@@ -43,6 +43,11 @@ export const OUTILS = {
43
43
  enable_rls: "ecriture",
44
44
  create_policy: "ecriture",
45
45
  get_oauth_provider: "lecture",
46
+ // ⚠️ RENDU EN LECTURE SEULE, ET C'EST TOUT L'INTERET. Cet outil ne rend que
47
+ // du texte : les conventions de la plateforme. C'est precisement le mode
48
+ // restreint qui en a le plus besoin, puisqu'il ne reste alors que trois
49
+ // outils et qu'un assistant doit deviner le reste.
50
+ clicbase_conventions: "lecture",
46
51
  };
47
52
 
48
53
  /** Ce qu'on garde en lecture seule. */
package/savoir.mjs ADDED
@@ -0,0 +1,122 @@
1
+ // CE QUE L'ASSISTANT DOIT SAVOIR, ET QU'AUCUN OUTIL NE LUI APPREND.
2
+ //
3
+ // Question de Teiki le 2026-09-17 : « quand un client ajoute la commande MCP à
4
+ // son IA, elle va tout comprendre ? » Mesuré dans le paquet publié, la réponse
5
+ // était non. En lecture seule, un assistant reçoit TROIS outils et une
6
+ // trentaine de mots de description. Il sait explorer un schéma. Il ignore tout
7
+ // ce qui fait perdre des journées ici.
8
+ //
9
+ // ⚠️ LES SIX PIÈGES CI-DESSOUS SONT RÉELS, PAS THÉORIQUES. Ils viennent de la
10
+ // section « Pièges qui ont déjà coûté cher » du dépôt, écrite après coup à
11
+ // chaque fois. Ils ont en commun de ne produire NI exception NI ligne de
12
+ // journal : une policy qui ne protège rien répond 200, un grant qui ne
13
+ // restreint pas répond 200, une clé service sans BYPASSRLS répond 200 avec un
14
+ // tableau vide. Un assistant qui les ignore croit avoir réussi.
15
+ //
16
+ // ⚠️ DEUX CANAUX, ET CE N'EST PAS UNE REDONDANCE. `INSTRUCTIONS` part dans le
17
+ // champ prévu par le protocole : le client MCP le pose dans le contexte du
18
+ // modèle À LA CONNEXION, avant le premier appel, et on ne peut donc pas
19
+ // oublier de le lire. `CONVENTIONS` est le détail, rendu par un outil quand
20
+ // l'assistant en a besoin. Tout mettre dans le premier gonflerait chaque
21
+ // conversation ; tout mettre dans le second laisserait l'assistant écrire une
22
+ // policy fausse sans avoir jamais su qu'il fallait demander.
23
+ //
24
+ // ⚠️ CE FICHIER N'IMPORTE RIEN, exprès : le lanceur de tests du dépôt charge
25
+ // les modules voisins avec leur extension, et un import casserait sa lecture.
26
+
27
+ /** Posé dans le contexte du modèle dès la connexion. Court par obligation. */
28
+ export const INSTRUCTIONS = `Tu es connecté à un projet Clicbase : une base PostgreSQL managée, exposée en API REST par PostgREST.
29
+
30
+ SIX PIÈGES QUI NE LÈVENT AUCUNE ERREUR. Ils répondent tous « 200 OK ».
31
+ 1. \`with check\` n'est PAS \`using\`. Sur INSERT et UPDATE, PostgreSQL n'évalue QUE \`with check\`. Une policy qui n'a qu'un \`using\` ne protège pas l'écriture.
32
+ 2. Chaque nouvelle table accorde le CRUD à \`authenticated\` par défaut. Un \`grant select\` ne restreint RIEN : il faut \`revoke\`.
33
+ 3. Après tout changement de schéma, termine par \`notify pgrst, 'reload schema';\` sinon l'API continue de servir l'ancien schéma.
34
+ 4. Le rôle \`service_role\` a besoin de \`BYPASSRLS\`. Sans lui : 200 OK et un tableau vide, sans explication.
35
+ 5. Une fonction Edge renvoie \`{ status, body }\`, jamais \`new Response(...)\`.
36
+ 6. Deux authentifications sans rapport coexistent. Celle du PROJET (tes utilisateurs finaux) vit dans le schéma \`auth\` de cette base et alimente \`auth.uid()\` dans les policies. Celle de la PLATEFORME (le compte Clicbase) ne s'écrit jamais dans une policy.
37
+
38
+ CE QUI N'EXISTE PAS, ET QUE DEUX ASSISTANTS ONT DEJA INVENTE. Il n'y a AUCUN registre Docker : ni \`docker login\`, ni \`docker pull\`, ni \`registry.*.clicbase.com\`. Ne devine aucun nom d'hote. Ce serveur MCP ne rend pas le code source d'un site heberge : pour recuperer des fichiers, c'est le SFTP du tableau de bord ou son gestionnaire de fichiers, et l'API d'administration ne fait que les ECRIRE (POST /sites/<id>/files), jamais les lire.
39
+
40
+ APPELS REST : en-têtes \`apikey\` ET \`Authorization: Bearer\`, la même clé dans les deux. Filtres dans l'URL, façon PostgREST : \`?select=id,title&status=eq.published&order=created_at.desc\`.
41
+
42
+ DEUX CLÉS À NE PAS CONFONDRE. \`anon\` est publique et soumise à la RLS, elle va dans le navigateur. \`service\` CONTOURNE la RLS et voit tout : serveur uniquement, jamais dans un front, jamais dans un dépôt.
43
+
44
+ Appelle \`clicbase_conventions\` avant d'écrire du SQL ou une policy : tu y trouveras les formes exactes.`;
45
+
46
+ /** Le détail, rendu à la demande par un outil de lecture. */
47
+ export const CONVENTIONS = `# Conventions Clicbase
48
+
49
+ ## Row Level Security
50
+
51
+ \`\`\`sql
52
+ alter table articles enable row level security;
53
+
54
+ -- LECTURE : using suffit.
55
+ create policy "lecture_publique" on articles
56
+ for select to anon, authenticated using (status = 'published');
57
+
58
+ -- ÉCRITURE : with check est OBLIGATOIRE, using ne sert pas.
59
+ create policy "chacun_ses_lignes" on articles
60
+ for insert to authenticated with check (user_id = auth.uid());
61
+
62
+ create policy "modifier_les_siennes" on articles
63
+ for update to authenticated
64
+ using (user_id = auth.uid()) -- quelles lignes il peut viser
65
+ with check (user_id = auth.uid()); -- ce qu'il peut y écrire
66
+
67
+ notify pgrst, 'reload schema';
68
+ \`\`\`
69
+
70
+ ⚠️ Sur UPDATE, les deux clauses ont des rôles différents : \`using\` choisit les
71
+ lignes visées, \`with check\` valide le résultat. Omettre la seconde laisse
72
+ réécrire \`user_id\` vers quelqu'un d'autre.
73
+
74
+ ## Les droits, avant la RLS
75
+
76
+ La RLS filtre les lignes ; les GRANT décident si la table est atteignable. Une
77
+ nouvelle table est ouverte en CRUD à \`authenticated\` par défaut, donc :
78
+
79
+ \`\`\`sql
80
+ revoke all on articles from anon, authenticated;
81
+ grant select on articles to anon, authenticated;
82
+ grant insert, update, delete on articles to authenticated;
83
+ \`\`\`
84
+
85
+ Un \`grant select\` seul n'enlève rien : il ajoute. C'est \`revoke\` qui ferme.
86
+
87
+ ## auth.uid()
88
+
89
+ Il rend le \`sub\` du JWT du projet, donc l'identifiant de l'utilisateur final
90
+ connecté. Il vaut NULL avec la clé \`anon\`. Une policy qui doit exiger un compte
91
+ écrit \`auth.uid() is not null\`, pas \`true\`.
92
+
93
+ ## API REST
94
+
95
+ \`\`\`
96
+ GET /db/<slug>/articles?select=id,title&status=eq.published&order=created_at.desc
97
+ en-têtes : apikey: <clé> et Authorization: Bearer <clé>
98
+ \`\`\`
99
+
100
+ Opérateurs : \`eq.\`, \`neq.\`, \`gt.\`, \`gte.\`, \`lt.\`, \`lte.\`, \`like.\`, \`ilike.\`,
101
+ \`in.(a,b)\`, \`is.null\`. Les relations se tirent par \`select=*,auteur(*)\`.
102
+
103
+ ## Après toute DDL
104
+
105
+ \`notify pgrst, 'reload schema';\` dans la même exécution. Sans cela, la table
106
+ existe en base et l'API répond 404 : le défaut ressemble à une erreur de nom.
107
+
108
+ ## Fonctions Edge
109
+
110
+ \`\`\`js
111
+ export default async function (req) {
112
+ return { status: 200, body: { ok: true } };
113
+ }
114
+ \`\`\`
115
+
116
+ Jamais \`new Response(...)\` : l'hôte attend un objet, pas une réponse HTTP.
117
+
118
+ ## Ce qu'aucun outil ne fera pour toi
119
+
120
+ Distinguer un \`drop table\` prévu d'un \`drop table\` accidentel. Le DDL est la
121
+ raison d'être de \`run_sql\`, et aucune liste de mots interdits ne sépare les
122
+ deux. Avant un ordre destructeur, dis ce que tu vas faire et demande.`;