Configuration Reference
EmDash is configured through two files: astro.config.mjs for the integration and src/live.config.ts for content collections.
Astro Integration
Section titled “Astro Integration”Configure EmDash as an Astro integration:
import { defineConfig } from "astro/config";import emdash, { local, r2, s3 } from "emdash/astro";import { sqlite, libsql, d1 } from "emdash/db";
export default defineConfig({ integrations: [ emdash({ database: sqlite({ url: "file:./data.db" }), storage: local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file", }), plugins: [], }), ],});Integration Options
Section titled “Integration Options”database
Section titled “database”Required. Database adapter configuration.
// SQLite (Node.js)database: sqlite({ url: "file:./data.db" });
// PostgreSQLdatabase: postgres({ connectionString: process.env.DATABASE_URL });
// libSQLdatabase: libsql({ url: process.env.LIBSQL_DATABASE_URL, authToken: process.env.LIBSQL_AUTH_TOKEN,});
// Cloudflare D1 (import from @emdash-cms/cloudflare)database: d1({ binding: "DB" });See Database Options for details.
storage
Section titled “storage”Required. Media storage adapter configuration.
// Local filesystem (development)storage: local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file",});
// R2 binding (Cloudflare Workers)storage: r2({ binding: "MEDIA", publicUrl: "https://pub-xxxx.r2.dev", // optional});
// S3-compatible (any platform)storage: s3({ endpoint: "https://s3.amazonaws.com", bucket: "my-bucket", accessKeyId: process.env.S3_ACCESS_KEY_ID, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, region: "us-east-1", // optional, default: "auto" publicUrl: "https://cdn.example.com", // optional});See Storage Options for details.
plugins
Section titled “plugins”Optional. Array of EmDash plugins.
import seoPlugin from "@emdash-cms/plugin-seo";
plugins: [seoPlugin()];Optional. Authentication configuration.
auth: { // Self-signup configuration selfSignup: { domains: ["example.com"], defaultRole: 20, // Contributor },
// OAuth providers oauth: { github: { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }, google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }, },
// Session configuration session: { maxAge: 30 * 24 * 60 * 60, // 30 days sliding: true, // Reset expiry on activity },
// OR use Cloudflare Access (exclusive mode) cloudflareAccess: { teamDomain: "myteam.cloudflareaccess.com", audience: "your-app-audience-tag", autoProvision: true, defaultRole: 30, syncRoles: false, roleMapping: { "Admins": 50, "Editors": 40, }, },}auth.selfSignup
Section titled “auth.selfSignup”Allow users to self-register if their email domain is allowed.
| Option | Type | Default | Description |
|---|---|---|---|
domains | string[] | [] | Allowed email domains |
defaultRole | number | 20 | Role for self-signups |
selfSignup: { domains: ["example.com", "acme.org"], defaultRole: 20, // Contributor}auth.oauth
Section titled “auth.oauth”Configure OAuth login providers.
oauth: { github: { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }, google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, },}auth.session
Section titled “auth.session”Session configuration.
| Option | Type | Default | Description |
|---|---|---|---|
maxAge | number | 2592000 (30d) | Session lifetime in seconds |
sliding | boolean | true | Reset expiry on activity |
auth.cloudflareAccess
Section titled “auth.cloudflareAccess”Use Cloudflare Access as the authentication provider instead of passkeys.
| Option | Type | Default | Description |
|---|---|---|---|
teamDomain | string | required | Your Access team domain |
audience | string | required | Application Audience (AUD) tag |
autoProvision | boolean | true | Create users on first login |
defaultRole | number | 30 | Default role for new users |
syncRoles | boolean | false | Update role on each login |
roleMapping | object | — | Map IdP groups to roles |
passkeyPublicOrigin
Section titled “passkeyPublicOrigin”Optional. Pass a full browser-facing origin (scheme + host + optional port, no path) so WebAuthn rpId and origin match what the user’s browser sends in clientData.origin.
By default, passkeys follow Astro.url / request.url. Behind a TLS-terminating reverse proxy, the app often still sees http:// on the internal hop while the tab is https://, or the reconstructed host does not match the public name — which breaks passkey verification. Set passkeyPublicOrigin to the origin users type in the address bar (for example https://cms.example.com or https://cms.example.com:8443).
The integration validates this value at load time: it must be a valid URL with http: or https: protocol and is normalized to origin.
emdash({ database: sqlite({ url: "file:./data.db" }), storage: local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file", }), passkeyPublicOrigin: "https://cms.example.com",});Reverse proxy and passkeys
Section titled “Reverse proxy and passkeys”Astro only reflects X-Forwarded-* when the public host is allowed. Configure security.allowedDomains for the hostname (and schemes) your users hit. In astro dev, add matching vite.server.allowedHosts so Vite accepts the proxy Host header.
Prefer fixing allowedDomains (and forwarded headers) first; use passkeyPublicOrigin when the reconstructed URL still diverges from the browser origin (typical when TLS is terminated in front and the upstream request stays http://).
With TLS in front, binding the dev server to loopback (astro dev --host 127.0.0.1) is often enough: the proxy connects locally while passkeyPublicOrigin matches the public HTTPS origin.
import { defineConfig } from "astro/config";import emdash, { local } from "emdash/astro";import { sqlite } from "emdash/db";
export default defineConfig({ security: { allowedDomains: [ { hostname: "cms.example.com", protocol: "https" }, { hostname: "cms.example.com", protocol: "http" }, ], }, vite: { server: { allowedHosts: ["cms.example.com"], }, }, integrations: [ emdash({ database: sqlite({ url: "file:./data.db" }), storage: local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file", }), passkeyPublicOrigin: "https://cms.example.com", }), ],});Database Adapters
Section titled “Database Adapters”Import from emdash/db:
import { sqlite, libsql, postgres, d1 } from "emdash/db";sqlite(config)
Section titled “sqlite(config)”SQLite database using better-sqlite3.
| Option | Type | Description |
|---|---|---|
url | string | File path with file: prefix |
sqlite({ url: "file:./data.db" });libsql(config)
Section titled “libsql(config)”libSQL database.
| Option | Type | Description |
|---|---|---|
url | string | Database URL |
authToken | string | Auth token (optional for local files) |
libsql({ url: process.env.LIBSQL_DATABASE_URL, authToken: process.env.LIBSQL_AUTH_TOKEN,});postgres(config)
Section titled “postgres(config)”PostgreSQL database with connection pooling.
| Option | Type | Description |
|---|---|---|
connectionString | string | PostgreSQL connection URL |
host | string | Database host |
port | number | Database port |
database | string | Database name |
user | string | Database user |
password | string | Database password |
ssl | boolean | Enable SSL |
pool.min | number | Minimum pool size (default: 0) |
pool.max | number | Maximum pool size (default: 10) |
postgres({ connectionString: process.env.DATABASE_URL });d1(config)
Section titled “d1(config)”Cloudflare D1 database. Import from @emdash-cms/cloudflare.
| Option | Type | Default | Description |
|---|---|---|---|
binding | string | — | D1 binding name from wrangler.jsonc |
session | string | "disabled" | Read replication mode: "disabled", "auto", or "primary-first" |
bookmarkCookie | string | "__ec_d1_bookmark" | Cookie name for session bookmarks |
// Basicd1({ binding: "DB" });
// With read replicasd1({ binding: "DB", session: "auto" });When session is "auto" or "primary-first", EmDash uses the D1 Sessions API to route read queries to nearby replicas. Authenticated users get bookmark-based read-your-writes consistency. See Database Options — Read Replicas for details.
Storage Adapters
Section titled “Storage Adapters”Import from emdash/astro:
import emdash, { local, r2, s3 } from "emdash/astro";local(config)
Section titled “local(config)”Local filesystem storage.
| Option | Type | Description |
|---|---|---|
directory | string | Directory path |
baseUrl | string | Base URL for serving files |
local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file",});r2(config)
Section titled “r2(config)”Cloudflare R2 binding.
| Option | Type | Description |
|---|---|---|
binding | string | R2 binding name |
publicUrl | string | Optional public URL |
r2({ binding: "MEDIA", publicUrl: "https://pub-xxxx.r2.dev",});s3(config)
Section titled “s3(config)”S3-compatible storage.
| Option | Type | Description |
|---|---|---|
endpoint | string | S3 endpoint URL |
bucket | string | Bucket name |
accessKeyId | string | Access key |
secretAccessKey | string | Secret key |
region | string | Region (default: "auto") |
publicUrl | string | Optional CDN URL |
s3({ endpoint: "https://xxx.r2.cloudflarestorage.com", bucket: "media", accessKeyId: process.env.R2_ACCESS_KEY_ID, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY, publicUrl: "https://cdn.example.com",});Live Collections
Section titled “Live Collections”Configure the EmDash loader in src/live.config.ts:
import { defineLiveCollection } from "astro:content";import { emdashLoader } from "emdash/runtime";
export const collections = { _emdash: defineLiveCollection({ loader: emdashLoader(), }),};Loader Options
Section titled “Loader Options”The emdashLoader() function accepts optional configuration:
emdashLoader({ // Currently no options - reserved for future use});Environment Variables
Section titled “Environment Variables”EmDash respects these environment variables:
| Variable | Description |
|---|---|
EMDASH_DATABASE_URL | Override database URL |
EMDASH_AUTH_SECRET | Secret for passkey authentication |
EMDASH_PREVIEW_SECRET | Secret for preview token generation |
EMDASH_URL | Remote EmDash URL for schema sync |
Generate an auth secret with:
npx emdash auth secretpackage.json Configuration
Section titled “package.json Configuration”Optional configuration in package.json:
{ "emdash": { "label": "My Blog Template", "description": "A clean, minimal blog template", "seed": ".emdash/seed.json", "url": "https://my-site.pages.dev", "preview": "https://emdash-blog.pages.dev" }}| Option | Description |
|---|---|
label | Template name for display |
description | Template description |
seed | Path to seed JSON file |
url | Remote URL for schema sync |
preview | Demo site URL for template preview |
TypeScript Configuration
Section titled “TypeScript Configuration”EmDash generates types in .emdash/types.ts. Add to your tsconfig.json:
{ "compilerOptions": { "paths": { "@emdash-cms/types": ["./.emdash/types.ts"] } }}Generate types with:
npx emdash types