Build a Custom Admin Interface

Build a Custom Admin Interface

ModulaCMS exposes a complete REST API for every administrative operation, so you can build a custom admin interface in any framework or language.

Authenticate your interface

Your interface must authenticate before calling admin endpoints. ModulaCMS supports two authentication methods.

Authenticate with email and password. The server returns an HTTP-only session cookie valid for 24 hours.

curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@example.com", "password": "your-password"}'

Include the session cookie in subsequent requests. See Authentication and access control for session management details.

API keys

For server-to-server or automated integrations, use an API key as a Bearer token:

curl http://localhost:8080/api/v1/media \
  -H "Authorization: Bearer mcms_01JMKX5V6QNPZ3R8W4T2YH9B0D"

API keys inherit the permissions of the user they belong to.

Check the current user

Verify the authenticated session and retrieve the user's role:

curl http://localhost:8080/api/v1/auth/me \
  -H "Cookie: session=YOUR_SESSION_COOKIE"
{
  "user_id": "01JMKW8N3QRYZ7T1B5K6F2P4HD",
  "email": "admin@example.com",
  "username": "admin",
  "name": "Admin User",
  "role": "admin"
}

Use the role to determine what the user can see and do. If the role is admin, the user has full access. Otherwise, query the role's permissions to enable or disable UI elements.

Common API patterns

All admin endpoints live under /api/v1. Request and response bodies use JSON. IDs are 26-character ULID strings.

Standard CRUD

Most resources follow the same endpoint structure:

Operation Method Path Example
List all GET /api/v1/{resource} /api/v1/datatype
Get one GET /api/v1/{resource}/?q={id} /api/v1/datatype/?q=01HXK...
Create POST /api/v1/{resource} /api/v1/datatype
Update PUT /api/v1/{resource}/?q={id} /api/v1/datatype/?q=01HXK...
Delete DELETE /api/v1/{resource}/?q={id} /api/v1/datatype/?q=01HXK...

The ?q= query parameter identifies the item for single-resource operations.

Paginate results

List endpoints support pagination via limit and offset query parameters:

curl "http://localhost:8080/api/v1/contentdata?limit=25&offset=50" \
  -H "Cookie: session=YOUR_SESSION_COOKIE"
  • Default limit: 50 items
  • Maximum limit: 1000 items
  • Without pagination parameters, the endpoint returns all items

Handle errors

All errors return a JSON body with an error field:

{"error": "not found"}
Status Meaning
400 Invalid input or missing required fields
401 No valid session or API key
403 Authenticated but missing required permission
404 Resource not found
409 Duplicate resource (e.g., same filename for media)
500 Internal server error

Set up the schema

Before creating content, define datatypes (content schemas) and fields (data definitions).

List available field types

Field types define the kind of data a field holds (text, number, image, etc.). ModulaCMS ships with built-in field types.

curl http://localhost:8080/api/v1/fieldtypes \
  -H "Cookie: session=YOUR_SESSION_COOKIE"

Create a field

Fields define the individual pieces of data within a datatype. Each field references a field type.

curl -X POST http://localhost:8080/api/v1/fields \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Title",
    "field_type_id": "01HXK4N2F8...",
    "required": true
  }'

Create a datatype

Datatypes define the structure of your content.

curl -X POST http://localhost:8080/api/v1/datatype \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Blog Post",
    "type": "page"
  }'

Fields are associated with a datatype via the parent_id field when creating the field:

curl -X POST http://localhost:8080/api/v1/fields \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Title",
    "type": "text",
    "parent_id": "01HXK4N2F8..."
  }'

Retrieve full datatypes

Fetch a datatype with all its linked fields in a single response:

curl http://localhost:8080/api/v1/datatype/full \
  -H "Cookie: session=YOUR_SESSION_COOKIE"

Manage content

Content entries are instances of a datatype. Each entry holds field values defined by its datatype's schema.

Create content

curl -X POST http://localhost:8080/api/v1/contentdata \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "datatype_id": "01HXK4N2F8...",
    "status": "draft",
    "author_id": "01JMKW8N3Q...",
    "parent_id": "",
    "first_child_id": "",
    "next_sibling_id": "",
    "prev_sibling_id": ""
  }'

Set content field values

After creating a content entry, set its field values:

curl -X POST http://localhost:8080/api/v1/contentfields \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "content_data_id": "01HXK4N2FA...",
    "field_id": "01HXK4N2F9...",
    "value": "My First Blog Post"
  }'

Batch update fields

Update multiple content fields in a single request:

curl -X POST http://localhost:8080/api/v1/content/batch \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "updates": [
      {"content_field_id": "01HXK...", "value": "Updated Title"},
      {"content_field_id": "01HXK...", "value": "Updated Body"}
    ]
  }'

Work with content trees

Content is organized in a tree structure with parent-child relationships.

Get the tree

Retrieve the assembled content tree:

curl http://localhost:8080/api/v1/content/tree/01HXK4N2F8... \
  -H "Cookie: session=YOUR_SESSION_COOKIE"

Save the tree

The tree save endpoint accepts batch creates, updates, and deletes in a single request:

curl -X POST http://localhost:8080/api/v1/content/tree \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "content_id": "01HXK4N2F8...",
    "creates": [
      {
        "client_id": "temp-uuid-1",
        "datatype_id": "01HXK4N2F8...",
        "parent_id": "01HXK4N2F9..."
      }
    ],
    "updates": [
      {
        "content_data_id": "01HXK4N2FA...",
        "parent_id": "01HXK4N2F8..."
      }
    ],
    "deletes": ["01HXK4N2FB..."]
  }'

The response includes an id_map that maps your temporary client IDs to server-generated ULIDs. Use this to update your local state after the save.

Reorder and move nodes

Reorder siblings within the same parent:

curl -X POST http://localhost:8080/api/v1/contentdata/reorder \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "content_data_id": "01HXK...",
    "prev_sibling_id": "01HXK...",
    "next_sibling_id": "01HXK..."
  }'

Move a node to a different parent:

curl -X POST http://localhost:8080/api/v1/contentdata/move \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "content_data_id": "01HXK...",
    "parent_id": "01HXK..."
  }'

Use the admin content API

ModulaCMS has a second, independent content system for admin panel configuration. Admin content endpoints mirror the public content endpoints with admin prefixes:

Public endpoint Admin equivalent
/api/v1/contentdata /api/v1/admincontentdatas
/api/v1/contentfields /api/v1/admincontentfields
/api/v1/datatype /api/v1/admindatatypes
/api/v1/fields /api/v1/adminfields
/api/v1/fieldtypes /api/v1/adminfieldtypes
/api/v1/routes /api/v1/adminroutes

Every operation shown in this guide works identically on admin endpoints. To create an admin screen, create an admin datatype via /api/v1/admindatatypes, attach admin fields via /api/v1/adminfields, and populate admin content via /api/v1/admincontentdatas.

Good to know: Admin content and public content are independent systems. Changing admin content does not affect your site's public content, and vice versa.

Create routes

Routes map URL slugs to content trees. Create routes to make content accessible:

curl -X POST http://localhost:8080/api/v1/routes \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "blog",
    "type": "page",
    "content_data_id": "01HXK4N2F8..."
  }'

List all routes with their associated content:

curl http://localhost:8080/api/v1/routes/full \
  -H "Cookie: session=YOUR_SESSION_COOKIE"

Manage media

Upload files

Media upload uses multipart/form-data:

curl -X POST http://localhost:8080/api/v1/media \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -F "file=@/path/to/image.jpg"

Organize media into folders

# Create a folder
curl -X POST http://localhost:8080/api/v1/media-folders \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{"name": "Photos", "parent_id": ""}'

# Get the full folder tree
curl http://localhost:8080/api/v1/media-folders/tree \
  -H "Cookie: session=YOUR_SESSION_COOKIE"

# Move media to a folder
curl -X POST http://localhost:8080/api/v1/media/move \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{"media_ids": ["01HXK...", "01HXK..."], "folder_id": "01HXK..."}'

Publish content

Content starts in draft status and must be published to appear on the public site.

# Publish content
curl -X POST http://localhost:8080/api/v1/content/publish \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{"content_data_id": "01HXK..."}'

# Unpublish content
curl -X POST http://localhost:8080/api/v1/content/unpublish \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{"content_data_id": "01HXK..."}'

Check permissions

Every admin endpoint requires a resource:operation permission. Query the user's role permissions to determine which actions to display:

curl "http://localhost:8080/api/v1/role-permissions/role/?q=ROLE_ID" \
  -H "Cookie: session=YOUR_SESSION_COOKIE"

If the user's role lacks a required permission, the API returns 403. Your interface should hide or disable actions the user cannot perform.

Good to know: The API supports CORS on auth and content delivery endpoints. For a custom admin interface on a different domain, add your domain to the CORS allowed origins in modula.config.json.

Good to know: Every create, update, and delete operation produces a change event in the audit trail with the authenticated user's ID, request ID, and IP address.