---
title: Configuration Reference
description: Complete reference for EmDash configuration options.
---

import { Aside } from "@astrojs/starlight/components";

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:

```js title="astro.config.mjs"
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.

```js
// 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](/deployment/database/) for details.

### `storage`

**Required.** Media storage adapter configuration.

```js
// 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](/deployment/storage/) for details.

### `plugins`

**Optional.** Array of EmDash plugins.

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

plugins: [seoPlugin()];
```

### `auth`

**Optional.** Authentication configuration.

```js
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.

| Option        | Type       | Default | Description               |
| ------------- | ---------- | ------- | ------------------------- |
| `domains`     | `string[]` | `[]`    | Allowed email domains     |
| `defaultRole` | `number`   | `20`    | Role for self-signups     |

```js
selfSignup: {
  domains: ["example.com", "acme.org"],
  defaultRole: 20, // Contributor
}
```

#### `auth.oauth`

Configure OAuth login providers.

```js
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.

| Option    | Type      | Default           | Description                    |
| --------- | --------- | ----------------- | ------------------------------ |
| `maxAge`  | `number`  | `2592000` (30d)   | Session lifetime in seconds    |
| `sliding` | `boolean` | `true`            | Reset expiry on activity       |

#### `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        |

<Aside>
	When `cloudflareAccess` is configured, it becomes the exclusive auth method. Passkeys, OAuth,
	magic links, and self-signup are disabled.
</Aside>

### `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`**.

```js
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

Astro only reflects **`X-Forwarded-*`** when the public host is allowed. Configure [**`security.allowedDomains`**](https://docs.astro.build/en/reference/configuration-reference/#securityalloweddomains) 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.

<Aside type="caution">
	Your reverse proxy should forward a **port-aware** `Host` / `X-Forwarded-Host` when you use non-default ports. If the proxy strips the port, **`rpId`** and Astro’s rebuilt URL can be wrong.
</Aside>

```js title="astro.config.mjs (excerpt)"
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

Import from `emdash/db`:

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

### `sqlite(config)`

SQLite database using better-sqlite3.

| Option | Type     | Description                   |
| ------ | -------- | ----------------------------- |
| `url`  | `string` | File path with `file:` prefix |

```js
sqlite({ url: "file:./data.db" });
```

### `libsql(config)`

libSQL database.

| Option      | Type     | Description                           |
| ----------- | -------- | ------------------------------------- |
| `url`       | `string` | Database URL                          |
| `authToken` | `string` | Auth token (optional for local files) |

```js
libsql({
	url: process.env.LIBSQL_DATABASE_URL,
	authToken: process.env.LIBSQL_AUTH_TOKEN,
});
```

### `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)      |

```js
postgres({ connectionString: process.env.DATABASE_URL });
```

### `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                   |

```js
// 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](/deployment/database/#read-replicas) for details.

<Aside type="caution">
	D1 requires migrations via Wrangler CLI. DDL statements are not allowed at runtime.
</Aside>

## Storage Adapters

Import from `emdash/astro`:

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

### `local(config)`

Local filesystem storage.

| Option      | Type     | Description                |
| ----------- | -------- | -------------------------- |
| `directory` | `string` | Directory path             |
| `baseUrl`   | `string` | Base URL for serving files |

```js
local({
	directory: "./uploads",
	baseUrl: "/_emdash/api/media/file",
});
```

### `r2(config)`

Cloudflare R2 binding.

| Option      | Type     | Description         |
| ----------- | -------- | ------------------- |
| `binding`   | `string` | R2 binding name     |
| `publicUrl` | `string` | Optional public URL |

```js
r2({
	binding: "MEDIA",
	publicUrl: "https://pub-xxxx.r2.dev",
});
```

### `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           |

```js
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

Configure the EmDash loader in `src/live.config.ts`:

```ts title="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:

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

## 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:

```bash
npx emdash auth secret
```

## package.json Configuration

Optional configuration in `package.json`:

```json title="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

EmDash generates types in `.emdash/types.ts`. Add to your `tsconfig.json`:

```json title="tsconfig.json"
{
	"compilerOptions": {
		"paths": {
			"@emdash-cms/types": ["./.emdash/types.ts"]
		}
	}
}
```

Generate types with:

```bash
npx emdash types
```
