Beta. EmDash is published to npm. During development you work inside the monorepo -- packages use
workspace:*links, so everything "just works" without publishing.
- Node.js 22+
- pnpm 10+ (
corepack enableif you don't have it) - Git
git clone <repo-url> && cd emdash
pnpm install
pnpm build # build all packages (required before first run)The demos/simple/ app is the primary development target. It is kept in sync with templates/blog/ and uses Node.js + SQLite — no Cloudflare account needed.
pnpm --filter emdash-demo seed # seed sample content
pnpm --filter emdash-demo dev # http://localhost:4321Open the admin at http://localhost:4321/_emdash/admin.
In dev mode, passkey auth is bypassed automatically. If you hit the login screen, visit:
http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
demos/cloudflare/ runs on the real workerd runtime with D1. See its README for setup.
Templates in templates/ are workspace members and can be run directly:
# First time: set up database and seed content
pnpm --filter @emdash-cms/template-portfolio bootstrap
# Run the dev server
pnpm --filter @emdash-cms/template-portfolio devAvailable templates:
| Template | Filter Name |
|---|---|
| Blog | @emdash-cms/template-blog |
| Portfolio | @emdash-cms/template-portfolio |
| Marketing | @emdash-cms/template-marketing |
Edit files in templates/{name}/src/ and changes hot reload.
Cloudflare variants (*-cloudflare) share source with their base templates via scripts/sync-cloudflare-templates.sh. Run that script after editing base template shared files.
Demo/template sync is handled by scripts/sync-blog-demos.sh:
- Full sync:
templates/blog->demos/simple - Frontend sync (keep runtime-specific config/files):
templates/blog-cloudflare->demos/cloudflaretemplates/blog-cloudflare->demos/previewtemplates/blog->demos/postgres
To start fresh, delete the database and re-bootstrap:
rm templates/portfolio/data.db
pnpm --filter @emdash-cms/template-portfolio bootstrapFor iterating on core packages alongside the demo, run two terminals:
# Terminal 1 — rebuild packages/core on change
pnpm --filter emdash dev
# Terminal 2 — run the demo
pnpm --filter emdash-demo devChanges to packages/core/src/ will be picked up by the demo's dev server automatically.
Run these before committing:
pnpm typecheck # TypeScript (packages)
pnpm typecheck:demos # TypeScript (Astro demos)
pnpm --silent lint:quick # fast lint (< 1s) — run often
pnpm --silent lint:json # full type-aware lint (~10s) — run before commits
pnpm format # auto-format with oxfmtType checking must pass. Lint must pass. Don't commit with known failures.
pnpm test # all packages
pnpm --filter emdash test # core only
pnpm --filter emdash test --watch # watch mode
pnpm test:e2e # Playwright (requires demo running)Tests use real in-memory SQLite — no mocking. Each test gets a fresh database.
emdash/
├── packages/
│ ├── core/ # emdash — the main package (Astro integration + APIs + admin)
│ ├── auth/ # @emdash-cms/auth — passkeys, OAuth, magic links
│ ├── admin/ # @emdash-cms/admin — React admin SPA
│ ├── cloudflare/ # @emdash-cms/cloudflare — CF adapter + plugin sandbox
│ ├── create-emdash/ # create-emdash — project scaffolder
│ ├── gutenberg-to-portable-text/ # WP block → Portable Text converter
│ └── plugins/ # first-party plugins (each dir = package)
├── demos/
│ ├── simple/ # emdash-demo — primary dev/test app (Node.js + SQLite)
│ ├── cloudflare/ # Cloudflare Workers demo (D1)
│ ├── plugins-demo/ # plugin development testbed
│ └── ...
├── templates/ # starter templates (blog, portfolio, marketing + cloudflare variants)
├── docs/ # public documentation site (Starlight)
└── e2e/ # Playwright test fixtures
The main package is packages/core. Most of your work will happen there.
The easiest way to build a real site during development is to add it as a workspace member.
-
Copy
templates/blog/(ortemplates/blank/) intodemos/:cp -r templates/blog demos/my-site
-
Edit
demos/my-site/package.json— set a uniquenamefield. -
Run
pnpm installfrom the root to link workspace dependencies. -
Start developing:
pnpm --filter my-site dev
Your site will use workspace:* links to the local packages, so any changes you make to core will be reflected immediately (with watch mode).
- Schema lives in the database, not in code.
_emdash_collectionsand_emdash_fieldsare the source of truth. - Real SQL tables per collection (
ec_posts,ec_products), not EAV. - Kysely for all queries. Never interpolate into SQL -- see
AGENTS.mdfor the full rules. - Handler layer (
api/handlers/*.ts) holds business logic. Route files are thin wrappers. - Middleware chain: runtime init -> setup check -> auth -> request context.
- Create
packages/core/src/database/migrations/NNN_description.ts(zero-padded sequence number). - Export
up(db)anddown(db)functions. - Register it in
packages/core/src/database/migrations/runner.ts— migrations are statically imported, not auto-discovered (Workers bundler compatibility).
- Create the file in
packages/core/src/astro/routes/api/. - Start with
export const prerender = false;. - Use
apiError(),handleError(),parseBody()from#api/. - Check authorization with
requirePerm()on all state-changing routes. - Register the route in
packages/core/src/astro/integration/routes.ts.
- Branch from
main. - Commit messages: describe why, not just what.
- Ensure
pnpm typecheckandpnpm --silent lint:jsonpass before pushing. - Run relevant tests.
These are known gaps -- don't try to fix them unless specifically asked:
- Rate limiting -- no brute-force protection on auth endpoints
- Password auth -- passkeys + magic links + OAuth only, by design
- Plugin marketplace -- architecture exists, runtime installation is post-beta
- Real-time collaboration -- planned for v1
- Read
AGENTS.mdfor architecture and code patterns - Check the documentation site for guides and API reference
- Open an issue or ask in the chat