Elige un adaptador de base de datos para cada despliegue. La base de datos contiene el modelo de contenido, las entradas, los usuarios, los ajustes y los datos de los plugins. Los binarios de medios van en un backend de almacenamiento aparte.
Resumen
| Base de datos | Úsala cuando | Entorno de ejecución |
|---|---|---|
| SQLite | Un proceso de Node.js tiene un disco persistente | Node.js o desarrollo local |
| D1 | El sitio se ejecuta en Cloudflare Workers y debe usar Cloudflare SQL | Cloudflare Workers |
| Hyperdrive | El sitio se ejecuta en Workers y debe usar un origen PostgreSQL existente | Cloudflare Workers |
| PostgreSQL | Varios procesos de Node.js necesitan una base de datos compartida | Node.js |
| libSQL | Un despliegue de Node.js necesita una base de datos remota compatible con SQLite | Node.js |
D1 es la opción predeterminada de las plantillas de Cloudflare. SQLite es la opción más sencilla para Node.js, pero requiere un volumen persistente con permiso de escritura y copias de seguridad operativas de la base de datos.
SQLite
SQLite usa el controlador de base de datos integrado de Node.js y es la opción más sencilla para los despliegues de Node.js.
import { sqlite } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
}),
],
});
Configuración
| Opción | Tipo | Descripción |
|---|---|---|
url | string | Ruta de archivo con el prefijo file: |
Ruta de archivo
La url debe empezar por file::
// Relative path
database: sqlite({ url: "file:./data/emdash.db" });
// Absolute path
database: sqlite({ url: "file:/var/data/emdash.db" });
// From environment variable
database: sqlite({ url: `file:${process.env.DATABASE_PATH}` });
Write-ahead logging
EmDash abre las bases de datos SQLite en modo write-ahead logging (WAL). Mientras el sitio se ejecuta, SQLite mantiene dos archivos adicionales junto a la base de datos, como emdash.db-wal y emdash.db-shm. El proceso necesita acceso de escritura al directorio de la base de datos para crearlos.
El archivo -wal puede contener cambios confirmados que todavía no están en el archivo principal de la base de datos. Haz la copia de seguridad con el comando de copia de seguridad de SQLite en lugar de copiar solo el archivo .db. Consulta Copia de seguridad y recuperación de SQLite.
WAL requiere memoria compartida, así que mantén la base de datos en un disco local o en un volumen de bloques, y no en un sistema de archivos de red como NFS o SMB.
Cloudflare D1
D1 es la base de datos SQLite sin servidor de Cloudflare. Úsala al desplegar en Cloudflare Workers.
import { d1 } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
database: d1({ binding: "DB" }),
}),
],
});
Configuración
| Opción | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
binding | string | — | Nombre del binding de D1 en wrangler.jsonc |
session | string | "disabled" | Modo de replicación de lectura (ver más abajo) |
bookmarkCookie | string | "__em_d1_bookmark" | Nombre de la cookie de los marcadores de sesión |
Binding de Wrangler
wrangler.jsonc
{
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db"
}
]
} wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "emdash-db" Wrangler puede aprovisionar una base de datos D1 que falte a partir de este binding durante el despliegue. Las migraciones de EmDash son un paso aparte. Sigue Desplegar en Cloudflare para el conjunto completo de bindings y Gestionar las migraciones de base de datos del núcleo para el runbook de migraciones.
Réplicas de lectura
D1 admite la replicación de lectura para reducir la latencia de lectura en sitios distribuidos globalmente. Cuando está habilitada, las consultas de lectura se enrutan a réplicas cercanas en lugar de ir siempre a la base de datos principal.
EmDash usa la API D1 Sessions para gestionarlo de forma transparente. Habilítalo con la opción session:
import { d1 } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
database: d1({
binding: "DB",
session: "auto",
}),
}),
],
});
Modos de sesión
| Modo | Comportamiento |
|---|---|
"disabled" | Sin sesiones. Todas las consultas van a la base de datos principal. Valor predeterminado. |
"auto" | Las solicitudes anónimas leen de la réplica más cercana. Los usuarios autenticados obtienen consistencia read-your-writes mediante cookies de marcador. |
"primary-first" | Como "auto", pero la primera consulta siempre va a la base de datos principal. Úsalo en sitios con escrituras muy frecuentes. |
Cómo funciona
- Los visitantes anónimos obtienen
first-unconstrained— las lecturas van a la réplica más cercana para obtener la latencia más baja. Como los usuarios anónimos nunca escriben, no necesitan garantías de consistencia. - Los usuarios autenticados (editores, autores) obtienen sesiones basadas en marcadores. Después de una escritura, una cookie de marcador garantiza que la siguiente solicitud vea al menos ese estado.
- Las solicitudes de escritura (
POST,PUT,DELETE) siempre empiezan en la base de datos principal. - Las consultas en tiempo de compilación (colecciones de contenido de Astro) omiten por completo las sesiones y usan directamente la base de datos principal.
libSQL
libSQL es una bifurcación de SQLite que admite conexiones remotas. Úsala cuando necesites una base de datos remota sin 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,
}),
}),
],
});
Configuración
| Opción | Tipo | Descripción |
|---|---|---|
url | string | URL de la base de datos (libsql://... o file:...) |
authToken | string | Token de autenticación en tiempo de ejecución para bases de datos remotas (opcional en local) |
migrationAuthTokenEnv | string | Nombre de la variable del token de migración (valor predeterminado TURSO_AUTH_TOKEN) |
Desarrollo local
Usa un archivo libSQL local durante el desarrollo:
database: libsql({ url: "file:./data.db" });
PostgreSQL
PostgreSQL es compatible con los despliegues de Node.js que necesitan una base de datos relacional completa.
import { postgres } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: postgres({
connectionString: process.env.DATABASE_URL,
}),
}),
],
});
Configuración
Puedes conectarte con una cadena de conexión o con parámetros individuales:
// Connection string
database: postgres({
connectionString: "postgres://user:password@localhost:5432/emdash",
});
// Individual parameters
database: postgres({
host: "localhost",
port: 5432,
database: "emdash",
user: "emdash",
password: process.env.DB_PASSWORD,
ssl: true,
});
| Opción | Tipo | Descripción |
|---|---|---|
connectionString | string | URL de conexión de PostgreSQL |
host | string | Host de la base de datos |
port | number | Puerto de la base de datos |
database | string | Nombre de la base de datos |
user | string | Usuario de la base de datos |
password | string | Contraseña de la base de datos |
ssl | boolean | Habilitar SSL |
pool.min | number | Conexiones mínimas del pool (valor predeterminado 0) |
pool.max | number | Conexiones máximas del pool (valor predeterminado 10) |
pool.connectionTimeoutMillis | number | Espera máxima de conexión (valor predeterminado de pg: 0, sin tiempo de espera) |
pool.idleTimeoutMillis | number | Duración de los clientes inactivos (valor predeterminado de pg: 10 000 ms) |
migrationConnectionStringEnv | string | Nombre de la variable de la cadena de conexión de migración (valor predeterminado DATABASE_URL) |
Establece pool.connectionTimeoutMillis en un valor distinto de cero para limitar cuánto espera una solicitud cuando PostgreSQL es inalcanzable o no queda disponible ninguna conexión del pool. Establece pool.idleTimeoutMillis en 0 para mantener abiertos los clientes inactivos hasta que se cierre el pool. Si omites cualquiera de las dos opciones, se conserva el valor predeterminado de pg.
Requisitos del rol de base de datos
EmDash crea y actualiza sus propias tablas de PostgreSQL. Las migraciones del núcleo crean y modifican las tablas del sistema y de las colecciones, los tipos de contenido crean tablas ec_*, y añadir o eliminar un campo modifica la tabla de su colección. Por tanto, el rol de PostgreSQL configurado necesita autoridad sobre el esquema durante toda la vida del sitio, no solo durante la configuración inicial.
Usa un único rol canónico para EmDash. Necesita:
CONNECTsobre la base de datos;USAGEyCREATEsobre el esquema activo;- la propiedad de cada tabla y función de EmDash, ya sea directamente o mediante la pertenencia con
INHERITal rol propietario; y SELECT,INSERT,UPDATEyDELETEsobre esas tablas.
No necesita ser superusuario, tener CREATEDB ni CREATEROLE, ni crear extensiones. PostgreSQL no ofrece un permiso ALTER o DROP de tabla: esas operaciones pertenecen al propietario del objeto y a los roles que heredan sus privilegios. Conceder ALL sobre una tabla a otro rol no convierte a ese rol en propietario. EmDash no ejecuta SET ROLE, así que una pertenencia configurada sin herencia no es suficiente.
La mayoría de las instalaciones pueden usar el esquema existente de la base de datos, normalmente public. Es la opción más sencilla cuando la base de datos está dedicada a EmDash. En los ejemplos siguientes, emdash_app es el rol de inicio de sesión de la cadena de conexión de EmDash; usa un rol existente de tu proveedor o crea un inicio de sesión dedicado. Concédele acceso con una conexión administrativa, sustituyendo los nombres de tu base de datos, esquema y rol:
GRANT CONNECT ON DATABASE app TO emdash_app;
GRANT USAGE, CREATE ON SCHEMA public TO emdash_app;
Estas concesiones permiten que el rol cree objetos nuevos. No cambian el propietario de las tablas existentes; usa el runbook de reparación de la propiedad de PostgreSQL cuando un sitio existente tenga propietarios mixtos.
EmDash usa el current_schema() activo de PostgreSQL. No crea un esquema ni establece search_path, así que verifica la conexión antes del despliegue:
SELECT
current_database(),
session_user,
current_user,
current_schema(),
current_setting('search_path');
Opcional: usar un esquema dedicado
Usa un esquema dedicado cuando EmDash comparta base de datos con otra aplicación o cuando quieras que sus objetos estén aislados de public. Es opcional y lo más fácil es configurarlo antes de la primera configuración de EmDash. Una base de datos dedicada a EmDash no necesita un esquema aparte.
Suponiendo que el rol canónico emdash_app ya existe, crea y selecciona su esquema con una conexión administrativa:
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;
Esto no traslada una instalación existente desde public ni repara una propiedad mixta. Los sitios existentes deben conservar su esquema actual y usar en su lugar el runbook de reparación de la propiedad de PostgreSQL.
Agrupación de conexiones
El adaptador usa pg.Pool. Ajusta el tamaño del pool según tu despliegue:
database: postgres({
connectionString: process.env.DATABASE_URL,
pool: { min: 2, max: 20 },
});
Hyperdrive
Usa el adaptador hyperdrive() para ejecutar EmDash en Cloudflare Workers con una base de datos PostgreSQL existente, o compatible con Postgres (por ejemplo, PlanetScale Postgres). Hyperdrive agrupa y acelera la conexión a través de la red de Cloudflare; el dialecto PostgreSQL de EmDash ejecuta las consultas.
import { hyperdrive, r2 } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
database: hyperdrive({ binding: "HYPERDRIVE" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});
Requisitos
pg >= 8.16.3instalado en tu sitio (pnpm add pg)compatibility_flags: ["nodejs_compat"]compatibility_date >= "2024-09-23"
Configuración
Primero prepara el rol de PostgreSQL. Después crea la configuración de Hyperdrive con la cadena de conexión de ese rol y añade el binding a tu configuración de 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>" Configuración
| Opción | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
binding | string | "HYPERDRIVE" | Nombre del binding principal de Hyperdrive (con la caché desactivada) |
cachedBinding | string | — | Binding opcional con la caché activada para lecturas anónimas (ver más abajo) |
preferUncachedAfterWriteMs | number | 60000* | Tras publicar contenido, prefiere binding durante estos ms en las lecturas públicas anónimas (ajústalo al max_age de Hyperdrive) |
migrationConnectionStringEnv | string | CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING> | Variable de entorno que contiene la URL directa del origen PostgreSQL para emdash migrate |
max | number | 5 | Tamaño máximo del pool de conexiones a Hyperdrive dentro del Worker |
*El valor predeterminado 60000 solo se aplica cuando cachedBinding está definido; en caso contrario se ignora.
Servir lecturas anónimas desde la caché
De forma predeterminada se desactiva por completo la caché de Hyperdrive, porque el administrador y las escrituras necesitan consistencia de lectura tras escritura. Pero las solicitudes públicas anónimas con GET o HEAD pueden tolerar una breve ventana de datos obsoletos. Si esa contrapartida es aceptable, ejecuta dos configuraciones de Hyperdrive sobre la misma base de datos: una con la caché desactivada (el binding principal) y otra con la caché activada (cachedBinding). EmDash enruta esas solicitudes públicas anónimas por el binding con la caché activada y todas las demás por el binding principal sin caché.
# Primary — caching OFF (used by admin, auth'd requests, writes, migrations)
wrangler hyperdrive create emdash-db \
--connection-string "postgres://user:password@host/db?sslmode=verify-full" \
--caching-disabled
# Cached — SAME database role and connection string, caching ON
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" });
Este es el patrón de dos configuraciones que Cloudflare documenta para el almacenamiento en caché. EmDash decide qué binding usar en cada solicitud:
- Lecturas anónimas de rutas del sitio público (
GET/HEAD, sin sesión, fuera de/_emdash) →cachedBindingcon la caché activada, excepto durante una breve ventana tras publicar contenido (60 s de forma predeterminada; ajustapreferUncachedAfterWriteMsalmax_agede tu Hyperdrive), en la que EmDash prefiere elbindingsin caché para que una reconstrucción no pueda volver a sembrar las cachés de borde y de objetos con resultados de Hyperdrive todavía obsoletos. - Solicitudes autenticadas (editores, autores) →
bindingsin caché. - Solicitudes de mutación (
POST,PUT,PATCH,DELETE, incluidas las anónimas) →bindingsin caché. - Cualquier solicitud bajo
/_emdash(administrador, configuración, autenticación, API internas), incluso unGETanónimo →bindingsin caché. - Migraciones en tiempo de ejecución y arranque en frío → siempre el
bindingprincipal. - Migraciones gestionadas por el despliegue → se conectan directamente al origen PostgreSQL mediante
migrationConnectionStringEnv; nunca usan ninguno de los dos bindings de Hyperdrive.
Opcional: usar un rol en caché separado
Las migraciones, la configuración, las solicitudes autenticadas y las solicitudes de escritura explícitas siempre usan el binding principal. Un rol separado para cachedBinding no necesita propiedad del esquema ni CREATE, pero sí CONNECT, USAGE sobre el esquema y SELECT sobre cada tabla que use el sitio público.
Las solicitudes públicas anónimas GET y HEAD también pueden registrar aciertos de redirección y errores 404. Para conservar esas funciones, el rol en caché necesita además UPDATE sobre _emdash_redirects y SELECT, INSERT, UPDATE y DELETE sobre _emdash_404_log. Los plugins o el código de la aplicación que escriban durante un GET o HEAD público pueden requerir más. Usa el mismo rol para ambos bindings a menos que hayas probado el sitio con un rol en caché restringido.
Añade el rol en caché después de que EmDash haya completado sus migraciones iniciales. Los ejemplos siguientes usan el esquema opcional emdash; sustitúyelo por tu esquema activo, como public. Crea el inicio de sesión y los ajustes de la base de datos con el rol administrativo de tu proveedor:
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;
Después conéctate como emdash_app, el propietario del esquema y de las tablas, para conceder acceso a las tablas existentes y futuras:
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;
Conéctate con ambos roles y verifica que informan el mismo current_database() y current_schema() antes de habilitar cachedBinding. En un esquema compartido, GRANT SELECT ON ALL TABLES también expone tablas no relacionadas. Concede en su lugar acceso a cada tabla de EmDash por separado y actualiza esas concesiones cuando se añadan colecciones u otros objetos del esquema.
Migraciones principales
EmDash ejecuta las migraciones principales automáticamente de forma predeterminada para todos los dialectos compatibles. La compilación y la sincronización de Astro también generan un .emdash/migrations.json validado y sin secretos, que emdash migrate puede aplicar antes del despliegue. SQLite, libSQL, PostgreSQL, D1 y el origen PostgreSQL directo detrás de Hyperdrive tienen ejecutores de despliegue.
Consulta Gestionar las migraciones de base de datos del núcleo para las credenciales del destino, la serialización en CI, la política de ejecución auto/check/manual y la recuperación ante registros desconocidos o escrituras ambiguas de D1.
En PostgreSQL, las migraciones en tiempo de ejecución se ejecutan a través de la conexión configurada; las migraciones en tiempo de ejecución de Hyperdrive siempre usan su binding principal. Las migraciones de Hyperdrive gestionadas por el despliegue se conectan directamente al origen PostgreSQL. Las migraciones principales pueden crear tablas, índices y funciones, modificar o eliminar columnas y restricciones, y actualizar filas existentes. Un rol que puede conectarse y modificar filas pero no es propietario de los objetos existentes de EmDash no es suficiente. El asistente de configuración no puede reparar los privilegios de base de datos que falten, porque las migraciones en tiempo de ejecución se ejecutan antes de la configuración.
Hasta que se haya completado el asistente de configuración, EmDash también aplica una sola vez, en la primera solicitud, el esquema y la estructura de un archivo seed. El contenido de ejemplo del seed solo se aplica cuando un administrador lo elige en el asistente de configuración. El seed se lee de .emdash/seed.json, de la ruta indicada en package.json#emdash.seed o de seed/seed.json — el que se encuentre primero — y se incorpora a la compilación en tiempo de compilación. Si no hay ninguno, se usa un seed predeterminado integrado. Los arranques posteriores contra una base de datos existente no tocan su contenido.
Usar bases de datos separadas para entornos separados
Da a desarrollo, preview, staging y producción su propia base de datos. Un despliegue de preview que apunte a producción puede ejecutar migraciones principales o comandos destructivos del modelo de contenido sobre datos en producción.
En Cloudflare, define cada binding de D1 o de Hyperdrive bajo el entorno de Wrangler correspondiente y pasa --env a los comandos de Wrangler. En Node.js, inyecta una URL de base de datos distinta en cada entorno de ejecución. Guarda las credenciales en secretos del entorno de ejecución, no en astro.config.mjs.