Skip to main content

Project management

The Tenant API has three mutations for managing the lifecycle of a project: createProject, updateProject, and setProjectSecret. In day-to-day work most teams reach for the Contember CLI instead — contember project create, contember deploy, and friends wrap these mutations with schema migrations, validation, and deploy-token handling. The GraphQL surface documented here is what those commands ultimately call, and what you'd use directly when scripting tenant provisioning or building a custom admin.

All three mutations require global tenant permissions (SUPER_ADMIN by default; extendable via Tenant ACL) and are recorded in the audit log.

Creating a project

mutation {
createProject(
projectSlug: "my-blog",
name: "My Blog",
config: { /* project-level config blob, optional */ },
secrets: [
{ key: "s3.accessKey", value: "AKIA…" },
{ key: "s3.secretKey", value: "…" }
],
options: {
noDeployToken: false,
deployTokenHash: null # supply a sha256 hex if you want to pre-set the deploy token
}
) {
ok
error { code developerMessage }
result {
deployerApiKey { id token }
}
}
}
ArgNotes
projectSlugRequired. Unique identifier (URL-safe).
nameOptional. Display name; defaults to projectSlug when omitted.
configOptional JSON blob — project-level configuration consumed by the engine and plugins.
secretsOptional list of {key, value} pairs, stored encrypted at rest via the tenant's Providers keychain.
options.deployTokenHashOptional. SHA-256 hex of the deploy token you want to use. When omitted, the engine generates a new token and returns it in result.deployerApiKey.token.
options.noDeployTokenSet to true to skip deploy-token creation entirely.

The deprecated top-level deployTokenHash argument is still accepted but options.deployTokenHash is preferred for new code.

Errors:

CodeCause
ALREADY_EXISTSA project with that projectSlug already exists.
INIT_ERRORThe schema or stage initialization failed; check the engine logs.

When the caller is not SUPER_ADMIN, the resulting project gets the caller's identity wired in as its owner so they keep PROJECT_ADMIN membership without an explicit invite step.

Audit: written as project_create with event_data {slug, name, secretKeys} — only the names of the secrets are logged, never the values.

Updating a project

Use updateProject to change the display name or replace/merge the config blob.

mutation {
updateProject(
projectSlug: "my-blog",
name: "My Personal Blog",
config: { foo: "bar" },
mergeConfig: true
) {
ok
error { code developerMessage }
}
}
ArgNotes
projectSlugRequired. Which project to update.
nameOptional. New display name.
configOptional JSON. With mergeConfig: true, the patch is deep-merged into the existing config; with mergeConfig: false (the default) the existing config is replaced wholesale.
mergeConfigOptional. Defaults to false (replace).

Errors:

CodeCause
PROJECT_NOT_FOUNDNo project with that slug.

Audit: project_update with event_data carrying the slug, a {before, after} snapshot when the name changed, and {configChanged: true, mergeConfig} when the config was touched. The config payload itself is not stored.

Managing project secrets

setProjectSecret upserts a single key in the project's secret store. Values are encrypted at rest.

mutation {
setProjectSecret(
projectSlug: "my-blog",
key: "s3.secretKey",
value: "new-secret-value"
) {
ok
error { code developerMessage }
}
}
ArgNotes
projectSlugRequired.
keyRequired. The secret's name.
valueRequired. The new value. Cannot be read back via the API — only consumers running inside the engine (plugins, S3 module, …) decrypt it.

Errors:

CodeCause
PROJECT_NOT_FOUNDNo project with that slug.

Audit: project_secret_change with event_data {slug, key} — never the value.

There is no removeProjectSecret mutation. To clear a secret, call setProjectSecret with an empty string.

Reading projects

The companion queries are:

  • projects — every project the caller has any membership on.
  • projectBySlug(slug) — a single project's metadata, including its roles and (with the right permission) its members.
query {
projects {
id slug name
}
}

These are read-side counterparts; they don't carry any of the secret-bearing fields.

When to use the CLI instead

The Tenant API operates only on the tenant layer — slug, config blob, secrets, deploy token. It does not run the system migrations that bring a new project's content schema to life. A bare createProject produces a tenant entry but no usable content stages until you also push a schema migration. The CLI's contember deploy command wraps both steps; if you call createProject from your own code, you'll typically follow it with a system-API migration execute against the new project's slug.