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}/graphqlapiBase 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. Callclient.queryScoped(...): when your query declares$workspaceIdand you do not pass it, the SDK resolves it fromworkspaceSlugand injects both the variable and thex-workspace-idheader.
// $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 fieldsseoTitle,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 ascontext.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
dataandfilteruse theJSONscalar - pass plain objects, not stringified JSON.customFieldson a page is aJSONmap of that page type's custom fields.- Reads return only published content unless a valid
previewSecretis supplied. The SDK does this for you in edit mode, using yourdraftSecret.
Next steps
- MCP server - the write path.
- Server loaders - where custom queries usually live.
- Member authentication - the flow behind the
siteMembermutations.