Skip to content

Ghost Schema & Proxies

Traditional CMS platforms require you to define a schema up front — you create content types, add fields, specify validation rules, and only then can you use them in your code. ACMS flips this entirely.

With ACMS, the schema is generated automatically based on how you access content in your code. We call this a Ghost Schema because it materializes from your code’s behavior rather than from explicit definitions.

// You write this in your component:
<h1>{acms.hero.title}</h1>
<p>{acms.hero.subtitle}</p>
<a href={acms.hero.ctaLink}>{acms.hero.ctaText}</a>

The moment this code runs, ACMS detects three fields: hero.title, hero.subtitle, hero.ctaLink, and hero.ctaText. They are registered in acms.json with empty values, ready for editing.

No schema file. No content model definition. No configuration panel. The code is the schema.

At the heart of ACMS is a JavaScript Proxy that intercepts property access on the acms object.

import { acms } from '@useacms/client';
// When you access acms.hero.title, the proxy:
// 1. Intercepts the property access
// 2. Checks if 'hero.title' has a value
// 3. If not, POSTs to the dev server to register the field
// 4. Returns the current value (or empty string)
const title = acms.hero.title;

The proxy is recursive — accessing acms.hero returns another proxy, so acms.hero.title triggers two interceptions. The system tracks the full path (hero.title) to register the correct nested field.

Your Code Client Proxy Dev Server (port 3001)
───────── ──────────── ──────────────────────
acms.hero.title ──────────► Intercepts access
├─ Field exists? ──► Yes ──► Return value
└─ Field missing? ──► POST /api/register ──► Creates field in acms.json
Regenerates acms.d.ts
◄── Return empty value

In production, there is no dev server. The client fetches content from your configured storage adapter (GitHub, Vercel Edge Config, Cloudflare KV, etc.) and the proxy returns values from the fetched content object.

Your Code Client Proxy Storage Adapter
───────── ──────────── ───────────────
acms.hero.title ──────────► Intercepts access
├─ Content loaded? ──► Yes ──► Return value from cache
└─ Not loaded? ──► Fetch from adapter ──► Cache and return

When the dev server receives a registration request, it performs several steps:

  1. Creates the field in acms.json at the specified path with an empty value.
  2. Adds metadata in the _meta section with the field type and access timestamp.
  3. Handles nesting — if hero already exists as a string and you access hero.title, the system migrates hero to an object with a displayName property preserving the original value.
  4. Regenerates typesacms.d.ts is updated with the new field definition.

Multiple fields may be registered simultaneously when a page renders. The dev server uses a promise queue to handle registrations sequentially, preventing race conditions when writing to acms.json.

Two field names are reserved and cannot be used as content fields:

  • displayName — Used internally for the field migration system
  • _meta — Stores field metadata (types, timestamps, labels)