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 reads for you

You normally never write these - createCmssyPage, CmssyServerLayout and the form resolution behind context.forms issue them internally.

  • public.page.get - one page by slug: { id, blocks, publishedBlocks } plus the SEO fields seoTitle, seoDescription, seoKeywords, displayName.
  • public.page.getById - { id, publishedBlocks }.
  • public.page.list - [{ id, slug, updatedAt, publishedAt }].
  • public.page.layouts - [{ position, blocks, settings }].
  • public.siteConfig - site name, default and enabled languages, features, branding.
  • public.form.get - form fields and settings, surfaced to blocks as context.forms.
  • public.form.submit - { success, message, submissionId, redirectUrl }.

The fetch helpers that wrap them - fetchPage, fetchPages, fetchLayouts, fetchPageMeta, fetchSiteConfig, resolveSiteLocales, resolveForms - live under @cmssy/core/internal and are not public API. The public surface is createCmssyClient and graphqlRequest: anything createCmssyPage does not already render, you query yourself.

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
) {
  public {
    page {
      byType(
        workspaceId: $workspaceId
        parentSlug: $parentSlug
        search: $search
        limit: $limit
        offset: $offset
      ) {
        items {
          id
          slug
          fullSlug
          publishedAt
          displayName
          seoTitle
          seoDescription
          customFields
          pageType
        }
        total
        hasMore
      }
    }
  }
}

byType also accepts pageType, sortBy and customFieldFilters, and - with a valid previewSecret - includeDrafts.

Submit a form

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

Pass { formId, input: { data } }. The SDK carries the same string as SUBMIT_FORM_MUTATION, but under @cmssy/core/internal - keep your own copy rather than importing it.

Member auth is yours to mount

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

The slim SDK ships no auth route and no session reader. login and refresh hand you the raw accessToken and refreshToken, and the backend sets no cookie - so your app owns the session. An httpOnly cookie written by your own route handler is the safe default. See member authentication for the whole flow, register through email verification.

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