Now with AI-powered page building via MCP Server

GraphQL delivery API

Which queries exist, how workspace scoping works, and which reads the SDK wraps versus which you write yourself.

cmssy serves published content over a single GraphQL endpoint. The SDK already wraps the common reads - pages, layouts, site config, forms - so most apps never write a query. For anything else (custom models, records, listing child pages) you send your own through the delivery client.

Endpoint and scoping

Public reads go to the org-scoped path:

{apiBase}/public/{orgSlug}/{workspaceSlug}/graphql

apiBase is your apiUrl with its trailing /graphql stripped - by default https://api.cmssy.io, so requests land on https://api.cmssy.io/public/{org}/{ws}/graphql. Override apiUrl only when self-hosting. org and workspaceSlug come from your config, and the SDK assembles the path for you.

Because the org sits in the path, a workspace slug only has to be unique within its organization.

Two ways to scope a query

Every operation is workspace-scoped, but not all of them the same way. This is the detail that trips people up:

  • workspaceSlug (String!) - used by the page, layout, config and form reads. The SDK fetch helpers pass it from your config automatically.
  • workspaceId (String!) - used by the model, record and page-by-type reads. Call client.queryScoped(...): when your query declares $workspaceId and you do not pass it, the SDK resolves it from workspaceSlug and injects both the variable and the x-workspace-id header.
// $workspaceId is filled in for you
await client.queryScoped(MY_QUERY, { modelSlug: "products", limit: 20 });

What the SDK already wraps

You normally never write these - the listed helper calls them for you.

  • publicPagefetchPage - returns { id, blocks, publishedBlocks }.
  • publicPageByIdfetchPageById - returns { id, publishedBlocks }.
  • publicPagesfetchPages - returns [{ id, slug, updatedAt, publishedAt }].
  • publicPage (SEO fields) → fetchPageMeta - returns { id, seoTitle, seoDescription, seoKeywords, displayName }.
  • publicPageLayoutsfetchLayouts - returns [{ position, blocks }].
  • publicSiteConfigfetchSiteConfig / resolveSiteLocales - site name, locales, features, branding.
  • public.form.getresolveForms (and context.forms) - form fields and settings.
  • public.form.submitSUBMIT_FORM_MUTATION - returns { success, message, submissionId, ... }.

Write these yourself

These have no SDK helper. Send them through client.queryScoped(...).

Custom model records

query PublicModelRecords(
  $workspaceId: String!
  $modelSlug: String!
  $filter: JSON
  $sort: String
  $limit: Int
  $offset: Int
  $populate: [String!]
) {
  public {
    model {
      records(
        workspaceId: $workspaceId
        modelSlug: $modelSlug
        filter: $filter
        sort: $sort
        limit: $limit
        offset: $offset
        populate: $populate
      ) {
        items { id modelId data status createdAt updatedAt }
        total
        hasMore
      }
    }
  }
}

Write the query yourself and keep it in your repo. The SDK does carry equivalent strings, but under @cmssy/core/internal - an internal subpath, which means it can change between releases without a breaking-change note. Your own query is four lines and never surprises you.

List child pages

This is what powers a blog index or a docs tree:

query PublicPagesByType(
  $workspaceId: String!
  $parentSlug: String
  $search: String
  $limit: Int
  $offset: Int
) {
  publicPagesByType(
    workspaceId: $workspaceId
    parentSlug: $parentSlug
    search: $search
    limit: $limit
    offset: $offset
  ) {
    items {
      id
      slug
      fullSlug
      publishedAt
      displayName
      seoTitle
      seoDescription
      customFields
      pageType
    }
    total
    hasMore
  }
}

Submit a form

mutation SubmitForm($formId: ID!, $input: SubmitFormInput!) {
  public {
    form {
      submit(formId: $formId, input: $input) {
        success
        message
        submissionId
        redirectUrl
      }
    }
  }
}

Exported as SUBMIT_FORM_MUTATION. Pass { formId, input: { data } }.

Member auth: do not call directly

The siteMember mutations - login, register, refresh, logout, forgotPassword, resetPassword, verifyEmail - back the member auth flow.

Do not call them from your own code. Mount createCmssyAuthRoute instead: it handles them server-side and seals the session cookie. Calling them directly means handling token sealing yourself, and getting that wrong is a security bug rather than a broken page.

Things worth knowing

  • data and filter use the JSON scalar - pass plain objects, not stringified JSON.
  • customFields on a page is a JSON map of that page type's custom fields.
  • Reads return only published content unless a valid previewSecret is supplied. The SDK does this for you in edit mode, using your draftSecret.

Next steps