EmDash soporta múltiples backends de base de datos. Elige según tu objetivo de despliegue.
Resumen
| Base de datos | Mejor para | Despliegue |
|---|---|---|
| D1 | Cloudflare Workers | Edge, distribuido globalmente |
| Hyperdrive | PostgreSQL en Cloudflare Workers | Edge, Postgres existente |
| PostgreSQL | Producción Node.js | Cualquier plataforma con Postgres |
| libSQL | Bases de datos remotas | Edge o Node.js |
| SQLite | Node.js, desarrollo local | Servidor único |
Cloudflare D1
D1 es la base de datos SQLite serverless 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 | Predeterminado | Descripción |
|---|---|---|---|
binding | string | — | Nombre del binding D1 de wrangler.jsonc |
session | string | "disabled" | Modo de replicación de lectura (ver abajo) |
bookmarkCookie | string | "__em_d1_bookmark" | Nombre de cookie para bookmarks de sesión |
Configuración
wrangler.jsonc
{
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db"
}
]
} wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "emdash-db" Réplicas de lectura
D1 soporta replicación de lectura para reducir la latencia de lectura para sitios distribuidos globalmente. Cuando está habilitada, las consultas de lectura se enrutan a réplicas cercanas en lugar de siempre acceder a la base de datos primaria.
EmDash usa la API de Sesiones D1 para gestionar esto 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 primaria. 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 bookmark. |
"primary-first" | Como "auto", pero la primera consulta siempre va a la primaria. Para sitios con escrituras muy frecuentes. |
Cómo funciona
- Visitantes anónimos obtienen
first-unconstrained— las lecturas van a la réplica más cercana para la menor latencia. Como los usuarios anónimos nunca escriben, no necesitan garantías de consistencia. - Usuarios autenticados (editores, autores) obtienen sesiones basadas en bookmarks. Después de una escritura, una cookie de bookmark asegura que la siguiente solicitud vea al menos ese estado.
- Solicitudes de escritura (
POST,PUT,DELETE) siempre comienzan en la base de datos primaria. - Consultas en tiempo de build (Astro content collections) omiten las sesiones por completo y usan la primaria directamente.
libSQL
libSQL es un fork de SQLite que soporta conexiones remotas. Úsalo 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 para bases de datos remotas (opcional para local) |
migrationAuthTokenEnv | string | Nombre de variable del token de migración (predeterminado TURSO_AUTH_TOKEN) |
Desarrollo local
Usa un archivo libSQL local durante el desarrollo:
database: libsql({ url: "file:./data.db" });
PostgreSQL
PostgreSQL es soportado para despliegues 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 conectar con un string de conexión o parámetros individuales:
// String de conexión
database: postgres({
connectionString: "postgres://user:password@localhost:5432/emdash",
});
// Parámetros individuales
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 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 (predeterminado 0) |
pool.max | number | Conexiones máximas del pool (predeterminado 10) |
pool.connectionTimeoutMillis | number | Espera máxima de conexión (predeterminado pg: 0, sin timeout) |
pool.idleTimeoutMillis | number | Vida útil del cliente inactivo (predeterminado pg: 10.000 ms) |
migrationConnectionStringEnv | string | Nombre de variable del string de conexión de migración (predeterminado DATABASE_URL) |
Establece pool.connectionTimeoutMillis a un valor distinto de cero para limitar cuánto tiempo espera una solicitud cuando PostgreSQL no es accesible o no hay una conexión del pool disponible. Establece pool.idleTimeoutMillis a 0 para mantener los clientes inactivos abiertos hasta que el pool se cierre. Omitir cualquiera de las opciones preserva el predeterminado de pg.
Requisitos del rol de base de datos
EmDash crea y actualiza sus propias tablas PostgreSQL. Las migraciones core crean y alteran tablas del sistema y de colecciones, los tipos de contenido crean tablas ec_*, y agregar o eliminar un campo altera su tabla de colección. El rol PostgreSQL configurado necesita por tanto autoridad sobre el esquema durante toda la vida del sitio, no solo durante la configuración inicial.
Usa un rol canónico para EmDash. Necesita:
CONNECTen la base de datos;USAGEyCREATEen el esquema activo;- propiedad de cada tabla y función de EmDash, ya sea directamente o a través de membresía con
INHERITen el rol propietario; y SELECT,INSERT,UPDATEyDELETEen esas tablas.
No necesita ser superusuario, tener CREATEDB o CREATEROLE, ni crear extensiones. PostgreSQL no proporciona un grant de ALTER o DROP tabla: esas operaciones pertenecen al propietario del objeto y a los roles que heredan sus privilegios. Otorgar ALL en una tabla a un rol diferente no convierte a ese rol en propietario. EmDash no ejecuta SET ROLE, por lo que una membresía configurada sin herencia no es suficiente.
La mayoría de las instalaciones pueden usar el esquema existente de la base de datos, comúnmente public. Esta es la opción más simple cuando la base de datos está dedicada a EmDash. En los ejemplos a continuación, emdash_app es el rol de login en el string de conexión de EmDash; usa un rol de proveedor existente o crea un login dedicado. Otorga acceso con una conexión administrativa, sustituyendo tus nombres de base de datos, esquema y rol:
GRANT CONNECT ON DATABASE app TO emdash_app;
GRANT USAGE, CREATE ON SCHEMA public TO emdash_app;
Estos grants permiten al rol crear nuevos objetos. No cambian el propietario de las tablas existentes; consulta Reparar propiedad mixta de PostgreSQL.
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 comparte una base de datos con otra aplicación o cuando quieres que sus objetos estén aislados de public. Esto es opcional y es más fácil de configurar antes de la primera configuración de EmDash. Una base de datos dedicada a EmDash no necesita un esquema separado.
Asumiendo 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 mueve una instalación existente desde public ni repara la propiedad mixta. Los sitios existentes deben mantener su esquema actual y seguir Reparar propiedad mixta de PostgreSQL en su lugar.
Pool de conexiones
El adaptador usa pg.Pool internamente. 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 respaldado por 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. Luego crea la configuración de Hyperdrive con el string de conexión de ese rol y agrega 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 | Predeterminado | Descripción |
|---|---|---|---|
binding | string | "HYPERDRIVE" | Nombre del binding primario (con caché deshabilitado) de Hyperdrive |
cachedBinding | string | — | Binding opcional con caché habilitado para lecturas anónimas (ver abajo) |
preferUncachedAfterWriteMs | number | 60000* | Después de publicar contenido, preferir binding durante estos ms en lecturas públicas anónimas (coincidir con Hyperdrive max_age) |
migrationConnectionStringEnv | string | CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING> | Variable de entorno que contiene la URL directa de PostgreSQL origin para emdash migrate |
max | number | 5 | Tamaño máximo del pool de conexiones en el Worker a Hyperdrive |
*El predeterminado 60000 solo aplica cuando cachedBinding está establecido; ignorado de lo contrario.
Servir lecturas anónimas desde el caché
Por defecto deshabilitas el caché de Hyperdrive completamente, porque el admin y las escrituras necesitan consistencia read-after-write. Pero las solicitudes públicas anónimas usando GET o HEAD pueden tolerar una ventana corta de desactualización. Si ese compromiso es aceptable, ejecuta dos configuraciones de Hyperdrive sobre la misma base de datos: una con caché desactivado (el binding primario) y una con caché activado (cachedBinding). EmDash enruta esas solicitudes públicas anónimas a través del binding con caché habilitado y todas las demás a través del primario sin caché.
# Primario — caché DESACTIVADO (usado por admin, solicitudes autenticadas, escrituras, migraciones)
wrangler hyperdrive create emdash-db \
--connection-string "postgres://user:password@host/db?sslmode=verify-full" \
--caching-disabled
# Con caché — MISMO rol de base de datos y string de conexión, caché ACTIVADO
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 caché. EmDash decide qué binding usar por solicitud:
- Lecturas anónimas de rutas del sitio público (
GET/HEAD, sin sesión, no bajo/_emdash) →cachedBindingcon caché habilitado, excepto por una ventana corta después de publicar contenido (predeterminado 60s; establecepreferUncachedAfterWriteMsa tu Hyperdrivemax_age) cuando EmDash prefiere elbindingsin caché para que un rebuild no pueda re-alimentar cachés edge/objetos desde resultados de Hyperdrive aún desactualizados. - Solicitudes autenticadas (editores, autores) →
bindingsin caché. - Solicitudes de mutación (
POST,PUT,PATCH,DELETE, incluidas las anónimas) →bindingsin caché. - Cualquier solicitud bajo
/_emdash(admin, configuración, auth, APIs internas), incluso unGETanónimo →bindingsin caché. - Migraciones en runtime y arranque en frío → siempre el
bindingprimario. - Migraciones gestionadas por despliegue → se conectan directamente al origin de PostgreSQL usando
migrationConnectionStringEnv; nunca usan ningún binding de Hyperdrive.
Opcional: usar un rol con caché separado
Las migraciones, configuración, solicitudes autenticadas y solicitudes de escritura explícitas siempre usan el binding primario. Un rol separado para cachedBinding no necesita propiedad del esquema ni CREATE, pero necesita CONNECT, USAGE del esquema y SELECT en cada tabla usada por el sitio público.
Las solicitudes públicas anónimas GET y HEAD también pueden registrar hits de redirección y 404s. Para preservar esas funciones, el rol con caché necesita adicionalmente UPDATE en _emdash_redirects y SELECT, INSERT, UPDATE y DELETE en _emdash_404_log. Los plugins o código de aplicación que escriben 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 con caché restringido.
Agrega el rol con caché después de que EmDash haya completado sus migraciones iniciales. Los ejemplos a continuación usan el esquema emdash opcional; sustituye tu esquema activo, como public. Crea el login y la configuración 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;
Luego conéctate como emdash_app, el propietario del esquema y las tablas, para otorgar acceso a 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 reporten 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. Otorga acceso a tablas individuales de EmDash en su lugar, y actualiza esos grants cuando se agreguen colecciones u otros objetos del esquema.
Reparar propiedad mixta de PostgreSQL
Si un sitio ha usado múltiples usuarios de PostgreSQL, primero elige el rol canónico que la conexión principal de EmDash continuará usando. Haz un respaldo y detén los cambios de esquema mientras reparas la propiedad.
Inspecciona cada tabla en el esquema activo:
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;
Los objetos de EmDash incluyen tablas del sistema _emdash_* y _plugin_*, tablas de colección ec_* y tablas sin prefijo como content_taxonomies, media, options, revisions y taxonomies. En un esquema dedicado de EmDash, cada tabla de aplicación debe tener el propietario canónico.
EmDash también crea funciones PostgreSQL usadas por triggers de uso de medios. Inspecciona la propiedad de las funciones y retén la firma de argumentos de cada función para el comando de reparación:
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;
Transfiere cada objeto que no coincida con un superusuario o rol de proveedor que pueda cambiar su propiedad, siempre usando nombres cualificados por esquema:
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;
Usa la lista de argumentos devuelta por la consulta de inventario en cada sentencia ALTER FUNCTION. Cambiar el propietario de una tabla también cubre sus índices, restricciones y triggers adjuntos, pero no sus funciones de trigger independientes. Repite ambas consultas de inventario hasta que cada tabla y función de EmDash reporte el propietario canónico, luego conéctate como ese rol y verifica current_schema() antes de iniciar la aplicación.
Para que un no-superusuario transfiera propiedad, debe ser propietario o heredar la propiedad del objeto, poder ejecutar SET ROLE al nuevo propietario, y el nuevo propietario debe tener CREATE en el esquema. Los proveedores PostgreSQL gestionados pueden requerir su rol administrativo para realizar la transferencia.
SQLite
SQLite usa el driver de base de datos integrado de Node.js y es la opción más simple para despliegues 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 prefijo file: |
Ruta de archivo
La url debe comenzar con file::
// Ruta relativa
database: sqlite({ url: "file:./data/emdash.db" });
// Ruta absoluta
database: sqlite({ url: "file:/var/data/emdash.db" });
// Desde variable de entorno
database: sqlite({ url: `file:${process.env.DATABASE_PATH}` });
Migraciones
EmDash ejecuta migraciones core automáticamente por defecto para cada dialecto soportado. Astro build y sync también emiten un .emdash/migrations.json validado y sin secretos, que emdash migrate puede aplicar antes del despliegue. SQLite, libSQL, PostgreSQL, D1 y el origin directo de PostgreSQL detrás de Hyperdrive tienen ejecutores de despliegue.
Consulta Gestionar migraciones de la base de datos core para credenciales de destino, serialización CI, política runtime auto/check/manual y recuperación de registros desconocidos o escrituras D1 ambiguas.
Para PostgreSQL, las migraciones en runtime se ejecutan a través de la conexión configurada; las migraciones en runtime de Hyperdrive siempre usan su binding primario. Las migraciones de Hyperdrive gestionadas por despliegue se conectan directamente al origin de PostgreSQL. Las migraciones core pueden crear tablas, índices y funciones, alterar 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 privilegios de base de datos faltantes porque las migraciones en runtime se ejecutan antes de la configuración.
Si la base de datos está vacía (sin colecciones) y el asistente de configuración no se ha completado, EmDash también aplica un archivo seed en el primer arranque. El seed se lee de .emdash/seed.json, la ruta en package.json#emdash.seed, o seed/seed.json — lo que se encuentre primero — y se incorpora en el build en tiempo de compilación. Si no hay ninguno presente, se usa un seed predeterminado integrado. Arranques posteriores contra una base de datos existente dejan su contenido intacto.
Configuración basada en entorno
Usa diferentes bases de datos por entorno:
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 })],
});
La elección también puede depender de una variable de entorno en lugar del modo de build:
const database = process.env.DATABASE_URL
? postgres({ connectionString: process.env.DATABASE_URL })
: sqlite({ url: "file:./data.db" });