How to Add Custom Fields in EmDash CMS

How to Add Custom Fields in EmDash CMS

Adding a custom field to a collection you've already got content in is one of the more common day-two EmDash tasks — a new "featured" toggle, an SEO override field, a rating. This covers the three ways to do it and what actually happens underneath.

Table of Contents
  1. What Actually Happens When You Add a Field
  2. Option 1: The Admin UI
  3. Option 2: The CLI
  4. Option 3: The REST API Directly
  5. Choosing the Right Field Type
  6. Reserved Field Slugs You Can't Use
  7. Removing a Field (And Why It's Irreversible)
  8. Frequently Asked Questions
  9. Can I change a field's type after content already exists?
  10. Will a non-technical editor accidentally break the schema?
  11. Does adding a field require a redeploy?
  12. How do I see what fields a collection already has before adding more?
  13. The Bottom Line

What Actually Happens When You Add a Field

Adding a field through the admin UI runs three steps: insert a record into _emdash_fields, run ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>, and regenerate the Zod schema used for validation.

That's a real, live schema change to a real database column — not a JSON blob getting a new key. It's also why field types map to specific SQLite column types (TEXT, REAL, INTEGER, JSON), and why the operation is instant: SQLite supports adding a column at runtime with no migration downtime, and existing rows simply get `NULL` (or the field's `defaultValue`) for the new column.

Option 1: The Admin UI

  1. Go to Content Types (the schema builder) in the admin panel.
  2. Open the collection you want to add a field to.
  3. Add a field, choose its type, and set validation/options as needed.
  4. Save — the field is live immediately, and existing content editors will see it on their next visit to that collection.
Read also:

Option 2: The CLI

npx emdash schema add-field posts featured --type boolean --required
npx emdash schema add-field posts subtitle --type string --label "Subtitle"
npx emdash schema add-field posts body --type portableText --label "Body Content"
emdashkits.com

Useful for scripting a repeatable setup, or for changing a deployed site's schema from your machine without opening the admin UI — the CLI's `schema` commands talk to the same REST API a running instance exposes, so they work identically against local dev or a live deployment (pass `--url` to target a deployed site).

Option 3: The REST API Directly

POST /_emdash/api/schema/collections/posts/fields
Content-Type: application/json

{
  "slug": "rating",
  "label": "Rating",
  "type": "number",
  "required": false,
  "validation": { "min": 1, "max": 5 }
}
emdashkits.com

The full set of field-management endpoints, for reference:

GET    /_emdash/api/schema/collections/:slug/fields
POST   /_emdash/api/schema/collections/:slug/fields
PUT    /_emdash/api/schema/collections/:collectionSlug/fields/:fieldSlug
DELETE /_emdash/api/schema/collections/:collectionSlug/fields/:fieldSlug
POST   /_emdash/api/schema/collections/:slug/fields/reorder
emdashkits.com

The reorder endpoint takes a full ordered array of field slugs — useful for cleaning up a collection's admin editor layout after several fields have been bolted on over time in a less-than-ideal order:

{ "fieldSlugs": ["title", "content", "author", "publishedAt"] }
emdashkits.com

Choosing the Right Field Type

A quick decision guide for the most common "which type do I actually want" questions:

  • A short label or name → string, not text.
  • A paragraph of plain description → text.
  • Rich formatted content with headings/links/images → portableText.
  • A yes/no toggle → boolean, not select with two options.
  • A fixed set of choices, pick one → select.
  • A fixed set of choices, pick several → multiSelect.
  • Linking to another collection's entry → reference (add allowMultiple: true in options for a many-relationship).
  • Genuinely unstructured, dynamic data → json, used sparingly since it has no validation.

Reserved Field Slugs You Can't Use

A specific, easy-to-hit error: certain slugs are reserved by EmDash's own system columns and will be rejected if you try to add a field with that exact name:

  • id, slug, status, author_id, primary_byline_id
  • created_at, updated_at, published_at, scheduled_at, deleted_at
  • version, live_revision_id, draft_revision_id
  • terms, bylines, byline

If you're porting a field from another platform that happens to use one of these names (a WordPress custom field literally named "status", for instance), rename it — `content_status` or similar — rather than fighting the collision.

Removing a Field (And Why It's Irreversible)

npx emdash schema remove-field posts legacy_field
emdashkits.com
Removing a field deletes its column and data. Test schema changes against a preview environment before running them against production.

There's no undo — the column and every value it held are gone. If there's any chance you'll want that data back, export a backup (`npx emdash export-seed --with-content posts`) before removing the field, not after.

Frequently Asked Questions

Can I change a field's type after content already exists?

Yes, though EmDash rebuilds the underlying table to do it (create a new table, copy rows, drop the old one, rename) since SQLite doesn't support changing a column's type in place. Review existing values first — a type change that can't cleanly convert existing data (free text into a strict `select` list, for instance) can produce validation conflicts.

Will a non-technical editor accidentally break the schema?

Only Admin-role users can create, modify, or delete collections and fields — Editor and below can use existing fields but can't change the schema itself, so a content editor with a lower role literally can't perform this operation by accident.

Does adding a field require a redeploy?

No — it's a live database change, effective immediately in the admin panel and in any query that reads the collection. No rebuild or redeploy needed, the same "instant" property that applies to content edits.

How do I see what fields a collection already has before adding more?

`npx emdash schema get posts`, or `GET /_emdash/api/schema/collections/posts?includeFields=true` — both return the current, live field list rather than relying on memory or stale documentation.

The Bottom Line

Adding a field to an existing EmDash collection is a real, instant, live schema change — three equivalent ways to do it (admin UI, CLI, REST API) depending on whether you want a form, a script, or programmatic control. Watch for the reserved-slug list, and back up before removing a field, since that specific operation has no undo. See our broader guide to customizing content models for building a whole new collection from scratch.

Share

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

TL;DR — A custom session-cookie login flow appeared to succeed on localhost (the OTP verified, the response looked fine) but every subsequent request to a login-gated page treated the visitor as logged out. Identical code worked fine on the live HTTPS site. The cookie's Secure attribute was hardcoded to true — and per the cookie spec, browsers never store or send a Secure cookie over a plain, non-HTTPS connection.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Fix
  4. Lessons learned

Diagnostic

Check the actual Set-Cookie response header and the browser's own cookie storage panel — on localhost over http://, the cookie is sent by the server but never actually stored by the browser.

Root cause

// before -- assumes the app is always served over HTTPS
setCookie("session", token, { secure: true, httpOnly: true });
emdashkits.com

A cookie config that quietly assumes "we're always on HTTPS" breaks the instant you test over plain HTTP, which local dev servers commonly are.

Read also:

Fix

// after -- derive secure from the actual request protocol
const isHttps = request.url.startsWith("https://");
setCookie("session", token, { secure: isHttps, httpOnly: true });
emdashkits.com

Lessons learned

  • Any Secure-flagged cookie needs to key off the real request scheme, not an assumption baked in once at cookie-creation time.
  • "Works in production, silently fails in local dev" is a strong signal to check cookie flags before anything else in an auth flow.
  • Check other cookies in the same codebase for the same hardcoded assumption — if one cookie has this bug, sibling cookies set the same way are worth auditing too.
Share
Previous Article

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Comments

Write a comment

Related Articles

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

TL;DR — GA4's gtag.js snippet was installed correctly, the page loaded with no visible JavaScript error, and GA4's real-time report still showed zero activity. The CMS's default Content-Security-Policy had no allowance for analytics domains and no config option to add one — so every request to Google's tracking endpoints was blocked at the browser level before it could fail loudly.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Lessons learned

Diagnostic

Check the browser's dedicated CSP violation reporting, not the regular console error list — CSP blocks are reported through their own channel, not thrown as normal script errors, so "no console errors" doesn't mean nothing was blocked.

Root cause

The CSP's script-src and connect-src directives had no entry for googletagmanager.com or google-analytics.com, and the CMS exposed no configuration surface to add one — the only way in was patching the CSP directives directly.

// patch-package: add analytics domains to the existing CSP directives
scriptSrc.push("https://www.googletagmanager.com");
connectSrc.push("https://www.google-analytics.com", "https://www.googletagmanager.com");
emdashkits.com
Read also:

Lessons learned

  • "No console errors" is not proof nothing was blocked — CSP violations live in their own reporting surface and are easy to miss if you're only scanning for red error text.
  • Before adding any third-party script tag to a site with a CSP already in place, check the CSP's directives first rather than assuming a silently-empty analytics dashboard means a snippet-installation mistake.
Share
Previous Article

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Next Article

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)