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 }
}
}
}
| Arg | Notes |
|---|---|
projectSlug | Required. Unique identifier (URL-safe). |
name | Optional. Display name; defaults to projectSlug when omitted. |
config | Optional JSON blob — project-level configuration consumed by the engine and plugins. |
secrets | Optional list of {key, value} pairs, stored encrypted at rest via the tenant's Providers keychain. |
options.deployTokenHash | Optional. 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.noDeployToken | Set 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:
| Code | Cause |
|---|---|
ALREADY_EXISTS | A project with that projectSlug already exists. |
INIT_ERROR | The 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 }
}
}
| Arg | Notes |
|---|---|
projectSlug | Required. Which project to update. |
name | Optional. New display name. |
config | Optional JSON. With mergeConfig: true, the patch is deep-merged into the existing config; with mergeConfig: false (the default) the existing config is replaced wholesale. |
mergeConfig | Optional. Defaults to false (replace). |
Errors:
| Code | Cause |
|---|---|
PROJECT_NOT_FOUND | No 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 }
}
}
| Arg | Notes |
|---|---|
projectSlug | Required. |
key | Required. The secret's name. |
value | Required. The new value. Cannot be read back via the API — only consumers running inside the engine (plugins, S3 module, …) decrypt it. |
Errors:
| Code | Cause |
|---|---|
PROJECT_NOT_FOUND | No 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.