Opções de Banco de Dados

Nesta página

O EmDash suporta múltiplos backends de banco de dados. Escolha com base no seu alvo de deploy.

Visão geral

Banco de dadosMelhor paraDeploy
D1Cloudflare WorkersEdge, distribuído globalmente
HyperdrivePostgreSQL no Cloudflare WorkersEdge, Postgres existente
PostgreSQLProdução Node.jsQualquer plataforma com Postgres
libSQLBancos de dados remotosEdge ou Node.js
SQLiteNode.js, desenvolvimento localServidor único

Cloudflare D1

D1 é o banco de dados SQLite serverless da Cloudflare. Use ao fazer deploy no Cloudflare Workers.

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

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

Configuração

OpçãoTipoPadrãoDescrição
bindingstringNome do binding D1 do wrangler.jsonc
sessionstring"disabled"Modo de replicação de leitura (veja abaixo)
bookmarkCookiestring"__em_d1_bookmark"Nome do cookie para bookmarks de sessão

Setup

wrangler.jsonc

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

wrangler.toml

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

Réplicas de leitura

D1 suporta replicação de leitura para reduzir a latência de leitura para sites distribuídos globalmente. Quando habilitada, as consultas de leitura são roteadas para réplicas próximas em vez de sempre acessar o banco primário.

O EmDash usa a API D1 Sessions para gerenciar isso transparentemente. Habilite com a opção session:

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

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

Modos de sessão

ModoComportamento
"disabled"Sem sessões. Todas as consultas vão para o primário. Padrão.
"auto"Requisições anônimas leem da réplica mais próxima. Usuários autenticados obtêm consistência read-your-writes via cookies bookmark.
"primary-first"Como "auto", mas a primeira consulta sempre vai para o primário. Para sites com escritas muito frequentes.

Como funciona

  • Visitantes anônimos obtêm first-unconstrained — leituras vão para a réplica mais próxima para menor latência. Como usuários anônimos nunca escrevem, não precisam de garantias de consistência.
  • Usuários autenticados (editores, autores) obtêm sessões baseadas em bookmarks. Após uma escrita, um cookie bookmark garante que a próxima requisição veja pelo menos aquele estado.
  • Requisições de escrita (POST, PUT, DELETE) sempre começam no banco primário.
  • Consultas em tempo de build (Astro content collections) ignoram sessões completamente e usam o primário diretamente.

libSQL

libSQL é um fork do SQLite que suporta conexões remotas. Use quando precisar de um banco remoto sem 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,
			}),
		}),
	],
});

Configuração

OpçãoTipoDescrição
urlstringURL do banco (libsql://... ou file:...)
authTokenstringToken de autenticação runtime para bancos remotos (opcional para local)
migrationAuthTokenEnvstringNome da variável do token de migração (padrão TURSO_AUTH_TOKEN)

Desenvolvimento local

Use um arquivo libSQL local durante o desenvolvimento:

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

PostgreSQL

PostgreSQL é suportado para deploys Node.js que precisam de um banco relacional completo.

import { postgres } from "emdash/db";

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

Configuração

Você pode conectar com uma string de conexão ou parâmetros individuais:

// String de conexão
database: postgres({
	connectionString: "postgres://user:password@localhost:5432/emdash",
});

// Parâmetros individuais
database: postgres({
	host: "localhost",
	port: 5432,
	database: "emdash",
	user: "emdash",
	password: process.env.DB_PASSWORD,
	ssl: true,
});
OpçãoTipoDescrição
connectionStringstringURL de conexão PostgreSQL
hoststringHost do banco
portnumberPorta do banco
databasestringNome do banco
userstringUsuário do banco
passwordstringSenha do banco
sslbooleanHabilitar SSL
pool.minnumberConexões pool mínimas (padrão 0)
pool.maxnumberConexões pool máximas (padrão 10)
pool.connectionTimeoutMillisnumberEspera máxima de conexão (pg padrão: 0, sem timeout)
pool.idleTimeoutMillisnumberTempo de vida do cliente ocioso (pg padrão: 10.000 ms)
migrationConnectionStringEnvstringNome da variável da string de conexão de migração (padrão DATABASE_URL)

Defina pool.connectionTimeoutMillis para um valor diferente de zero para limitar quanto tempo uma requisição espera quando o PostgreSQL está inacessível ou nenhuma conexão do pool está disponível. Defina pool.idleTimeoutMillis para 0 para manter clientes ociosos abertos até o pool fechar. Omitir qualquer opção preserva o padrão do pg.

Requisitos do role do banco

O EmDash cria e atualiza suas próprias tabelas PostgreSQL. Migrações core criam e alteram tabelas do sistema e de coleções, tipos de conteúdo criam tabelas ec_*, e adicionar ou remover um campo altera sua tabela de coleção. O role PostgreSQL configurado precisa de autoridade sobre o schema durante toda a vida do site, não apenas durante a configuração inicial.

Use um role canônico para o EmDash. Ele precisa de:

  • CONNECT no banco;
  • USAGE e CREATE no schema ativo;
  • propriedade de cada tabela e função do EmDash, diretamente ou através de membership com INHERIT no role proprietário; e
  • SELECT, INSERT, UPDATE e DELETE nessas tabelas.

Não precisa ser superusuário, ter CREATEDB ou CREATEROLE, nem criar extensões. PostgreSQL não fornece um grant ALTER ou DROP em tabelas: essas operações pertencem ao proprietário do objeto e aos roles que herdam seus privilégios. Conceder ALL em uma tabela a um role diferente não torna esse role proprietário. O EmDash não executa SET ROLE, então membership configurada sem herança não é suficiente.

A maioria das instalações pode usar o schema existente do banco, comumente public. Esta é a opção mais simples quando o banco é dedicado ao EmDash. Nos exemplos abaixo, emdash_app é o role de login na string de conexão do EmDash; use um role de provedor existente ou crie um login dedicado. Conceda acesso com uma conexão administrativa, substituindo seus nomes de banco, schema e role:

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

Esses grants permitem que o role crie novos objetos. Não mudam o proprietário de tabelas existentes; veja Reparar propriedade mista PostgreSQL.

O EmDash usa o current_schema() ativo do PostgreSQL. Não cria um schema nem define search_path, então verifique a conexão antes do deploy:

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

Opcional: usar um schema dedicado

Use um schema dedicado quando o EmDash compartilha um banco com outra aplicação ou quando você quer seus objetos isolados de public. Isso é opcional e mais fácil de configurar antes da primeira instalação do EmDash. Um banco dedicado ao EmDash não precisa de schema separado.

Assumindo que o role canônico emdash_app já existe, crie e selecione seu schema com uma conexão 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;

Isso não move uma instalação existente de public nem repara propriedade mista. Sites existentes devem manter seu schema atual e seguir Reparar propriedade mista PostgreSQL.

Pool de conexões

O adaptador usa pg.Pool internamente. Ajuste o tamanho do pool baseado no seu deploy:

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

Hyperdrive

Use o adaptador hyperdrive() para rodar o EmDash no Cloudflare Workers com um banco PostgreSQL existente — ou compatível com Postgres (ex: PlanetScale Postgres). O Hyperdrive faz pool e acelera a conexão pela rede da Cloudflare; o dialeto PostgreSQL do EmDash executa as 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.3 instalado no seu site (pnpm add pg)
  • compatibility_flags: ["nodejs_compat"]
  • compatibility_date >= "2024-09-23"

Setup

Primeiro prepare o role PostgreSQL. Depois crie a configuração Hyperdrive com a string de conexão desse role e adicione o binding à sua config 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>"

Configuração

OpçãoTipoPadrãoDescrição
bindingstring"HYPERDRIVE"Nome do binding primário (cache desabilitado) Hyperdrive
cachedBindingstringBinding opcional com cache habilitado para leituras anônimas (veja abaixo)
preferUncachedAfterWriteMsnumber60000*Após publicação, preferir binding por estes ms em leituras públicas anônimas (corresponder ao Hyperdrive max_age)
migrationConnectionStringEnvstringCLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING>Variável de ambiente contendo a URL direta do PostgreSQL origin para emdash migrate
maxnumber5Tamanho máximo do pool de conexões no Worker para o Hyperdrive

*Padrão 60000 aplica-se apenas quando cachedBinding está definido; ignorado caso contrário.

Servindo leituras anônimas do cache

Por padrão você desabilita o cache do Hyperdrive completamente. Mas requisições públicas anônimas usando GET ou HEAD podem tolerar uma curta janela de desatualização. Se aceitável, rode duas configurações Hyperdrive sobre o mesmo banco: uma com cache desabilitado (o binding primário) e uma com cache habilitado (cachedBinding). O EmDash roteia requisições públicas anônimas pelo binding com cache e todas as outras pelo primário sem cache.

# Primário — cache OFF
wrangler hyperdrive create emdash-db \
  --connection-string "postgres://user:password@host/db?sslmode=verify-full" \
  --caching-disabled

# Com cache — MESMO role e string de conexão, cache 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 é o padrão de duas configurações que a Cloudflare documenta para cache. O EmDash decide qual binding usar por requisição:

  • Leituras anônimas de caminhos do site público (GET/HEAD, sem sessão, não sob /_emdash) → cachedBinding com cache habilitado, exceto por uma curta janela após publicação de conteúdo (padrão 60s; defina preferUncachedAfterWriteMs para seu Hyperdrive max_age).
  • Requisições autenticadas (editores, autores) → binding sem cache.
  • Requisições de mutação (POST, PUT, PATCH, DELETE, incluindo anônimas) → binding sem cache.
  • Qualquer requisição sob /_emdash (admin, setup, auth, APIs internas), mesmo um GET anônimo → binding sem cache.
  • Migrações runtime e cold-start → sempre o binding primário.
  • Migrações gerenciadas por deploy → conectam diretamente ao origin PostgreSQL via migrationConnectionStringEnv.

Opcional: usar um role com cache separado

Migrações, setup, requisições autenticadas e escritas explícitas sempre usam o binding primário. Um role separado para cachedBinding não precisa de propriedade do schema ou CREATE, mas precisa de CONNECT, USAGE do schema e SELECT em cada tabela usada pelo site público.

Requisições públicas anônimas GET e HEAD também podem registrar hits de redirecionamento e 404s. Para preservar essas funcionalidades, o role com cache precisa adicionalmente de UPDATE em _emdash_redirects e SELECT, INSERT, UPDATE, DELETE em _emdash_404_log.

Adicione o role com cache após o EmDash completar suas migrações iniciais:

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;

Depois conecte como emdash_app para conceder acesso:

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;

Conecte com ambos os roles e verifique que reportam o mesmo current_database() e current_schema() antes de habilitar cachedBinding.

Reparar propriedade mista PostgreSQL

Se um site usou múltiplos usuários PostgreSQL, primeiro escolha o role canônico que a conexão principal do EmDash continuará usando. Faça backup e pare mudanças de schema durante o reparo.

Inspecione cada tabela no schema ativo:

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;

Inspecione propriedade de funções:

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;

Transfira cada objeto com nomes qualificados por schema:

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;

SQLite

SQLite usa o driver de banco integrado do Node.js e é a opção mais simples para deploys Node.js.

import { sqlite } from "emdash/db";

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

Configuração

OpçãoTipoDescrição
urlstringCaminho de arquivo com prefixo file:

Caminho do arquivo

A url deve começar com file::

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

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

// De variável de ambiente
database: sqlite({ url: `file:${process.env.DATABASE_PATH}` });

Migrações

O EmDash executa migrações core automaticamente por padrão para cada dialeto suportado. Astro build e sync também emitem um .emdash/migrations.json validado e sem secrets, que emdash migrate pode aplicar antes do deploy. SQLite, libSQL, PostgreSQL, D1 e o origin PostgreSQL direto atrás do Hyperdrive têm executores de deploy.

Veja Gerenciar Migrações do Banco Core para credenciais de destino, serialização CI, política runtime auto/check/manual e recuperação de registros desconhecidos ou escritas D1 ambíguas.

Se o banco está vazio (sem coleções) e o assistente de setup não foi completado, o EmDash aplica um arquivo seed no primeiro boot. O seed é lido de .emdash/seed.json, do caminho em package.json#emdash.seed, ou seed/seed.json — o que for encontrado primeiro — e incorporado no build em tempo de compilação. Se nenhum estiver presente, um seed padrão embutido é usado.

Configuração baseada em ambiente

Use diferentes bancos por ambiente:

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 })],
});

A escolha também pode depender de uma variável de ambiente ao invés do modo de build:

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