* fix: passkeys behind TLS reverse proxy Add passkeyPublicOrigin and wire it through passkey routes so origin/rpId match the browser when dev runs behind nginx. Expose dev-only /_emdash/api/dev/passkey-url, add admin messaging for insecure WebAuthn contexts, nginx repro under demos/simple, and direct kysely dependency for the simple demo Node adapter bundle. Made-with: Cursor * docs: add passkeyPublicOrigin to configuration reference Adds the new passkeyPublicOrigin option and reverse proxy guidance to the public-facing configuration docs as requested in PR review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * update tests and more docs * fix: add missing refresh-server-pat fixture and restore docs heading --------- Co-authored-by: Joseph Eftekhari <jdeftekhari@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.0 KiB
Configuration
astro.config.mjs
Node.js (local development / self-hosted)
import node from "@astrojs/node";
import react from "@astrojs/react";
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
image: {
layout: "constrained",
responsiveStyles: true,
},
integrations: [
react(),
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
devToolbar: { enabled: false },
});
Reverse proxy and passkeys
Passkey rpId / origin follow Astro context.url, which only reflects X-Forwarded-* when you declare allowed public hosts (security.allowedDomains). In dev, add matching vite.server.allowedHosts or Vite rejects the proxy Host. Use emdash({ passkeyPublicOrigin: "https://…" }) when the browser origin and reconstructed URL still disagree (common with TLS termination). With TLS terminated in front, astro dev --host 127.0.0.1 (loopback) is usually enough: the proxy reaches the dev server locally while passkeyPublicOrigin matches the browser’s HTTPS origin—without opening the Node port on the LAN.
Cloudflare (D1 + R2)
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import { d1, r2 } from "@emdash-cms/cloudflare";
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
export default defineConfig({
output: "server",
adapter: cloudflare(),
image: {
layout: "constrained",
responsiveStyles: true,
},
integrations: [
react(),
emdash({
database: d1({ binding: "DB", session: "auto" }),
storage: r2({ binding: "MEDIA" }),
}),
],
devToolbar: { enabled: false },
});
Requires a wrangler.jsonc with D1 and R2 bindings:
{
"name": "my-site",
"compatibility_date": "2026-02-24",
"compatibility_flags": ["nodejs_compat"],
"assets": { "directory": "./dist" },
"d1_databases": [
{
"binding": "DB",
"database_name": "my-site",
"database_id": "", // from `wrangler d1 create my-site`
},
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "my-site-media",
},
],
}
Plugins
Register plugins in astro.config.mjs:
import { auditLogPlugin } from "@emdash-cms/plugin-audit-log";
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file" }),
plugins: [auditLogPlugin()],
}),
live.config.ts
Every EmDash site needs this file at src/live.config.ts. It's boilerplate -- the same in every project:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
This registers EmDash's live content collections with Astro. All content types are served through the single _emdash collection -- you query specific types using getEmDashCollection("posts") etc.
emdash-env.d.ts
Auto-generated at the project root when the dev server starts. Provides TypeScript types for your collections. This is the file your tsconfig.json includes.
/// <reference types="emdash/locals" />
import type { PortableTextBlock } from "emdash";
export interface Post {
id: string;
slug: string | null;
status: string;
title: string;
featured_image?: {
id: string;
src?: string;
alt?: string;
width?: number;
height?: number;
};
content?: PortableTextBlock[];
excerpt?: string;
createdAt: Date;
updatedAt: Date;
publishedAt: Date | null;
}
declare module "emdash" {
interface EmDashCollections {
posts: Post;
}
}
The dev server regenerates this file automatically when schema changes. You can also generate it manually:
Type Generation
# From local dev server (writes emdash-env.d.ts at project root)
npx emdash types
# From remote instance
npx emdash types --url https://my-site.pages.dev
# Custom output path
npx emdash types --output src/types/cms.ts
The CLI also writes .emdash/schema.json with the raw schema for tooling.
package.json
Key dependencies for a Node.js site:
{
"dependencies": {
"astro": "^6.0.0",
"emdash": "workspace:*",
"@astrojs/node": "^9.0.0",
"@astrojs/react": "^4.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
For Cloudflare, replace @astrojs/node with @astrojs/cloudflare and add @emdash-cms/cloudflare.
Dev Server
npx emdash dev # Start dev server (runs migrations, applies seed)
npx emdash dev --types # Start and generate types from schema
The admin UI is at http://localhost:4321/_emdash/admin. On first run, you'll go through setup to create an admin account.