AGENTS.md · emdash-cms/emdash
AGENTS.md from emdash-cms/emdash: 438 lines covering Workflow, Architecture, Database: Never Interpolate Into SQL, API Routes and more.
Template by emdash-cms
Install
Works with Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Claude Code. Read the repository before you give any tool access to your data.
curl -fsSL https://raw.githubusercontent.com/emdash-cms/emdash/main/AGENTS.md -o AGENTS.md
Read it and adapt it to your project before use.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: prompt-engineering, code-generation
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
What's in it
An AGENTS.md file: instructions coding agents (Codex, Cursor, Copilot, Gemini CLI and others) read before working in a repo.
- For
- Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Claude Code
- Length
- 438 lines
- File
AGENTS.md
Sections
- Workflow
- Architecture
- Database: Never Interpolate Into SQL
- API Routes
- Handler Layer
- Migrations
- Indexes
- Content Tables
- Content Localization
- Performance: Caching and Query Patterns
- Kumo Components
- Admin UI: Localization (Lingui)
- Admin UI: RTL-safe Tailwind
- Comments
- Imports
- Environment
- Cloudflare Env
First lines (shown under the repo's MIT license)
This file provides guidance to agentic coding tools working in this repository.
For human-facing contributor info (setup, repo layout, PR policy, i18n), see [CONTRIBUTING.md](CONTRIBUTING.md). This file focuses on the patterns and gotchas an agent needs to write correct code.
Follow [AI_POLICY.md](AI_POLICY.md) for external actions and project communication. In particular, do not open design PRs, and do not comment on issues, Discussions, or design PRs. On implementation PRs, post only a specifically requested code review or a reply to code review, and sign agent-written comments posted from a human account as the policy requires.
To check the autofix bot's status on an issue or PR, read `https://autofix.emdashcms.com/api/cases?issue=N`, where N is the issue or PR number.
`CLAUDE.md` is a symlink to this file. `.agents/skills` and `.claude/skills` are symlinks to `skills/`. Don't try to sync between them.
When writing, revising, or reviewing documentation, load the `writing-emdash-docs` skill. Use it for public docs, READMEs, contributor guidance, technical specifications, release notes and changesets, and skill instructions.
# Rules
**Backwards compatibility matters.** EmDash is published, in active use, and on 1.x. Prefer additive changes (new fields, new routes, new options with defaults). Breaking changes need an explicit decision, a major version bump, and a changeset that calls the break out clearly. Database migrations are forward-only -- never write one that leaves existing content inaccessible. When the compatibility decision is unclear, propose a Discussion instead of choosing a breaking design.
**Regression evidence for bugs.** A fix must demonstrate that it changes the reported behavior. Add a regression test when it can protect meaningful behavior; otherwise report the reproduction and verification. See [Testing](#testing).
**Localize everything user-facing.** All admin UI strings, aria labels, and toast messages go through Lingui. All admin layout uses RTL-safe logical Tailwind classes. See [Localization](#admin-ui-localization-lingui) and [RTL](#admin-ui-rtl-safe-tailwind).
**Scope discipline.** No drive-by refactors, no bulk lint/type cleanups, no "while I'm here" edits in unrelated files. Report systemic issues separately instead of expanding the current change. See [CONTRIBUTING.md § Contribution Policy](CONTRIBUTING.md#contribution-policy).
**Protect the logged-out hot path.** Any change that increases the query count of a logged-out route needs explicit justification -- and that includes cold-start or first-request-only queries, not just steady state. See [Performance](#performance-caching-and-query-patterns).
**Comments are for code readers, not reviewers.** Write a comment only when the code can't be made clearer, or to give a non-obvious "why". Never justification, narrative, or issue/PR references. See [Comments](#comments).
## Workflow
Main is kept green. Do not run checks before editing merely to establish a baseline, and do not dismiss validation failures as pre-existing. Before committing code:
- Run `pnpm format`.
- Run `pnpm build` from the repository root. Package-scoped builds are not sufficient because validation resolves declaration output from other workspace packages.
- Run `pnpm lint:quick` and confirm `pnpm --silent lint:json | jq '.diagnostics | length'` returns zero.
- Run the tests relevant to the changed behavior.
- Run `pnpm typecheck` (packages) or `pnpm typecheck:demos` (Astro demos).
If a check fails, make sure the root build is current and rerun it. Treat any remaining failure as introduced by the current work and fix it before committing.
Before opening a PR: tests pass, lint clean, formatted, changeset added if a published package changed. See [.changeset/README.md](.changeset/README.md).
A changeset is user-facing documentation that lands verbatim in a package CHANGELOG. Review its usefulness to someone upgrading, not only its presence and frontmatter. Follow [.changeset/README.md](.changeset/README.md) for the canonical writing and review standard, including proportional detail and migration guidance for default or breaking changes.
When opening an implementation PR with `gh`/the API, copy `.github/PULL_REQUEST_TEMPLATE.md` into the body and fill every section. When helping prepare a design PR for a human to open, use `.github/PULL_REQUEST_TEMPLATE/design.md` instead. The GitHub UI injects a template automatically but the CLI does not, and PRs missing one are auto-closed. Check the AI assistance disclosure box and name the model. Tick checklist items only for what you actually verified; for test-only/docs/CI PRs, note why changeset, i18n, screenshots, or design links are not applicable.
Issues that refer to the interface must include a screenshot that shows the reported state. PRs that change the UI must include before-and-after screenshots of every screen they affect; for a new screen, the after state is enough. Keep the behavior described in text and give every image useful alt text.
Lay out screenshots as [CONTRIBUTING.md § Interface screenshots](CONTRIBUTING.md#interface-screenshots) shows: each pair side by side in a two-column table, cropped to the changed area, at the same viewport and with the same content, and extra states such as Arabic in a collapsed `<details>` block.
Agents can attach local images with GitHub CLI 2.99.0 or later. The `--attach` flag is repeatable on `gh issue create|edit|comment` and `gh pr create|edit|comment`. For example:
```bash
gh issue create --body-file /tmp/emdash-issue.md \
--attach './interface-error.png#The settings screen showing the validation error'
gh pr create --body-file /tmp/emdash-pr.md \
--attach './before.png#Settings screen before the change' \
--attach './after.png#Settings screen after the change'
```
To place an image at a specific point in the body, add `` to the body file and pass `--attach ./after.png`; `gh` replaces the local path with the uploaded asset URL. An attachment that is not referenced in the body is appended. CLI `--attach` uploads require repository write access. See [CONTRIBUTING.md § Interface screenshots](CONTRIBUTING.md#interface-screenshots).
## Architecture
EmDash is an Astro-native CMS that deploys to Cloudflare Workers or Node.js. The database is SQLite, libSQL, or PostgreSQL on Node.js, and D1 or PostgreSQL via Hyperdrive on Workers. Media storage is an R2 binding on Workers, and S3-compatible storage or the local filesystem on Node.js.
- **Schema in the database.** `_emdash_collections` and `_emdash_fields` are the source of truth. Each collection gets a real SQL table (`ec_posts`, `ec_products`) with typed columns -- not EAV.
- **Middleware chain:** runtime init -> setup check -> auth -> request context (ALS). Auth middleware checks authentication only; routes check authorization.
- **Handler layer** (`packages/core/src/api/handlers/*.ts`) holds business logic and returns `ApiResult<T>` (`{ success: true, data } | { success: false, error: { code, message, details? } }`). Route files are thin wrappers.
- **Storage abstraction:** `Storage` interface with `upload/download/delete/exists/list/getSignedUploadUrl/getPublicUrl`. `LocalStorage` for a local directory, `S3Storage` for S3-compatible services (including R2's S3 API), `R2Storage` (in `@emdash-cms/cloudflare`) for R2 bindings. Access via `emdash.storage` from locals.
Key files:
| File | Purpose |
| ------------------------------------------------- | ----------------------------------------------------- |
| `packages/core/src/emdash-runtime.ts` | Central runtime; orchestrates DB, plugins, storage |
| `packages/core/src/schema/registry.ts` | Manages `ec_*` table creation/modification |
| `packages/core/src/database/migrations/runner.ts` | StaticMigrationProvider; register new migrations here |
| `packages/core/src/plugins/manager.ts` | Loads and orchestrates plugins |
# Code Patterns
## Database: Never Interpolate Into SQL
Kysely is the query builder.
- **Never** use `sql.raw()` with string interpolation or template literals containing variables.
- For **values**, use Kysely's `sql` tagged template -- interpolated values are automatically parameterized.
- For **identifiers** (table/column names), use `sql.ref()`.
- If you must use `sql.raw()` for dynamic identifiers, validate with `validateIdentifier()` from `database/validate.ts` first (asserts `/^[a-z][a-z0-9_]*$/`).
- The `json_extract(data, '$.${field}')` pattern is particularly dangerous -- always validate `field`.
```typescript
// WRONG -- SQL injection
const query = `SELECT * FROM ${table} WHERE name = '${name}'`;
await sql.raw(query).execute(db);
// RIGHT -- parameterized value, safe identifier
await sql`SELECT * FROM ${sql.ref(table)} WHERE name = ${name}`.execute(db);
// RIGHT -- validated identifier in raw SQL
validateIdentifier(field);
return sql.raw(`json_extract(data, '$.${field}')`);
```
## API Routes
Routes live in `packages/core/src/astro/routes/api/`. Conventions:
- Every route file starts with `export const prerender = false;`.
- Named exports: `export const GET: APIRoute`, etc. Destructure from the Astro context.
- Access runtime via `const { emdash } = locals;`, user via `locals.user`.
- File structure mirrors URLs: `content/[collection]/index.ts` for list/create, `[id].ts` for get/update/delete, sub-actions as siblings.
- **Never** add GET handlers for state-changing operations.
Use the shared utilities -- don't roll your own:
| Need | Use |
| --------------- | ---------------------------------------------------------------------------------------- |
| Error response | `apiError(code, message, status)` from `#api/error.js` |
| Catch block | `handleError(error, message, code)` -- never expose `error.message` to clients |
| Body validation | `parseBody(request, zodSchema)` from `#api/parse.js` -- never `as` cast `request.json()` |
| Unwrap handler | `unwrapResult(result)` -- maps error codes to HTTP statuses automatically |
| Init check | `if (!emdash) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);` |
The error helper is `mapErrorStatus`, not `mapErrorToStatus`.
### Authorization
Every state-changing route must check authorization. Authorization is permission-based, not role-based -- the `Permissions` map in `packages/auth/src/rbac.ts` is authoritative. Never invent permission strings in route files; add them to `rbac.ts` with a sensible minimum role.
```typescript
import { requirePerm, requireOwnerPerm } from "#api/authorize.js";
// Any-actor capability (settings, schema)
const denied = requirePerm(user, "schema:manage");
if (denied) return denied;
// Ownership-aware (authors edit their own; editors edit anyone's)
const denied = requireOwnerPerm(user, post.authorId, "content:edit_own", "content:edit_any");
if (denied) return denied;
```
Both helpers return `null` on success or a `Response` (401/403) to return directly.
### CSRF
All state-changing endpoints require the `X-EmDash-Request: 1` header, enforced by auth middleware. The admin UI and visual editing client send it automatically.
### Pagination
List endpoints return `{ items, nextCursor? }` -- never a bare array. Use `encodeCursor(orderValue, id)` / `decodeCursor(cursor)`. Default limit 50, max 100, always clamp. The repository-level shape is `FindManyResult<T>`.
### URL/Redirect handling
When accepting redirect URLs from query params or bodies: require leading `/`, reject `//`, HTML-escape before interpolation, prefer `Response.redirect()` over `<meta http-equiv="refresh">`.
## Handler Layer
Handlers in `api/handlers/*.ts` are standalone async functions, not class methods.Signals
- Stars
- 13,920
- Forks
- 1,332
- Open issues
- 183
- Last commit (this folder)
- Oct 21, 2026 (7 days ago)
- Last release
- Oct 18, 2026 (10 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/emdash-cms/emdash
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
Cite this page
AGENTS.md · emdash-cms/emdash. Agent Mart. https://agentmart.dev/templates/emdash-cms-emdash-agents-md (last updated 2026-10-24)