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 wraps
You normally never write these - the listed helper calls them for you.
publicPage→fetchPage- returns{ id, blocks, publishedBlocks }.publicPageById→fetchPageById- returns{ id, publishedBlocks }.publicPages→fetchPages- returns[{ id, slug, updatedAt, publishedAt }].publicPage(SEO fields) →fetchPageMeta- returns{ id, seoTitle, seoDescription, seoKeywords, displayName }.publicPageLayouts→fetchLayouts- returns[{ position, blocks }].publicSiteConfig→fetchSiteConfig/resolveSiteLocales- site name, locales, features, branding.public.form.get→resolveForms(andcontext.forms) - form fields and settings.public.form.submit→SUBMIT_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
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.