Admin SDK

Admin SDK

Use @modulacms/admin-sdk for full CRUD access to every ModulaCMS API resource from admin panels, automation scripts, CI/CD pipelines, and data migration tools.

Creating a Client

import { createAdminClient } from '@modulacms/admin-sdk'

const client = createAdminClient({
  baseUrl: 'https://cms.example.com',
  apiKey: process.env.CMS_API_KEY,
})

See Getting Started for ClientConfig options.

CrudResource Pattern

Most resources on the admin client share the same generic interface:

type CrudResource<Entity, CreateParams, UpdateParams, Id = string> = {
  list:          (opts?: RequestOptions) => Promise<Entity[]>
  get:           (id: Id, opts?: RequestOptions) => Promise<Entity>
  create:        (params: CreateParams, opts?: RequestOptions) => Promise<Entity>
  update:        (params: UpdateParams, opts?: RequestOptions) => Promise<Entity>
  remove:        (id: Id, opts?: RequestOptions) => Promise<void>
  listPaginated: (params: PaginationParams, opts?: RequestOptions) => Promise<PaginatedResponse<Entity>>
  count:         (opts?: RequestOptions) => Promise<number>
}

Every CRUD resource provides these seven methods. listPaginated returns a PaginatedResponse<Entity> envelope with data, total, limit, and offset fields.

URL Patterns

The underlying HTTP calls follow a consistent pattern:

Method HTTP URL
list GET /api/v1/{resource}
get GET /api/v1/{resource}/?q={id}
create POST /api/v1/{resource}
update PUT /api/v1/{resource}/
remove DELETE /api/v1/{resource}/?q={id}
listPaginated GET /api/v1/{resource}?limit={n}&offset={n}

RequestOptions

Every method accepts an optional RequestOptions parameter:

type RequestOptions = {
  signal?: AbortSignal
}

The client merges your abort signal with its default timeout signal. Either one aborting cancels the request.

Authentication

// Login
const response = await client.auth.login({
  email: 'admin@example.com' as Email,
  password: 'secret',
})
// response: { user_id, email, username, created_at }

// Get current user
const me = await client.auth.me()
// me: { user_id, email, username, name, role }

// Logout
await client.auth.logout()

// Register
const user = await client.auth.register({
  username: 'newuser',
  name: 'New User',
  email: 'new@example.com' as Email,
  password: 'strongpassword',
  role: 'editor',
  date_created: new Date().toISOString(),
  date_modified: new Date().toISOString(),
})

Content Data

// Standard CRUD
const nodes = await client.contentData.list()
const node = await client.contentData.get(contentId)

// Create a node with fields in one request
const result = await client.contentData.createWithFields({
  datatype_id: datatypeId,
  parent_id: parentId,
  route_id: routeId,
  fields: { title: 'Hello World', body: '<p>Content</p>' },
})

// Reorder siblings
await client.contentData.reorder({
  parent_id: parentId,
  ordered_ids: [id1, id2, id3],
})

// Move to a new parent
await client.contentData.move({
  node_id: nodeId,
  new_parent_id: newParentId,
  position: 0,
})

// Batch update content data + field values
await client.contentData.batch({
  content_data_id: contentId,
  fields: { [fieldId]: 'updated value' },
})

// Recursive delete
const deleteResult = await client.contentData.deleteRecursive(rootId)
// deleteResult: { deleted_root, total_deleted, deleted_ids }

Content Tree Save

Apply creates, deletes, and tree structure updates atomically in a single HTTP request. This is the preferred method for persisting structural changes from a block editor or tree manipulation UI.

const result = await client.contentTree.save({
  content_id: rootContentId,
  creates: [
    {
      client_id: 'temp-1',
      datatype_id: datatypeId,
      parent_id: rootContentId,
      first_child_id: null,
      next_sibling_id: null,
      prev_sibling_id: null,
    },
  ],
  updates: [
    {
      content_data_id: existingNodeId,
      parent_id: rootContentId,
      first_child_id: 'temp-1', // references new node by client ID
      next_sibling_id: null,
      prev_sibling_id: null,
    },
  ],
  deletes: [oldNodeId],
})

// result.id_map maps client IDs to server-generated ULIDs
const serverId = result.id_map?.['temp-1']

Datatypes and Fields

// List datatypes
const datatypes = await client.datatypes.list()

// Get a fully composed datatype with field definitions
const full = await client.datatypes.getFull(datatypeId)
// full: { datatype_id, name, label, type, fields: Field[] }

// Cascade delete -- removes the datatype and all content nodes using it
const result = await client.datatypes.deleteCascade(datatypeId)
// result: { deleted_datatype_id, content_deleted, errors }

// Sort order management
await client.datatypes.updateSortOrder(datatypeId, 5)
const maxOrder = await client.datatypes.maxSortOrder(parentId)

Media

// Standard CRUD
const media = await client.media.list()
const asset = await client.media.get(mediaId)

// Upload a file
const uploaded = await client.mediaUpload.upload(file, {
  path: 'products/shoes', // optional S3 key prefix
})

// Health check -- find orphaned S3 objects
const health = await client.media.health()
// health: { total_objects, tracked_keys, orphaned_keys, orphan_count }

// Clean up orphaned files
const cleanup = await client.media.cleanup()

// Find content fields referencing a media asset
const refs = await client.media.getReferences(mediaId)

// Delete with reference cleanup
await client.media.deleteWithCleanup(mediaId)

Admin Media

Admin media items are stored in a separate bucket and power the admin panel UI. The API mirrors the public media resources.

// List and get admin media
const adminMedia = await client.adminMedia.list()
const adminAsset = await client.adminMedia.get(adminMediaId)

// Upload a file to admin media
const uploaded = await client.adminMediaUpload.upload(file)

// Update admin media metadata
await client.adminMedia.update({ admin_media_id: adminMediaId, alt: 'Logo' })

// Delete admin media
await client.adminMedia.remove(adminMediaId)

Admin Media Folders

// Get the full admin media folder tree
const tree = await client.adminMediaFolders.tree()

// List media in an admin folder
const folderMedia = await client.adminMediaFolders.listMedia(folderId, {
  limit: 20,
  offset: 0,
})

// Move admin media items to a folder (or null for root)
await client.adminMediaFolders.moveMedia({
  media_ids: [id1, id2],
  folder_id: targetFolderId,
})

Users

// List users with role labels
const users = await client.users.listFull()

// Get a fully composed user profile
const fullUser = await client.users.getFull(userId)
// Includes: oauth, ssh_keys, sessions, tokens (all safe views)

// Reassign content and delete user
const result = await client.users.reassignDelete({
  user_id: targetUserId,
  reassign_to: newOwnerId,
})

Roles and Permissions

// Manage roles
const roles = await client.roles.list()
const role = await client.roles.create({ label: 'moderator' })

// Manage permissions
const perms = await client.permissions.list()

// Role-permission associations
const assocs = await client.rolePermissions.list()
const byRole = await client.rolePermissions.listByRole(roleId)
await client.rolePermissions.create({
  role_id: roleId,
  permission_id: permId,
})
await client.rolePermissions.remove(assocId)

Publishing and Versioning

// Publish content (creates a version snapshot)
await client.publishing.publish({
  content_data_id: contentId,
  locale: 'en',
})

// Unpublish
await client.publishing.unpublish({ content_data_id: contentId })

// Schedule future publication
await client.publishing.schedule({
  content_data_id: contentId,
  publish_at: '2026-04-01T00:00:00Z',
})

// List version history
const versions = await client.publishing.listVersions(contentId)

// Restore to a previous version
await client.publishing.restore({
  content_data_id: contentId,
  content_version_id: versionId,
})

// Admin content has a separate publishing resource
await client.adminPublishing.publish({
  admin_content_data_id: adminContentId,
})

Content Delivery

The admin SDK also includes content delivery for fetching rendered content trees:

import type { Slug, ContentFormat } from '@modulacms/types'

const tree = await client.contentDelivery.getPage(
  'about' as Slug,
  'clean' as ContentFormat,
  'en',
)

Admin Tree

Fetch the full admin content tree for an admin route:

const tree = await client.adminTree.get('settings' as Slug)
// tree: { route: AdminRoute, tree: ContentTreeNode[] }

// With format conversion
const formatted = await client.adminTree.get('settings' as Slug, 'contentful')

Plugins

// List installed plugins
const plugins = await client.plugins.list()

// Get detailed plugin info
const info = await client.plugins.get('my-plugin')

// Lifecycle management
await client.plugins.reload('my-plugin')
await client.plugins.enable('my-plugin')
await client.plugins.disable('my-plugin')

// Cleanup orphaned plugin tables
const dryRun = await client.plugins.cleanupDryRun()
if (dryRun.count > 0) {
  await client.plugins.cleanupDrop({
    confirm: true,
    tables: dryRun.orphaned_tables,
  })
}

// Plugin route approval
const routes = await client.pluginRoutes.list()
await client.pluginRoutes.approve([{ plugin: 'my-plugin', method: 'GET', path: '/custom' }])

// Plugin hook approval
const hooks = await client.pluginHooks.list()
await client.pluginHooks.approve([{ plugin: 'my-plugin', event: 'after_create', table: 'content_data' }])

Deploy

// Health check
const health = await client.deploy.health()
// health: { status, version, node_id }

// Export CMS data
const payload = await client.deploy.export()

// Export specific tables
const partial = await client.deploy.export(['content_data', 'content_fields'])

// Import (dry run)
const dryResult = await client.deploy.importPayload(payload, true)

// Import (apply)
const result = await client.deploy.importPayload(payload, false)

Webhooks

// CRUD
const webhooks = await client.webhooks.list()
const wh = await client.webhooks.create({
  name: 'Deploy Notification',
  url: 'https://hooks.example.com/deploy',
  events: ['content.published', 'content.updated'],
  is_active: true,
})

// Test delivery
const testResult = await client.webhooks.test(webhookId)

// Delivery history
const deliveries = await client.webhooks.listDeliveries(webhookId)

// Retry a failed delivery
await client.webhooks.retryDelivery(deliveryId)

Locales

// CRUD
const locales = await client.locales.list()
const locale = await client.locales.create({
  code: 'fr',
  label: 'French',
  is_default: false,
  is_enabled: true,
  sort_order: 1,
})

// Create translations for a content node
const result = await client.locales.createTranslation(contentDataId, {
  locale: 'fr',
})
// result: { locale, fields_created }

Content Query

const result = await client.query.query('blog-post', {
  sort: '-published_at',
  limit: 10,
  filters: { category: 'news' },
})

See Read-Only SDK for QueryParams details.

Configuration

// Get current config (sensitive fields redacted)
const config = await client.config.get()

// Update config fields
const result = await client.config.update({
  port: 8080,
  cors_allowed_origins: '*',
})
if (result.restart_required?.length) {
  console.log('Restart required for:', result.restart_required)
}

// Get field metadata for building config UIs
const meta = await client.config.meta()
// meta: { fields: ConfigFieldMeta[], categories: string[] }

Import

Import content from external CMS platforms:

// Platform-specific imports
await client.import.contentful(exportData)
await client.import.sanity(exportData)
await client.import.strapi(exportData)
await client.import.wordpress(exportData)
await client.import.clean(exportData)

// Dynamic format
await client.import.bulk('contentful', exportData)

Content Heal

Scan and repair structural inconsistencies in the content tree:

// Dry run -- preview repairs without changes
const report = await client.contentHeal.heal(true)

// Apply repairs
const result = await client.contentHeal.heal(false)
// result: { dry_run, content_data_scanned, content_data_repairs, ... }

Sessions and SSH Keys

// Sessions (created via login, managed here)
await client.sessions.update(sessionParams)
await client.sessions.remove(sessionId)

// SSH keys
const keys = await client.sshKeys.list()
const key = await client.sshKeys.create({
  public_key: 'ssh-ed25519 AAAA...',
  label: 'My Laptop',
})
await client.sshKeys.remove(keyId)