EmDash는 여러 데이터베이스 백엔드를 지원합니다. 배포 대상에 따라 선택하세요.
개요
| 데이터베이스 | 최적 용도 | 배포 |
|---|---|---|
| D1 | Cloudflare Workers | 엣지, 전 세계 분산 |
| Hyperdrive | Cloudflare Workers의 PostgreSQL | 엣지, 기존 Postgres |
| PostgreSQL | 프로덕션 Node.js | Postgres가 있는 모든 플랫폼 |
| libSQL | 원격 데이터베이스 | 엣지 또는 Node.js |
| SQLite | Node.js, 로컬 개발 | 단일 서버 |
Cloudflare D1
D1은 Cloudflare의 서버리스 SQLite 데이터베이스입니다. Cloudflare Workers에 배포할 때 사용합니다.
import { d1 } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
database: d1({ binding: "DB" }),
}),
],
});
구성
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
binding | string | — | wrangler.jsonc의 D1 바인딩 이름 |
session | string | "disabled" | 읽기 복제 모드 (아래 참조) |
bookmarkCookie | string | "__em_d1_bookmark" | 세션 북마크용 쿠키 이름 |
설정
wrangler.jsonc
{
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-db"
}
]
} wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "emdash-db" 읽기 복제본
D1은 전 세계적으로 분산된 사이트의 읽기 지연 시간을 줄이기 위해 읽기 복제를 지원합니다. 활성화되면 읽기 쿼리가 항상 기본 데이터베이스를 조회하는 대신 가까운 복제본으로 라우팅됩니다.
EmDash는 D1 Sessions API를 사용하여 이를 투명하게 관리합니다. session 옵션으로 활성화합니다:
import { d1 } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
database: d1({
binding: "DB",
session: "auto",
}),
}),
],
});
세션 모드
| 모드 | 동작 |
|---|---|
"disabled" | 세션 없음. 모든 쿼리가 기본 데이터베이스로 전송. 기본값. |
"auto" | 익명 요청은 가장 가까운 복제본에서 읽기. 인증된 사용자는 북마크 쿠키를 통해 read-your-writes 일관성 확보. |
"primary-first" | "auto"와 같지만 첫 번째 쿼리는 항상 기본으로 전송. 쓰기가 매우 빈번한 사이트용. |
작동 방식
- 익명 방문자는
first-unconstrained를 받음 — 읽기는 가장 낮은 지연 시간을 위해 가장 가까운 복제본으로 전송. 익명 사용자는 쓰기를 하지 않으므로 일관성 보장이 필요 없음. - 인증된 사용자 (편집자, 작성자)는 북마크 기반 세션을 받음. 쓰기 후 북마크 쿠키가 다음 요청이 적어도 해당 상태를 보도록 보장.
- 쓰기 요청 (
POST,PUT,DELETE)은 항상 기본 데이터베이스에서 시작. - 빌드 시간 쿼리 (Astro content collections)는 세션을 완전히 우회하고 기본을 직접 사용.
libSQL
libSQL은 원격 연결을 지원하는 SQLite 포크입니다. 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,
}),
}),
],
});
구성
| 옵션 | 타입 | 설명 |
|---|---|---|
url | string | 데이터베이스 URL (libsql://... 또는 file:...) |
authToken | string | 원격 데이터베이스용 런타임 인증 토큰 (로컬의 경우 선택 사항) |
migrationAuthTokenEnv | string | 마이그레이션 토큰 변수 이름 (기본값 TURSO_AUTH_TOKEN) |
로컬 개발
개발 중 로컬 libSQL 파일을 사용합니다:
database: libsql({ url: "file:./data.db" });
PostgreSQL
PostgreSQL은 완전한 관계형 데이터베이스가 필요한 Node.js 배포에 지원됩니다.
import { postgres } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: postgres({
connectionString: process.env.DATABASE_URL,
}),
}),
],
});
구성
연결 문자열 또는 개별 매개변수로 연결할 수 있습니다:
// 연결 문자열
database: postgres({
connectionString: "postgres://user:password@localhost:5432/emdash",
});
// 개별 매개변수
database: postgres({
host: "localhost",
port: 5432,
database: "emdash",
user: "emdash",
password: process.env.DB_PASSWORD,
ssl: true,
});
| 옵션 | 타입 | 설명 |
|---|---|---|
connectionString | string | PostgreSQL 연결 URL |
host | string | 데이터베이스 호스트 |
port | number | 데이터베이스 포트 |
database | string | 데이터베이스 이름 |
user | string | 데이터베이스 사용자 |
password | string | 데이터베이스 비밀번호 |
ssl | boolean | SSL 활성화 |
pool.min | number | 최소 풀 연결 수 (기본값 0) |
pool.max | number | 최대 풀 연결 수 (기본값 10) |
pool.connectionTimeoutMillis | number | 최대 연결 대기 시간 (pg 기본값: 0, 타임아웃 없음) |
pool.idleTimeoutMillis | number | 유휴 클라이언트 수명 (pg 기본값: 10,000 ms) |
migrationConnectionStringEnv | string | 마이그레이션 연결 문자열 변수 이름 (기본값 DATABASE_URL) |
pool.connectionTimeoutMillis를 0이 아닌 값으로 설정하여 PostgreSQL에 도달할 수 없거나 풀된 연결을 사용할 수 없을 때 요청이 대기하는 시간을 제한합니다. pool.idleTimeoutMillis를 0으로 설정하여 풀이 닫힐 때까지 유휴 클라이언트를 열어 둡니다. 어느 옵션이든 생략하면 pg 기본값이 유지됩니다.
데이터베이스 역할 요구 사항
EmDash는 자체 PostgreSQL 테이블을 생성하고 업데이트합니다. 코어 마이그레이션은 시스템 및 컬렉션 테이블을 생성하고 변경하며, 콘텐츠 타입은 ec_* 테이블을 생성하고, 필드를 추가하거나 제거하면 해당 컬렉션 테이블이 변경됩니다. 따라서 구성된 PostgreSQL 역할은 초기 설정 시뿐만 아니라 사이트의 전체 수명 동안 스키마 권한이 필요합니다.
EmDash에 하나의 표준 역할을 사용하세요. 필요한 것:
- 데이터베이스에 대한
CONNECT; - 활성 스키마에 대한
USAGE및CREATE; - 모든 EmDash 테이블과 함수의 소유권 (직접 또는 소유 역할의
INHERIT멤버십을 통해); 및 - 해당 테이블에 대한
SELECT,INSERT,UPDATE,DELETE.
슈퍼유저일 필요 없고, CREATEDB나 CREATEROLE도 필요 없으며, 확장 생성도 불필요합니다. PostgreSQL은 ALTER 또는 DROP 테이블 grant를 제공하지 않습니다: 이러한 작업은 객체 소유자와 해당 권한을 상속하는 역할에 속합니다. 다른 역할에 테이블에 대한 ALL을 부여해도 해당 역할이 소유자가 되지 않습니다. EmDash는 SET ROLE을 실행하지 않으므로 상속 없이 구성된 멤버십은 충분하지 않습니다.
대부분의 설치는 일반적으로 public인 데이터베이스의 기존 스키마를 사용할 수 있습니다. 데이터베이스가 EmDash 전용인 경우 가장 간단한 옵션입니다. 아래 예제에서 emdash_app은 EmDash 연결 문자열의 로그인 역할입니다. 기존 프로바이더 역할을 사용하거나 전용 로그인을 만드세요. 관리 연결로 액세스를 부여하고 데이터베이스, 스키마, 역할 이름을 대체하세요:
GRANT CONNECT ON DATABASE app TO emdash_app;
GRANT USAGE, CREATE ON SCHEMA public TO emdash_app;
이러한 grant는 역할이 새 객체를 생성할 수 있게 합니다. 기존 테이블의 소유자는 변경되지 않습니다. PostgreSQL 혼합 소유권 복구를 참조하세요.
EmDash는 PostgreSQL의 활성 current_schema()를 사용합니다. 스키마를 생성하거나 search_path를 설정하지 않으므로 배포 전에 연결을 확인하세요:
SELECT
current_database(),
session_user,
current_user,
current_schema(),
current_setting('search_path');
선택 사항: 전용 스키마 사용
EmDash가 다른 애플리케이션과 데이터베이스를 공유하거나 객체를 public에서 격리하려는 경우 전용 스키마를 사용합니다. 이는 선택 사항이며 EmDash 초기 설정 전에 구성하는 것이 가장 쉽습니다. EmDash 전용 데이터베이스는 별도의 스키마가 필요하지 않습니다.
표준 역할 emdash_app이 이미 존재한다고 가정하고, 관리 연결로 스키마를 생성하고 선택합니다:
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;
이것은 기존 설치를 public에서 이동하거나 혼합 소유권을 복구하지 않습니다. 기존 사이트는 현재 스키마를 유지하고 대신 PostgreSQL 혼합 소유권 복구를 따라야 합니다.
연결 풀링
어댑터는 내부적으로 pg.Pool을 사용합니다. 배포에 따라 풀 크기를 조정합니다:
database: postgres({
connectionString: process.env.DATABASE_URL,
pool: { min: 2, max: 20 },
});
Hyperdrive
hyperdrive() 어댑터를 사용하여 기존 PostgreSQL — 또는 Postgres 호환 (예: PlanetScale Postgres) — 데이터베이스를 백엔드로 Cloudflare Workers에서 EmDash를 실행합니다. Hyperdrive는 Cloudflare 네트워크를 통해 연결을 풀링하고 가속합니다. EmDash의 PostgreSQL 방언이 쿼리를 실행합니다.
import { hyperdrive, r2 } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
database: hyperdrive({ binding: "HYPERDRIVE" }),
storage: r2({ binding: "MEDIA" }),
}),
],
});
요구 사항
- 사이트에
pg >= 8.16.3설치 (pnpm add pg) compatibility_flags: ["nodejs_compat"]compatibility_date >= "2024-09-23"
설정
먼저 PostgreSQL 역할을 준비합니다. 그런 다음 해당 역할의 연결 문자열로 Hyperdrive 구성을 만들고 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>" 구성
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
binding | string | "HYPERDRIVE" | 기본 (캐싱 비활성화) Hyperdrive 바인딩 이름 |
cachedBinding | string | — | 익명 읽기용 선택적 캐싱 활성화 바인딩 (아래 참조) |
preferUncachedAfterWriteMs | number | 60000* | 콘텐츠 게시 후 익명 공개 읽기에서 이 ms 동안 binding 선호 (Hyperdrive max_age에 맞춤) |
migrationConnectionStringEnv | string | CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING> | emdash migrate용 직접 PostgreSQL origin URL이 포함된 환경 변수 |
max | number | 5 | Worker 내 Hyperdrive 연결 풀 최대 크기 |
*기본값 60000은 cachedBinding이 설정된 경우에만 적용; 그렇지 않으면 무시.
캐시에서 익명 읽기 제공
기본적으로 Hyperdrive 캐싱을 완전히 비활성화합니다. 관리자와 쓰기에 read-after-write 일관성이 필요하기 때문입니다. 하지만 GET 또는 HEAD를 사용하는 익명 공개 요청은 짧은 부실 기간을 허용할 수 있습니다. 해당 트레이드오프가 허용되면 동일한 데이터베이스에 두 개의 Hyperdrive 구성을 실행합니다: 캐싱이 비활성화된 것 (기본 binding)과 캐싱이 활성화된 것 (cachedBinding). EmDash는 익명 공개 요청을 캐시 활성화 바인딩으로 라우팅하고 다른 모든 요청을 캐시 없는 기본으로 라우팅합니다.
# 기본 — 캐싱 OFF (관리자, 인증된 요청, 쓰기, 마이그레이션에 사용)
wrangler hyperdrive create emdash-db \
--connection-string "postgres://user:password@host/db?sslmode=verify-full" \
--caching-disabled
# 캐시 — 동일한 데이터베이스 역할과 연결 문자열, 캐싱 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" });
이것은 Cloudflare가 캐싱을 위해 문서화한 두 구성 패턴입니다. EmDash는 요청별로 어떤 바인딩을 사용할지 결정합니다:
- 공개 사이트 경로의 익명 읽기 (
GET/HEAD, 세션 없음,/_emdash아래 아님) → 캐시 활성화cachedBinding, 단 콘텐츠 게시 후 짧은 기간 (기본 60초;preferUncachedAfterWriteMs를 Hyperdrivemax_age로 설정)에는 EmDash가 캐시 없는binding을 선호하여 리빌드가 아직 부실한 Hyperdrive 결과에서 엣지/객체 캐시를 재시드하지 않도록 함. - 인증된 요청 (편집자, 작성자) → 캐시 없는
binding. - 변경 요청 (
POST,PUT,PATCH,DELETE, 익명 포함) → 캐시 없는binding. /_emdash아래의 모든 요청 (관리, 설정, 인증, 내부 API), 익명GET도 → 캐시 없는binding.- 런타임 마이그레이션과 콜드 스타트 → 항상 기본
binding. - 배포 관리 마이그레이션 →
migrationConnectionStringEnv를 사용하여 PostgreSQL origin에 직접 연결; Hyperdrive 바인딩을 절대 사용하지 않음.
선택 사항: 별도의 캐시 역할 사용
마이그레이션, 설정, 인증된 요청, 명시적 쓰기 요청은 항상 기본 binding을 사용합니다. cachedBinding용 별도 역할은 스키마 소유권이나 CREATE가 필요 없지만 CONNECT, 스키마 USAGE, 공개 사이트에서 사용하는 모든 테이블에 대한 SELECT가 필요합니다.
익명 공개 GET 및 HEAD 요청은 리다이렉트 히트와 404도 기록할 수 있습니다. 이러한 기능을 유지하려면 캐시 역할에 _emdash_redirects에 대한 UPDATE와 _emdash_404_log에 대한 SELECT, INSERT, UPDATE, DELETE가 추가로 필요합니다. 공개 GET 또는 HEAD 중에 쓰는 플러그인이나 애플리케이션 코드는 더 많이 필요할 수 있습니다. 제한된 캐시 역할로 사이트를 테스트하지 않았다면 두 바인딩에 동일한 역할을 사용하세요.
EmDash가 초기 마이그레이션을 완료한 후 캐시 역할을 추가합니다. 아래 예제는 선택적 emdash 스키마를 사용합니다; public 같은 활성 스키마로 대체하세요. 프로바이더의 관리 역할로 로그인과 데이터베이스 설정을 만듭니다:
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;
그런 다음 스키마와 테이블 소유자인 emdash_app으로 연결하여 기존 및 향후 테이블에 대한 액세스를 부여합니다:
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;
cachedBinding을 활성화하기 전에 두 역할로 연결하여 동일한 current_database()와 current_schema()를 보고하는지 확인하세요. 공유 스키마에서 GRANT SELECT ON ALL TABLES는 관련 없는 테이블도 노출합니다. 대신 개별 EmDash 테이블에 액세스를 부여하고, 컬렉션이나 다른 스키마 객체가 추가되면 해당 grant를 업데이트하세요.
PostgreSQL 혼합 소유권 복구
사이트가 여러 PostgreSQL 사용자를 사용한 경우, 먼저 기본 EmDash 연결이 계속 사용할 표준 역할을 선택합니다. 백업을 만들고 소유권 복구 중 스키마 변경을 중지합니다.
활성 스키마의 모든 테이블을 검사합니다:
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;
EmDash 객체에는 _emdash_* 및 _plugin_* 시스템 테이블, ec_* 컬렉션 테이블, content_taxonomies, media, options, revisions, taxonomies 같은 접두사 없는 테이블이 포함됩니다. 전용 EmDash 스키마에서 모든 애플리케이션 테이블은 표준 소유자를 가져야 합니다.
EmDash는 미디어 사용 트리거에서 사용되는 PostgreSQL 함수도 생성합니다. 함수 소유권을 검사하고 복구 명령을 위해 각 함수의 인수 서명을 유지합니다:
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;
소유권을 변경할 수 있는 슈퍼유저 또는 프로바이더 역할로 일치하지 않는 각 객체를 이전합니다. 항상 스키마 한정 이름을 사용합니다:
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;
각 ALTER FUNCTION 문에서 인벤토리 쿼리가 반환한 인수 목록을 사용합니다. 테이블의 소유자를 변경하면 연결된 인덱스, 제약 조건, 트리거도 포함되지만 독립적인 트리거 함수는 포함되지 않습니다. 모든 EmDash 테이블과 함수가 표준 소유자를 보고할 때까지 두 인벤토리 쿼리를 반복한 다음 해당 역할로 연결하여 애플리케이션을 시작하기 전에 current_schema()를 확인합니다.
비슈퍼유저가 소유권을 이전하려면 객체를 소유하거나 소유권을 상속해야 하고, 새 소유자에게 SET ROLE할 수 있어야 하며, 새 소유자는 스키마에 CREATE를 가져야 합니다. 관리형 PostgreSQL 프로바이더는 이전을 수행하기 위해 관리 역할을 요구할 수 있습니다.
SQLite
SQLite는 Node.js의 내장 데이터베이스 드라이버를 사용하며 Node.js 배포의 가장 간단한 옵션입니다.
import { sqlite } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
}),
],
});
구성
| 옵션 | 타입 | 설명 |
|---|---|---|
url | string | file: 접두사가 있는 파일 경로 |
파일 경로
url은 file:로 시작해야 합니다:
// 상대 경로
database: sqlite({ url: "file:./data/emdash.db" });
// 절대 경로
database: sqlite({ url: "file:/var/data/emdash.db" });
// 환경 변수에서
database: sqlite({ url: `file:${process.env.DATABASE_PATH}` });
마이그레이션
EmDash는 지원되는 모든 방언에 대해 기본적으로 코어 마이그레이션을 자동으로 실행합니다. Astro 빌드와 동기화는 검증된 시크릿 없는 .emdash/migrations.json도 출력하며, emdash migrate가 배포 전에 적용할 수 있습니다. SQLite, libSQL, PostgreSQL, D1, Hyperdrive 뒤의 직접 PostgreSQL origin에 배포 실행기가 있습니다.
대상 자격 증명, CI 직렬화, auto/check/manual 런타임 정책, 알 수 없는 레코드 또는 모호한 D1 쓰기에서의 복구는 코어 데이터베이스 마이그레이션 관리를 참조하세요.
PostgreSQL의 경우 런타임 마이그레이션은 구성된 연결을 통해 실행됩니다. Hyperdrive 런타임 마이그레이션은 항상 기본 바인딩을 사용합니다. 배포 관리 Hyperdrive 마이그레이션은 PostgreSQL origin에 직접 연결합니다. 코어 마이그레이션은 테이블, 인덱스, 함수를 생성하고 열과 제약 조건을 변경하거나 삭제하며 기존 행을 업데이트할 수 있습니다. 연결하고 행을 수정할 수 있지만 기존 EmDash 객체를 소유하지 않는 역할로는 충분하지 않습니다. 런타임 마이그레이션이 설정 전에 실행되므로 설정 마법사는 누락된 데이터베이스 권한을 복구할 수 없습니다.
데이터베이스가 비어 있고 (컬렉션 없음) 설정 마법사가 완료되지 않은 경우, EmDash는 첫 부팅 시 시드 파일도 적용합니다. 시드는 .emdash/seed.json, package.json#emdash.seed의 경로, 또는 seed/seed.json에서 읽혀집니다 — 먼저 발견되는 것이 사용됩니다 — 그리고 컴파일 시 빌드에 인라인됩니다. 없으면 내장 기본 시드가 사용됩니다. 기존 데이터베이스에 대한 후속 부팅은 내용을 그대로 둡니다.
환경 기반 구성
환경별로 다른 데이터베이스를 사용합니다:
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 })],
});
빌드 모드 대신 환경 변수에 따라 선택할 수도 있습니다:
const database = process.env.DATABASE_URL
? postgres({ connectionString: process.env.DATABASE_URL })
: sqlite({ url: "file:./data.db" });