Configuration Reference

On this page

EmDash is configured through two files: astro.config.mjs for the integration and src/live.config.ts for content collections.

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

database

Required. Database adapter configuration.

// SQLite (Node.js)
database: sqlite({ url: "file:./data.db" });

// PostgreSQL
database: postgres({ connectionString: process.env.DATABASE_URL });

// libSQL
database: 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

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) — all fields from S3_* environment variables
storage: s3()

// Or with explicit values
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

Optional. Array of EmDash plugins.

import seoPlugin from "@emdash-cms/plugin-seo";

plugins: [seoPlugin()];

fonts

Optional. Admin UI font configuration.

By default, EmDash loads Noto Sans via the Astro Font API. Fonts are downloaded from Google at build time and self-hosted, so there are no runtime CDN requests. The base font covers Latin, Cyrillic, Greek, Devanagari, and Vietnamese scripts.

To add support for additional writing systems, pass script names:

emdash({
  fonts: {
    scripts: ["arabic", "japanese"],
  },
})

Available scripts: arabic, armenian, bengali, chinese-simplified, chinese-traditional, chinese-hongkong, devanagari, ethiopic, georgian, gujarati, gurmukhi, hebrew, japanese, kannada, khmer, korean, lao, malayalam, myanmar, oriya, sinhala, tamil, telugu, thai, tibetan.

Each script maps to the corresponding Noto Sans variant on Google Fonts (e.g. "arabic" loads Noto Sans Arabic). All font faces share a single font-family name and use unicode-range so the browser only downloads the files it needs for the characters on the page.

Set to false to disable font injection entirely and use system fonts:

emdash({
  fonts: false,
})

The admin CSS uses the --font-emdash CSS variable. This is set automatically by the font configuration above.

auth

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

Allow users to self-register if their email domain is allowed.

OptionTypeDefaultDescription
domainsstring[][]Allowed email domains
defaultRolenumber20Role for self-signups
selfSignup: {
  domains: ["example.com", "acme.org"],
  defaultRole: 20, // Contributor
}

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

Session configuration.

OptionTypeDefaultDescription
maxAgenumber2592000 (30d)Session lifetime in seconds
slidingbooleantrueReset expiry on activity

auth.cloudflareAccess

Use Cloudflare Access as the authentication provider instead of passkeys.

OptionTypeDefaultDescription
teamDomainstringrequiredYour Access team domain
audiencestringrequiredApplication Audience (AUD) tag
autoProvisionbooleantrueCreate users on first login
defaultRolenumber30Default role for new users
syncRolesbooleanfalseUpdate role on each login
roleMappingobjectMap IdP groups to roles

siteUrl

Optional. The public browser-facing origin for the site (scheme + host + optional port, no path).

Behind a TLS-terminating reverse proxy, Astro.url returns the internal address (http://localhost:4321) instead of the public one (https://cms.example.com). This breaks passkeys, CSRF origin matching, OAuth redirects, login redirects, MCP discovery, snapshot exports, sitemap, robots.txt, and JSON-LD structured data. Set siteUrl to fix all of these at once.

The integration validates this value at load time: it must be a valid URL with http: or https: protocol and is normalized to origin (path is stripped).

emdash({
	database: sqlite({ url: "file:./data.db" }),
	storage: local({
		directory: "./uploads",
		baseUrl: "/_emdash/api/media/file",
	}),
	siteUrl: "https://cms.example.com",
});

When siteUrl is not set in config, EmDash checks environment variables in order: EMDASH_SITE_URL, then SITE_URL. This is useful for container deployments where the public URL is set at runtime.

Reverse proxy setup

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 siteUrl 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 siteUrl 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",
			}),
			siteUrl: "https://cms.example.com",
		}),
	],
});

maxUploadSize

Optional. Maximum allowed media file upload size in bytes. Applies to both direct multipart uploads and signed-URL uploads. Defaults to 52_428_800 (50 MB).

emdash({
	database: sqlite({ url: "file:./data.db" }),
	storage: local({
		directory: "./uploads",
		baseUrl: "/_emdash/api/media/file",
	}),
	maxUploadSize: 100 * 1024 * 1024, // 100 MB
});
ValueDescription
number (bytes)Must be a positive finite integer
omittedDefaults to 50 MB

Uploads that exceed the configured limit are rejected with a 413 Payload Too Large response on the direct upload path, or a 400 Validation Error on the signed-URL path.

Database Adapters

Import from emdash/db:

import { sqlite, libsql, postgres, d1 } from "emdash/db";

sqlite(config)

SQLite database using better-sqlite3.

OptionTypeDescription
urlstringFile path with file: prefix
sqlite({ url: "file:./data.db" });

libsql(config)

libSQL database.

OptionTypeDescription
urlstringDatabase URL
authTokenstringAuth token (optional for local files)
libsql({
	url: process.env.LIBSQL_DATABASE_URL,
	authToken: process.env.LIBSQL_AUTH_TOKEN,
});

postgres(config)

PostgreSQL database with connection pooling.

OptionTypeDescription
connectionStringstringPostgreSQL connection URL
hoststringDatabase host
portnumberDatabase port
databasestringDatabase name
userstringDatabase user
passwordstringDatabase password
sslbooleanEnable SSL
pool.minnumberMinimum pool size (default: 0)
pool.maxnumberMaximum pool size (default: 10)
postgres({ connectionString: process.env.DATABASE_URL });

d1(config)

Cloudflare D1 database. Import from @emdash-cms/cloudflare.

OptionTypeDefaultDescription
bindingstringD1 binding name from wrangler.jsonc
sessionstring"disabled"Read replication mode: "disabled", "auto", or "primary-first"
bookmarkCookiestring"__em_d1_bookmark"Cookie name for session bookmarks
// Basic
d1({ binding: "DB" });

// With read replicas
d1({ 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

Import from emdash/astro:

import emdash, { local, r2, s3 } from "emdash/astro";

local(config)

Local filesystem storage.

OptionTypeDescription
directorystringDirectory path
baseUrlstringBase URL for serving files
local({
	directory: "./uploads",
	baseUrl: "/_emdash/api/media/file",
});

r2(config)

Cloudflare R2 binding.

OptionTypeDescription
bindingstringR2 binding name
publicUrlstringOptional public URL
r2({
	binding: "MEDIA",
	publicUrl: "https://pub-xxxx.r2.dev",
});

s3(config?)

S3-compatible storage. All config fields are optional: any field omitted from s3({...}) is resolved from the matching S3_* environment variable when the Node process starts. Explicit values always take precedence.

Prerequisite: install @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner in your project. EmDash core does not bundle the AWS SDK. See Storage Options → S3-Compatible Storage for details.

OptionTypeDescription
endpointstringS3 endpoint URL (S3_ENDPOINT)
bucketstringBucket name (S3_BUCKET)
accessKeyIdstringAccess key (S3_ACCESS_KEY_ID)
secretAccessKeystringSecret key (S3_SECRET_ACCESS_KEY)
regionstringRegion, default "auto" (S3_REGION)
publicUrlstringOptional CDN URL (S3_PUBLIC_URL)
// All fields from S3_* environment variables (Node container deployments)
s3()

// Mix: CDN from config, rest from environment
s3({ publicUrl: "https://cdn.example.com" })

// All explicit (unchanged from before)
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",
})

Runtime environment variable resolution is a Node-only feature. On Cloudflare Workers, secrets and variables are exposed through the env parameter of the fetch handler, not through process.env, so S3_* environment variables are not picked up. Workers deployments should either use the r2(config) adapter or pass explicit values to s3({...}). See Storage Options for details.

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

The emdashLoader() function accepts optional configuration:

emdashLoader({
	// Currently no options - reserved for future use
});

Environment Variables

EmDash respects these environment variables:

VariableDescription
EMDASH_SITE_URLPublic browser-facing origin (falls back to SITE_URL)
EMDASH_DATABASE_URLOverride database URL
EMDASH_AUTH_SECRETSecret for passkey authentication
EMDASH_PREVIEW_SECRETSecret for preview token generation
EMDASH_URLRemote EmDash URL for schema sync

Generate an auth secret with:

npx emdash auth secret

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"
	}
}
OptionDescription
labelTemplate name for display
descriptionTemplate description
seedPath to seed JSON file
urlRemote URL for schema sync
previewDemo site URL for template preview

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