Domma CMS User Manual

Scaffold API

Updated by Darryl Waterhouse on 29 September 2026 4 min read

The scaffolder builds a working system - collection, form, Actions and optionally a project, roles, users, menus, API tokens and API Builder endpoints - from a bundled recipe in one call. In the admin it is the "scaffold a working system in one click" panel in the page editor's CRUD shortcut slideover and on the Building a CRUD App tutorial. Full recipe format: docs/scaffolding.md.

Endpoints

GET /api/scaffold/recipes

Requires: collections.create

List the bundled recipes with just what a picker needs.

// Response 200
{ "recipes": [
  { "slug": "contact-list", "name": "Contact list (CRM-lite)", "description": "...", "icon": "...",
    "options": [
      { "name": "collectionSlug", "label": "Collection slug", "default": "contacts", "hint": "..." },
      { "name": "formSlug",       "label": "Form slug",       "default": "contact-quick-add" },
      { "name": "actionPrefix",   "label": "Action slug prefix", "default": "contact" }
    ] }
] }

GET /api/scaffold/recipes/:slug

Requires: collections.create

The whole recipe document, unresolved (placeholders such as {{collectionSlug}} still in place) - the preview of what apply will create. 404 for an unknown recipe. There is no separate dry-run endpoint.

POST /api/scaffold/apply

Requires: collections.create, plus the permission for everything the recipe creates (see Permissions)

Apply a recipe. options overrides the recipe's option defaults by name; anything omitted or blank keeps its default.

curl -X POST https://example.com/api/scaffold/apply \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{"recipe":"contact-list","options":{"collectionSlug":"leads","formSlug":"lead-form"}}'
// Response 200
{
  "created": {
    "collection": "leads",
    "form": "lead-form",
    "actions": ["contact-followup"],
    "roles": [], "users": [], "menus": [],
    "apiTokens": [],
    "apiEndpoints": []
  },
  "skipped": [],
  "warnings": [],
  "snippet": "[form name=\"lead-form\" /]\n\n## Contacts\n\n[collection slug=\"leads\" ...]"
}
// Error 400
{ "error": "recipe slug is required" }
// Error 400 - an option is not valid (an email that is not one, a password under 8 characters)
{ "error": "HR email (optional): \"hr\" is not an email address." }
// Error 403 - the caller may not create something the recipe creates
{ "error": "You cannot apply this recipe - it creates things you do not have permission to create: user accounts (needs users.create); actions (needs actions.create).",
  "missing": ["users.create", "actions.create"] }
// Error 409 - something the recipe would create already exists
{ "error": "Cannot apply recipe - conflicts: ...", "conflicts": ["Collection \"leads\" already exists"] }

snippet is Markdown ready to paste into a page - usually the form embed plus a collection display.

What apply creates

In this order, with every option value substituted into the recipe first:

  1. Checks - the permission check (403) and the seed accounts' email and password (400). Nothing has been written yet.
  2. Pre-flight - the collection, form, each Action and each menu must not exist yet; otherwise 409 with the full list and nothing below runs.
  3. Project - when the recipe has a project block, the namespace option is the project slug. Created if missing, left alone if it exists. A failure here aborts with 400.
  4. Roles - an existing role name is skipped with a warning (its permissions are not changed).
  5. Menus - written, and mapped to their locations slots unless a slot is already taken (warning) or the menu says force: true.
  6. Collection - schema, API access rules and rowAccess.
  7. Form - its collection action points at the new collection.
  8. Users - only when the recipe supplies a password (8 characters or more); an existing email or a missing password is skipped with a warning. The password is never logged or returned. Users in a project recipe get projects: [namespace].
  9. Actions - need MongoDB; without it each is skipped with the warning MongoDB not configured - action "..." skipped (Pro feature). When the form's settings.actionSlug is "", it is wired to the first Action created.
  10. API tokens (apiTokens: [{name, scopes?, expiresAt?}]) - bound to the recipe's project (else the namespace option, else core). The plaintext is returned once in created.apiTokens[].token; a token with the same name in that project is skipped and never re-issued. See External API.
  11. API endpoints (apiEndpoints: [{path, collection, filter, ...}]) - same project rule; a definition with the same path shape is skipped. See API Builder.

Everything a project recipe creates is tagged meta.project: <namespace>. Recipes cannot create pages, blocks or views.

Partial results

Apply is not a transaction. Pre-flight keeps the main pieces from colliding, but after it a failing role, menu, user, Action, token or endpoint becomes a warning and the rest carries on. Read skipped (entries such as role:hr, user:hr@example.com, apiToken:mobile, apiEndpoint:/latest or an Action slug) and warnings to see what did not land. A refusal from the checks or pre-flight writes nothing, the project included.

Recipes and options

Recipes are JSON files in server/services/recipes/ (bundled: contact-list and onboarding); a new file is listed on the next request. Each options[] entry has a name, type, label, default and hint. A default may refer to an earlier option ("default": "{{namespace}}-form").

  • {{optionName}} is replaced throughout the recipe at apply time.
  • Runtime placeholders such as {{entry.data.email}}, {{user.id}} and {{now}} are left for the Action to resolve when it runs.
  • An option's type decides what happens to the value you supply: slug (the default when a recipe gives none) is slugified - lower case, anything other than letters and digits becomes a hyphen; email is trimmed and must look like an address (400 otherwise); password is kept exactly as typed and never echoed; text is trimmed. The onboarding recipe's hrEmail and hrPassword are typed email and password, and the Apply template form shows them as email and password fields.

Permissions

All three endpoints need a signed-in user whose role holds collections.create. Apply then checks the permission for each kind of thing the chosen recipe will create, before it writes anything, and refuses with 403 naming every one that is missing (also listed in missing):

The recipe createsNeeds
a project (one that does not exist yet)projects.create
roles (ones that do not exist yet), user accountsusers.create
menus / menu slot mappingmenus.create / menus.update
a collection, a formcollections.create
Actionsactions.create
API tokensapi-tokens.create
API endpointsapi-endpoints.create

A recipe cannot be a way round the Users and Roles rules either: a role it creates must sit below your own level and grant only permissions you hold, and a user it creates must get a role below your level. The level-0 role passes every check.