Skip to content

Dynamic Collections

Dynamic collections let editors define new content types from the Studio UI. No code changes, no redeploys, no caret.config.ts edits. Useful when you don’t know all your collections at build time, or when non-developers need to add content surfaces.

Use caseBest fit
Adding products, team, faqs to an existing site without redeployingDynamic
Letting clients add their own content typesDynamic
Stable schema known at build timeRegistered
Truly ad-hoc data, schema unknownInferred

You can mix all three in the same site. Registered collections take priority; dynamic ones appear alongside them.

Send a create_collection mutation through the API:

curl
curl -X POST http://localhost:4321/api/cms/mutate \
-H 'Content-Type: application/json' \
-H 'x-caret-request: 1' \
-H 'Cookie: caret_session=<your-token>' \
-d '{
"type": "create_collection",
"id": "products",
"label": "Products",
"icon": "📦",
"creatable": true,
"orderable": true,
"schema": {
"type": "object",
"properties": {
"name": { "type": "string", "title": "Product name" },
"price": { "type": "number", "minimum": 0, "title": "Price (USD)" }
},
"required": ["name", "price"]
}
}'

Field-level options the schema builder will support when shipped:

  • id — lowercase, alphanumeric, hyphens or underscores. Becomes the collection name in code.
  • label — human-readable name shown in Studio.
  • icon — optional emoji.
  • creatable — whether editors can add new entries (default: true).
  • orderable — whether entries can be drag-reordered (default: false).
  • schema — JSON Schema. See Schemas — JSON Schema features supported for what’s understood.

The adapter writes the metadata, creates an empty collection directory (filesystem) or initializes an empty index (KV), and the new collection appears on /admin/cms.

Once created, the collection behaves identically to a registered one in Studio. The schema you defined drives form rendering, validation, and the template for new entries.

Send a delete_collection mutation through the API (no UI button yet — destructive, kept gated behind the API):

curl
curl -X POST http://localhost:4321/api/cms/mutate \
-H 'Content-Type: application/json' \
-H 'x-caret-request: 1' \
-H 'Cookie: caret_session=<your-token>' \
-d '{ "type": "delete_collection", "id": "products" }'

Dynamic collections work the same as any other collection through the live loader:

src/caret.config.ts
import { defineLiveCollection } from 'astro:content';
import { caretLoader } from '@caretcms/core';
const products = defineLiveCollection({
loader: caretLoader('products'),
});
export const collections = { products };

Then use getLiveEntry('products', '...') from astro:content.

Where the metadata lives depends on your storage adapter.

  • Directory.caret/data/
    • Directoryproducts/
      • widget-001.json entries
  • Directory.caretcms/
    • Directorycollections/
      • products.json CollectionMetadata

The schema builder UI is intentionally focused on the JSON Schema features CaretCMS understands. See Schemas — JSON Schema features supported for the full table.

Dynamic collections are stored as JSON Schema. Once a collection’s shape stabilizes, you can promote it to a registered schema:

  1. Read the metadata: GET /api/cms/collections-metadata.
  2. Copy the schema object into your config:
    caret({
    schemas: {
    products: {/* the JSON Schema you copied */},
    },
    })
  3. Optionally delete the dynamic metadata via delete_collection. The entries will go too — make sure you’ve migrated them first by reading and re-writing under the registered schema.