Contents
- Domma CMS User Manual
- 1. Using the CMS
- 2. Tutorials
- 3. Components
- 4. API Reference
- 5. Tools
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:
- Checks - the permission check (
403) and the seed accounts' email and password (400). Nothing has been written yet. - Pre-flight - the collection, form, each Action and each menu must not exist yet; otherwise
409with the full list and nothing below runs. - Project - when the recipe has a
projectblock, thenamespaceoption is the project slug. Created if missing, left alone if it exists. A failure here aborts with400. - Roles - an existing role name is skipped with a warning (its permissions are not changed).
- Menus - written, and mapped to their
locationsslots unless a slot is already taken (warning) or the menu saysforce: true. - Collection - schema, API access rules and
rowAccess. - Form - its collection action points at the new collection.
- 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]. - Actions - need MongoDB; without it each is skipped with the warning
MongoDB not configured - action "..." skipped (Pro feature). When the form'ssettings.actionSlugis"", it is wired to the first Action created. - API tokens (
apiTokens: [{name, scopes?, expiresAt?}]) - bound to the recipe's project (else thenamespaceoption, elsecore). The plaintext is returned once increated.apiTokens[].token; a token with the same name in that project is skipped and never re-issued. See External API. - 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
typedecides 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;emailis trimmed and must look like an address (400otherwise);passwordis kept exactly as typed and never echoed;textis trimmed. The onboarding recipe'shrEmailandhrPasswordare typedemailandpassword, 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 creates | Needs |
|---|---|
| a project (one that does not exist yet) | projects.create |
| roles (ones that do not exist yet), user accounts | users.create |
| menus / menu slot mapping | menus.create / menus.update |
| a collection, a form | collections.create |
| Actions | actions.create |
| API tokens | api-tokens.create |
| API endpoints | api-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.