Options de base de données

Sur cette page

EmDash prend en charge plusieurs backends de base de données. Choisissez en fonction de votre cible de déploiement.

Aperçu

Base de donnéesIdéale pourDéploiement
D1Cloudflare WorkersEdge, distribué mondialement
HyperdrivePostgreSQL sur Cloudflare WorkersEdge, Postgres existant
PostgreSQLProduction Node.jsToute plateforme avec Postgres
libSQLBases de données distantesEdge ou Node.js
SQLiteNode.js, développement localServeur unique

Cloudflare D1

D1 est la base de données SQLite serverless de Cloudflare. Utilisez-la lors du déploiement sur Cloudflare Workers.

import { d1 } from "@emdash-cms/cloudflare";

export default defineConfig({
	integrations: [
		emdash({
			database: d1({ binding: "DB" }),
		}),
	],
});

Configuration

OptionTypePar défautDescription
bindingstringNom du binding D1 de wrangler.jsonc
sessionstring"disabled"Mode de réplication de lecture (voir ci-dessous)
bookmarkCookiestring"__em_d1_bookmark"Nom du cookie pour les bookmarks de session

Mise en place

wrangler.jsonc

{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "emdash-db"
    }
  ]
}

wrangler.toml

[[d1_databases]]
binding = "DB"
database_name = "emdash-db"

Réplicas de lecture

D1 prend en charge la réplication de lecture pour réduire la latence de lecture pour les sites distribués mondialement. Lorsqu’elle est activée, les requêtes de lecture sont routées vers des réplicas proches au lieu de toujours accéder à la base de données primaire.

EmDash utilise l’API Sessions D1 pour gérer cela de manière transparente. Activez-le avec l’option session :

import { d1 } from "@emdash-cms/cloudflare";

export default defineConfig({
	integrations: [
		emdash({
			database: d1({
				binding: "DB",
				session: "auto",
			}),
		}),
	],
});

Modes de session

ModeComportement
"disabled"Pas de sessions. Toutes les requêtes vont au primaire. Par défaut.
"auto"Les requêtes anonymes lisent depuis la réplica la plus proche. Les utilisateurs authentifiés obtiennent la consistance read-your-writes via des cookies bookmark.
"primary-first"Comme "auto", mais la première requête va toujours au primaire. Pour les sites avec des écritures très fréquentes.

Comment ça fonctionne

  • Visiteurs anonymes obtiennent first-unconstrained — les lectures vont à la réplica la plus proche pour la latence la plus basse. Comme les utilisateurs anonymes n’écrivent jamais, ils n’ont pas besoin de garanties de consistance.
  • Utilisateurs authentifiés (éditeurs, auteurs) obtiennent des sessions basées sur des bookmarks. Après une écriture, un cookie bookmark assure que la requête suivante voit au moins cet état.
  • Requêtes d’écriture (POST, PUT, DELETE) commencent toujours à la base de données primaire.
  • Requêtes au moment du build (Astro content collections) contournent complètement les sessions et utilisent directement le primaire.

libSQL

libSQL est un fork de SQLite qui prend en charge les connexions distantes. Utilisez-le quand vous avez besoin d’une base de données distante sans Cloudflare D1.

import { libsql } from "emdash/db";

export default defineConfig({
	integrations: [
		emdash({
			database: libsql({
				url: process.env.LIBSQL_DATABASE_URL,
				authToken: process.env.LIBSQL_AUTH_TOKEN,
			}),
		}),
	],
});

Configuration

OptionTypeDescription
urlstringURL de la base de données (libsql://... ou file:...)
authTokenstringJeton d’authentification runtime pour les bases distantes (optionnel pour local)
migrationAuthTokenEnvstringNom de variable du jeton de migration (par défaut TURSO_AUTH_TOKEN)

Développement local

Utilisez un fichier libSQL local pendant le développement :

database: libsql({ url: "file:./data.db" });

PostgreSQL

PostgreSQL est pris en charge pour les déploiements Node.js qui nécessitent une base de données relationnelle complète.

import { postgres } from "emdash/db";

export default defineConfig({
	integrations: [
		emdash({
			database: postgres({
				connectionString: process.env.DATABASE_URL,
			}),
		}),
	],
});

Configuration

Vous pouvez vous connecter avec une chaîne de connexion ou des paramètres individuels :

// Chaîne de connexion
database: postgres({
	connectionString: "postgres://user:password@localhost:5432/emdash",
});

// Paramètres individuels
database: postgres({
	host: "localhost",
	port: 5432,
	database: "emdash",
	user: "emdash",
	password: process.env.DB_PASSWORD,
	ssl: true,
});
OptionTypeDescription
connectionStringstringURL de connexion PostgreSQL
hoststringHôte de la base de données
portnumberPort de la base de données
databasestringNom de la base de données
userstringUtilisateur de la base de données
passwordstringMot de passe de la base de données
sslbooleanActiver SSL
pool.minnumberConnexions pool minimum (par défaut 0)
pool.maxnumberConnexions pool maximum (par défaut 10)
pool.connectionTimeoutMillisnumberAttente maximale de connexion (pg par défaut : 0, pas de timeout)
pool.idleTimeoutMillisnumberDurée de vie du client inactif (pg par défaut : 10 000 ms)
migrationConnectionStringEnvstringNom de variable de la chaîne de connexion de migration (par défaut DATABASE_URL)

Définissez pool.connectionTimeoutMillis à une valeur non nulle pour limiter combien de temps une requête attend lorsque PostgreSQL est inaccessible ou qu’aucune connexion poolée ne devient disponible. Définissez pool.idleTimeoutMillis à 0 pour garder les clients inactifs ouverts jusqu’à la fermeture du pool. Omettre l’une ou l’autre option préserve la valeur par défaut de pg.

Exigences du rôle de base de données

EmDash crée et met à jour ses propres tables PostgreSQL. Les migrations core créent et modifient les tables système et de collections, les types de contenu créent les tables ec_*, et ajouter ou supprimer un champ modifie sa table de collection. Le rôle PostgreSQL configuré a donc besoin de l’autorité sur le schéma pendant toute la durée de vie du site, pas seulement pendant la configuration initiale.

Utilisez un rôle canonique pour EmDash. Il a besoin de :

  • CONNECT sur la base de données ;
  • USAGE et CREATE sur le schéma actif ;
  • la propriété de chaque table et fonction EmDash, soit directement, soit par l’appartenance avec INHERIT au rôle propriétaire ; et
  • SELECT, INSERT, UPDATE et DELETE sur ces tables.

Il n’a pas besoin d’être superutilisateur, d’avoir CREATEDB ou CREATEROLE, ni de créer des extensions. PostgreSQL ne fournit pas de grant ALTER ou DROP sur les tables : ces opérations appartiennent au propriétaire de l’objet et aux rôles qui héritent de ses privilèges. Accorder ALL sur une table à un rôle différent ne fait pas de ce rôle un propriétaire. EmDash n’exécute pas SET ROLE, donc une appartenance configurée sans héritage n’est pas suffisante.

La plupart des installations peuvent utiliser le schéma existant de la base de données, généralement public. C’est l’option la plus simple quand la base de données est dédiée à EmDash. Dans les exemples ci-dessous, emdash_app est le rôle de connexion dans la chaîne de connexion d’EmDash ; utilisez un rôle fournisseur existant ou créez un login dédié. Accordez l’accès avec une connexion administrative, en substituant vos noms de base de données, schéma et rôle :

GRANT CONNECT ON DATABASE app TO emdash_app;
GRANT USAGE, CREATE ON SCHEMA public TO emdash_app;

Ces grants permettent au rôle de créer de nouveaux objets. Ils ne changent pas le propriétaire des tables existantes ; voir Réparer la propriété mixte PostgreSQL.

EmDash utilise le current_schema() actif de PostgreSQL. Il ne crée pas de schéma et ne définit pas search_path, donc vérifiez la connexion avant le déploiement :

SELECT
  current_database(),
  session_user,
  current_user,
  current_schema(),
  current_setting('search_path');

Optionnel : utiliser un schéma dédié

Utilisez un schéma dédié quand EmDash partage une base de données avec une autre application ou quand vous voulez isoler ses objets de public. C’est optionnel et plus facile à configurer avant la première installation d’EmDash. Une base de données dédiée à EmDash n’a pas besoin d’un schéma séparé.

En supposant que le rôle canonique emdash_app existe déjà, créez et sélectionnez son schéma avec une connexion administrative :

GRANT CONNECT ON DATABASE app TO emdash_app;
CREATE SCHEMA emdash AUTHORIZATION emdash_app;
ALTER ROLE emdash_app IN DATABASE app SET search_path = emdash;

Cela ne déplace pas une installation existante depuis public et ne répare pas la propriété mixte. Les sites existants doivent garder leur schéma actuel et suivre Réparer la propriété mixte PostgreSQL à la place.

Pool de connexions

L’adaptateur utilise pg.Pool sous le capot. Ajustez la taille du pool selon votre déploiement :

database: postgres({
	connectionString: process.env.DATABASE_URL,
	pool: { min: 2, max: 20 },
});

Hyperdrive

Utilisez l’adaptateur hyperdrive() pour exécuter EmDash sur Cloudflare Workers adossé à une base de données PostgreSQL existante — ou compatible Postgres (par ex. PlanetScale Postgres). Hyperdrive pool et accélère la connexion via le réseau de Cloudflare ; le dialecte PostgreSQL d’EmDash exécute les requêtes.

import { hyperdrive, r2 } from "@emdash-cms/cloudflare";

export default defineConfig({
	integrations: [
		emdash({
			database: hyperdrive({ binding: "HYPERDRIVE" }),
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

Prérequis

  • pg >= 8.16.3 installé dans votre site (pnpm add pg)
  • compatibility_flags: ["nodejs_compat"]
  • compatibility_date >= "2024-09-23"

Mise en place

D’abord préparez le rôle PostgreSQL. Puis créez la configuration Hyperdrive avec la chaîne de connexion de ce rôle et ajoutez le binding à votre configuration Wrangler :

wrangler hyperdrive create emdash-db \
  --connection-string "postgres://user:password@host/db?sslmode=verify-full" \
  --caching-disabled

wrangler.jsonc

{
  "hyperdrive": [
    {
      "binding": "HYPERDRIVE",
      "id": "<your-hyperdrive-id>"
    }
  ]
}

wrangler.toml

[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<your-hyperdrive-id>"

Configuration

OptionTypePar défautDescription
bindingstring"HYPERDRIVE"Nom du binding primaire (cache désactivé) Hyperdrive
cachedBindingstringBinding optionnel avec cache activé pour les lectures anonymes (voir ci-dessous)
preferUncachedAfterWriteMsnumber60000*Après une publication, préférer binding pendant ces ms pour les lectures publiques anonymes (correspondre au Hyperdrive max_age)
migrationConnectionStringEnvstringCLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING>Variable d’environnement contenant l’URL directe PostgreSQL origin pour emdash migrate
maxnumber5Taille max du pool de connexions dans le Worker vers Hyperdrive

*La valeur par défaut 60000 s’applique uniquement quand cachedBinding est défini ; ignoré sinon.

Servir les lectures anonymes depuis le cache

Par défaut vous désactivez entièrement le cache Hyperdrive, car l’admin et les écritures ont besoin de la consistance read-after-write. Mais les requêtes publiques anonymes utilisant GET ou HEAD peuvent tolérer une courte fenêtre de péremption. Si ce compromis est acceptable, exécutez deux configurations Hyperdrive sur la même base de données : une avec le cache désactivé (le binding primaire) et une avec le cache activé (cachedBinding). EmDash route ces requêtes publiques anonymes via le binding avec cache activé et toutes les autres via le primaire sans cache.

# Primaire — cache DÉSACTIVÉ (utilisé par l'admin, requêtes authentifiées, écritures, migrations)
wrangler hyperdrive create emdash-db \
  --connection-string "postgres://user:password@host/db?sslmode=verify-full" \
  --caching-disabled

# Avec cache — MÊME rôle de base de données et chaîne de connexion, cache ACTIVÉ
wrangler hyperdrive create emdash-db-cached \
  --connection-string "postgres://user:password@host/db?sslmode=verify-full"
{
	"hyperdrive": [
		{ "binding": "HYPERDRIVE", "id": "<caching-disabled-id>" },
		{ "binding": "HYPERDRIVE_CACHED", "id": "<caching-enabled-id>" }
	]
}
database: hyperdrive({ binding: "HYPERDRIVE", cachedBinding: "HYPERDRIVE_CACHED" });

C’est le patron à deux configurations que Cloudflare documente pour le cache. EmDash décide quel binding utiliser par requête :

  • Lectures anonymes des chemins du site public (GET/HEAD, pas de session, pas sous /_emdash) → cachedBinding avec cache activé, sauf pour une courte fenêtre après une publication de contenu (par défaut 60s ; définissez preferUncachedAfterWriteMs à votre Hyperdrive max_age) quand EmDash préfère le binding sans cache pour qu’un rebuild ne puisse pas re-alimenter les caches edge/objets depuis des résultats Hyperdrive encore périmés.
  • Requêtes authentifiées (éditeurs, auteurs) → binding sans cache.
  • Requêtes de mutation (POST, PUT, PATCH, DELETE, y compris anonymes) → binding sans cache.
  • Toute requête sous /_emdash (admin, configuration, auth, APIs internes), même un GET anonyme → binding sans cache.
  • Migrations runtime et démarrage à froid → toujours le binding primaire.
  • Migrations gérées par déploiement → se connectent directement à l’origin PostgreSQL via migrationConnectionStringEnv ; elles n’utilisent jamais aucun binding Hyperdrive.

Optionnel : utiliser un rôle caché séparé

Les migrations, la configuration, les requêtes authentifiées et les requêtes d’écriture explicites utilisent toujours le binding primaire. Un rôle séparé pour cachedBinding n’a pas besoin de la propriété du schéma ni de CREATE, mais a besoin de CONNECT, USAGE du schéma et SELECT sur chaque table utilisée par le site public.

Les requêtes publiques anonymes GET et HEAD peuvent aussi enregistrer les hits de redirection et les 404. Pour préserver ces fonctionnalités, le rôle caché a en plus besoin de UPDATE sur _emdash_redirects et SELECT, INSERT, UPDATE et DELETE sur _emdash_404_log. Les plugins ou le code applicatif qui écrivent pendant un GET ou HEAD public peuvent nécessiter plus. Utilisez le même rôle pour les deux bindings sauf si vous avez testé le site avec un rôle caché restreint.

Ajoutez le rôle caché après qu’EmDash a terminé ses migrations initiales. Les exemples ci-dessous utilisent le schéma emdash optionnel ; substituez votre schéma actif, comme public. Créez le login et les paramètres de base de données avec le rôle administratif de votre fournisseur :

CREATE ROLE emdash_cached LOGIN PASSWORD 'replace-with-a-secret';
GRANT CONNECT ON DATABASE app TO emdash_cached;
ALTER ROLE emdash_cached IN DATABASE app SET search_path = emdash;

Puis connectez-vous en tant qu’emdash_app, le propriétaire du schéma et des tables, pour accorder l’accès aux tables existantes et futures :

GRANT USAGE ON SCHEMA emdash TO emdash_cached;
GRANT SELECT ON ALL TABLES IN SCHEMA emdash TO emdash_cached;
GRANT UPDATE ON emdash._emdash_redirects TO emdash_cached;
GRANT SELECT, INSERT, UPDATE, DELETE ON emdash._emdash_404_log TO emdash_cached;

ALTER DEFAULT PRIVILEGES IN SCHEMA emdash
  GRANT SELECT ON TABLES TO emdash_cached;

Connectez-vous avec les deux rôles et vérifiez qu’ils rapportent les mêmes current_database() et current_schema() avant d’activer cachedBinding. Sur un schéma partagé, GRANT SELECT ON ALL TABLES expose aussi les tables non liées. Accordez plutôt l’accès aux tables EmDash individuelles, et mettez à jour ces grants quand des collections ou d’autres objets de schéma sont ajoutés.

Réparer la propriété mixte PostgreSQL

Si un site a utilisé plusieurs utilisateurs PostgreSQL, choisissez d’abord le rôle canonique que la connexion principale EmDash continuera d’utiliser. Faites une sauvegarde et arrêtez les changements de schéma pendant la réparation de la propriété.

Inspectez chaque table dans le schéma actif :

SELECT
  n.nspname AS schema_name,
  c.relname AS table_name,
  pg_get_userbyid(c.relowner) AS owner
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
  AND c.relkind IN ('r', 'p')
ORDER BY c.relname;

Les objets EmDash incluent les tables système _emdash_* et _plugin_*, les tables de collection ec_* et les tables sans préfixe comme content_taxonomies, media, options, revisions et taxonomies. Dans un schéma EmDash dédié, chaque table d’application devrait avoir le propriétaire canonique.

EmDash crée aussi des fonctions PostgreSQL utilisées par les triggers d’utilisation des médias. Inspectez la propriété des fonctions et conservez la signature d’arguments de chaque fonction pour la commande de réparation :

SELECT
  n.nspname AS schema_name,
  p.proname AS function_name,
  pg_get_function_identity_arguments(p.oid) AS arguments,
  pg_get_userbyid(p.proowner) AS owner
FROM pg_proc AS p
JOIN pg_namespace AS n ON n.oid = p.pronamespace
WHERE n.nspname = current_schema()
ORDER BY p.proname, arguments;

Transférez chaque objet non correspondant avec un superutilisateur ou un rôle fournisseur qui peut changer sa propriété, en utilisant toujours des noms qualifiés par schéma :

ALTER TABLE emdash.content_taxonomies OWNER TO emdash_app;
ALTER TABLE emdash.ec_posts OWNER TO emdash_app;
ALTER FUNCTION emdash.emdash_media_usage_capture_work() OWNER TO emdash_app;

Utilisez la liste d’arguments retournée par la requête d’inventaire dans chaque instruction ALTER FUNCTION. Changer le propriétaire d’une table couvre aussi ses index, contraintes et triggers attachés, mais pas leurs fonctions trigger indépendantes. Répétez les deux requêtes d’inventaire jusqu’à ce que chaque table et fonction EmDash rapporte le propriétaire canonique, puis connectez-vous avec ce rôle et vérifiez current_schema() avant de démarrer l’application.

Pour qu’un non-superutilisateur transfère la propriété, il doit posséder ou hériter de la propriété de l’objet, pouvoir exécuter SET ROLE vers le nouveau propriétaire, et le nouveau propriétaire doit avoir CREATE sur le schéma. Les fournisseurs PostgreSQL gérés peuvent exiger leur rôle administratif pour effectuer le transfert.

SQLite

SQLite utilise le driver de base de données intégré de Node.js et est l’option la plus simple pour les déploiements Node.js.

import { sqlite } from "emdash/db";

export default defineConfig({
	integrations: [
		emdash({
			database: sqlite({ url: "file:./data.db" }),
		}),
	],
});

Configuration

OptionTypeDescription
urlstringChemin de fichier avec préfixe file:

Chemin de fichier

L’url doit commencer par file: :

// Chemin relatif
database: sqlite({ url: "file:./data/emdash.db" });

// Chemin absolu
database: sqlite({ url: "file:/var/data/emdash.db" });

// Depuis une variable d'environnement
database: sqlite({ url: `file:${process.env.DATABASE_PATH}` });

Migrations

EmDash exécute les migrations core automatiquement par défaut pour chaque dialecte pris en charge. Astro build et sync émettent aussi un .emdash/migrations.json validé et sans secrets, que emdash migrate peut appliquer avant le déploiement. SQLite, libSQL, PostgreSQL, D1 et l’origin PostgreSQL direct derrière Hyperdrive ont des exécuteurs de déploiement.

Voir Gérer les migrations de la base de données core pour les identifiants cible, la sérialisation CI, la politique runtime auto/check/manual et la récupération d’enregistrements inconnus ou d’écritures D1 ambiguës.

Pour PostgreSQL, les migrations runtime s’exécutent via la connexion configurée ; les migrations runtime Hyperdrive utilisent toujours son binding primaire. Les migrations Hyperdrive gérées par déploiement se connectent directement à l’origin PostgreSQL. Les migrations core peuvent créer des tables, des index et des fonctions, modifier ou supprimer des colonnes et des contraintes, et mettre à jour des lignes existantes. Un rôle qui peut se connecter et modifier des lignes mais ne possède pas les objets EmDash existants n’est pas suffisant. L’assistant de configuration ne peut pas réparer les privilèges de base de données manquants car les migrations runtime s’exécutent avant la configuration.

Si la base de données est vide (pas de collections) et que l’assistant de configuration n’a pas été complété, EmDash applique aussi un fichier seed au premier démarrage. Le seed est lu depuis .emdash/seed.json, le chemin dans package.json#emdash.seed, ou seed/seed.json — ce qui est trouvé en premier — et intégré dans le build au moment de la compilation. Si aucun n’est présent, un seed par défaut intégré est utilisé. Les démarrages suivants contre une base de données existante laissent son contenu intact.

Configuration basée sur l’environnement

Utilisez différentes bases de données par environnement :

import { sqlite, libsql, postgres } from "emdash/db";
import { d1 } from "@emdash-cms/cloudflare";

const database = import.meta.env.PROD ? d1({ binding: "DB" }) : sqlite({ url: "file:./data.db" });

export default defineConfig({
	integrations: [emdash({ database })],
});

Le choix peut aussi dépendre d’une variable d’environnement plutôt que du mode de build :

const database = process.env.DATABASE_URL
	? postgres({ connectionString: process.env.DATABASE_URL })
	: sqlite({ url: "file:./data.db" });