# Base de données

Chaque projet Bastide peut porter une base PostgreSQL 17, avec son API de données, son authentification et son stockage. Cette page dit comment votre application s'y connecte, comment la lire et l'écrire depuis la console, par un agent ou par l'API, quelles extensions sont permises, et ce que chaque palier change aux connexions, aux délais et à l'isolation.

## Ce que vous obtenez

- **Une base par projet**, et un seul jeu de rôles pour elle. Toutes les classes d'environnement du projet (production, préproduction, prévisualisations) parlent à la **même** base : il n'existe pas aujourd'hui de base par branche (voir [Branches et migrations](branches.md)).
- **Un mutualiseur de connexions** (Supavisor) devant la base. Votre application ne joint jamais PostgreSQL directement.
- **Une API de données** au format de Supabase sous l'adresse du projet : `/rest/v1` (PostgREST), `/auth/v1`, `/storage/v1`, `/realtime/v1`.
- **pgvector** construit dans l'image du cluster, permis sur tous les paliers.

La base naît avec le projet quand vous le créez par `project_create_with_backend`, qui met son approvisionnement en file. Son état se lit par `backend_read` : `provisioning`, `ready`, `failed`, `deleting` ou `retained`. Un projet sans base affiche sur l'écran **Données** : « Ce projet n'a pas de base de données. »

## Se connecter depuis votre application

À chaque déploiement, Bastide pose deux variables sur l'environnement visé, si la base est prête :

| Variable | Mode du mutualiseur | Usage |
| --- | --- | --- |
| `DATABASE_URL` | transaction | L'adresse par défaut de l'application : Prisma, Drizzle, `pg`, NextAuth. |
| `DIRECT_URL` | session, avec `connection_limit=1` | Les migrations (`prisma migrate` tient un verrou consultatif de session) et tout ce qui exige l'état de session. |

Les deux adresses portent le même rôle et le même mot de passe. Le nom d'utilisateur a la forme `<rôle>.<base>` : c'est ainsi que le mutualiseur trouve votre base.

- **Votre variable l'emporte.** Si vous posez vous-même `DATABASE_URL` ou `DIRECT_URL` (par exemple vers une base hébergée ailleurs), Bastide n'y touche plus.
- **N'ajoutez pas `pgbouncer=true`.** Avec ce paramètre, Prisma envoie des requêtes simples que le mode transaction refuse (`EPSSIMPLEQUERY`). Sans lui, les instructions préparées passent dans les deux modes.
- **Un environnement isolé ne reçoit rien.** Une prévisualisation bifurquée d'un dépôt tiers n'emporte aucune variable sensible, et ces deux adresses en sont.

> **Attention.** La base n'est joignable que depuis les conteneurs de votre projet, sur le réseau privé de la plateforme. Aucune connexion directe depuis votre poste (`psql`, un client graphique) n'est ouverte. Pour lire ou écrire à la main, passez par l'éditeur SQL de la console, par un agent ou par l'API, décrits plus bas.

### Ce que le mode transaction ne tient pas

Le mode transaction partage les connexions serveur entre vos clients. Trois comportements en découlent, chacun reproduit par un test :

| Réf | Ce qui se passe | Ce qu'il faut faire |
| --- | --- | --- |
| LIM-03 | `NOTIFY` n'atteint pas un client qui fait `LISTEN`. | Écouter depuis une connexion en mode session (`DIRECT_URL`). |
| LIM-04 | `pg_advisory_lock` n'exclut pas une autre connexion du même projet. | `pg_advisory_xact_lock` dans une transaction, ou le mode session. |
| LIM-05 | Un `SET`, une table temporaire ou un curseur `WITH HOLD` passent à la connexion suivante. | `SET LOCAL`, `ON COMMIT DROP`, un curseur sans `WITH HOLD`, ou le mode session. |

## L'API de données

L'adresse de votre projet est `https://<identifiant>.donnees.bastide.app`, où l'identifiant est celui du backend (`bak_…`, rendu par `backend_read`) sans le préfixe `bak_`, en minuscules. Les chemins de Supabase y sont servis sans réécriture : la clé dans l'en-tête `apikey`, un jeton de session dans `Authorization`.

- Seul le schéma `public` est servi par l'API.
- Une réponse porte au plus 1000 lignes, tronquée sans erreur (LIM-01) : paginez avec `Range`, ou `limit` et `offset`.
- Une ressource ne s'embarque que le long d'une clé étrangère déclarée, sinon 400 `PGRST200` (LIM-02).
- Une table n'est accordée aux rôles de l'API que si elle porte la sécurité au niveau des lignes **et** au moins une politique (LIM-11). Voir [Sécurité des données](data-security.md).

Les clés publiable et secrètes se gèrent depuis la section « Clés et points d'entrée » de **Données** : voir [Authentification](auth.md).

## L'éditeur SQL de la console

Ouvrez **Données**, puis la section **Éditeur SQL**. La liste des tables est à gauche ; un clic en montre les lignes ou la structure.

1. **Lecture seule par défaut.** La case est cochée : la base refuse alors toute écriture, quelle que soit la forme de l'instruction. Une instruction à la fois, cent lignes rendues au plus. Ctrl+Entrée (Cmd+Entrée sur Mac) exécute.
2. **Passer en écriture** demande le rang Mainteneur, qu'on n'attribue qu'à un compte qui tient un second facteur (voir [Compte](account.md)). Le bouton devient « Mesurer l'effet » : l'instruction est exécutée puis annulée, et la fenêtre dit combien de lignes seraient insérées, modifiées ou supprimées, et quelles tables seraient supprimées ou vidées.
3. **Appliquer** rejoue l'instruction et ne la valide que si l'effet est le même. Si la base a changé entre-temps, le décompte est refait et la fenêtre le signale.
4. **Un effet destructif** (table supprimée ou vidée, lignes supprimées) demande de retaper le nombre de lignes affectées.

Chaque écriture appliquée entre au journal d'audit avec son effet et votre nom. Un résultat se copie en CSV. Ce que la base rend (noms de colonnes, cellules, messages) est affiché comme du texte, jamais interprété.

## Les tables

La section « Tables » de **Données** montre, en trois colonnes, la liste des tables, les cent premières lignes de celle que vous choisissez, et à droite l'essentiel de la base : points d'entrée, état de l'API REST, clés, temps réel et posture de sécurité. Le bouton **Structure** bascule vers les colonnes et leurs types. Une table du schéma `public` sans sécurité au niveau des lignes porte l'étiquette « Exposée sans politique ».

Ces lectures passent par la même opération que l'éditeur, `database.sql.read` : elles sont en lecture seule et tracées.

## Par un agent, la ligne de commande ou l'API

| Geste | Outil MCP | Ligne de commande | API |
| --- | --- | --- | --- |
| Lire | `database_sql_read` | `bastide database sql read` | `GET /v1/projects/{projectId}/database/sql` |
| Écrire | `database_sql_write` | `bastide database sql write` | `POST /v1/projects/{projectId}/database/sql` |
| Lire la base et ses clés | `backend_read` | `bastide backend read` | `GET /v1/projects/{projectId}/backend` |

Une lecture (rang `developer`, portée `db:read`) :

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "sql": "select id, total from commandes order by id desc limit 10"
}
```

Une écriture (rang `maintainer`, portée `db:write`) se fait en deux temps. Le premier appel exécute, mesure, annule et rend un nonce ; le second, avec `confirmationNonce`, ne valide que si l'effet est identique. Un effet destructif exige en plus `typedConfirmation` (le nombre de lignes, en chiffres) et la portée `db:destructive`. Une personne valide l'appel avant qu'il parte ; un compte de service ne peut pas écrire.

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "sql": "delete from commandes where etat = 'annulee'",
  "idempotencyKey": "commandes-annulees-2026-10-05"
}
```

Un agent ne lit aucune colonne classée donnée personnelle : l'instruction est refusée sous `colonne_personnelle_refusee` (voir [Sécurité des données](data-security.md)).

## Les extensions permises

Une extension s'installe par `create extension` dans une migration ou dans l'éditeur SQL en écriture. La base n'accepte que celles de la liste blanche du palier du projet.

| Palier | Extensions permises |
| --- | --- |
| Découverte | `btree_gin`, `btree_gist`, `citext`, `cube`, `earthdistance`, `fuzzystrmatch`, `hstore`, `intarray`, `isn`, `lo`, `ltree`, `moddatetime`, `pg_trgm`, `pgcrypto`, `seg`, `tablefunc`, `tsm_system_rows`, `unaccent`, `uuid-ossp`, `vector` |
| Standard, Pro, Souverain | celles de Découverte, plus `autoinc`, `bloom`, `dict_int`, `dict_xsyn`, `insert_username`, `intagg`, `refint`, `sslinfo`, `tcn`, `tsm_system_time` |

La grille annonce des plafonds (20 en Découverte, 50 en Standard) : la liste Standard en nomme moins, volontairement. Les extensions arbitraires de Pro et Souverain demandent une instance dédiée, qui n'est pas encore servie : un projet Pro suit aujourd'hui la liste Standard.

Ne sont jamais permises sur le cluster partagé : `adminpack`, `amcheck`, `dblink`, `file_fdw`, `pageinspect`, `pg_buffercache`, `pg_freespacemap`, `pg_prewarm`, `pg_stat_statements`, `pg_surgery`, `pg_visibility`, `pg_walinspect`, `pgrowlocks`, `pgstattuple`, `plperl`, `plpython3u`, `pltcl`, `postgres_fdw`, `xml2`. Elles ouvrent le réseau depuis le nœud de données, le système de fichiers, le cache commun ou du code libre.

Changer de palier remplace la liste mais ne retire aucune extension déjà installée : c'est sa prochaine installation qui serait refusée.

## Connexions et délais par palier

| Palier | Connexions clientes / serveur | Délai d'une requête | Délai d'une transaction inactive |
| --- | --- | --- | --- |
| Découverte | 30 / 3 | 30 s | 60 s |
| Standard | 60 / 5 | 30 s | 60 s |
| Pro | 200 / 15 | 300 s | 300 s |
| Souverain | négociées | 300 s | 300 s |

- **Au-delà des connexions clientes**, le mutualiseur refuse le client de trop avec un message qui nomme le plafond (« max client connections reached, limit: 60 »).
- **Au-delà du délai**, PostgreSQL interrompt la requête (code `57014`) ou ferme la connexion restée inactive en transaction (code `25P03`). Le délai en vigueur se lit par `SHOW statement_timeout`.
- **Un changement de palier** prend effet sans redémarrage : les connexions du seul projet sont fermées une fois, et se rouvrent sous les nouvelles bornes.
- **Le temps réel** tient une connexion en mode session par instance tant qu'un client est branché : elle compte dans votre plafond serveur (trois en Découverte).

## L'isolation par palier

| Palier | Isolation de la base, d'après la grille | Ce qui est servi aujourd'hui |
| --- | --- | --- |
| Découverte | Cluster partagé | Cluster partagé, 800 projets au plus par cluster |
| Standard | Cluster partagé | Cluster partagé, 400 projets au plus par cluster dès qu'il porte un projet Standard |
| Pro | Instance PostgreSQL dédiée | Pas encore : le projet reste sur le cluster partagé, avec les bornes Pro ci-dessus |
| Souverain | Instance et nœud dédiés | Pas encore : sur devis |

Sur le cluster partagé, chaque projet a sa base et ses rôles ; les rôles du projet ne se connectent que par le mutualiseur. Le nombre de projets par cluster est un choix de risque publié : un incident du cluster touche d'un coup tout ce qu'il porte.

## Erreurs fréquentes

- **`variables_requises_manquantes` sur `DATABASE_URL`** : la base n'était pas prête au déploiement. Attendez l'état `ready` (`backend_read`), puis redéployez.
- **`EPSSIMPLEQUERY`** : retirez `pgbouncer=true` de l'adresse.
- **`backend_indisponible`** : la plateforme ne joint pas la base ; le message nomme la cause.
- **`colonne_personnelle_refusee`** : l'instruction d'un agent touche une colonne classée personnelle. Une personne la lit depuis la console.
- **`42704` sur un `grant … to authenticated`** : les rôles portent le nom de la base en préfixe. Activez la sécurité et écrivez une politique : les droits suivent (LIM-11).
- **`57014`** : la requête a dépassé le délai du palier. Ajoutez un index, découpez le travail, ou changez de palier.

## Voir aussi

- [Branches et migrations](branches.md), [Sauvegardes](backups.md), [Sécurité des données](data-security.md), [Authentification](auth.md)
- [Limites par palier](limits.md), [Migrer depuis Supabase](from-supabase.md)
- Référence : [database.sql.read](../reference/operations/database.sql.read.md), [database.sql.write](../reference/operations/database.sql.write.md), [backend.read](../reference/operations/backend.read.md)
