Skip to main content

Config Store

Config Store is the ZUI Cloud module for saving AppSpec and ViewSpec documents on behalf of consuming applications and their users. It provides the storage guarantees a portable configuration needs: organization isolation, immutable versions, deterministic latest-version resolution, ownership, and user or group access grants.

The consumer remains responsible for authentication, business authorization, data access, and runtime adapters. Config Store stores configuration documents; it does not execute actions or fetch the application's data.

Normative contract

The terms MUST, MUST NOT, SHOULD, and MAY describe requirements for a conforming integration.

  1. A configuration MUST have one stable id, one kind, and one organization.
  2. Creating a configuration MUST atomically create immutable version 1 and set headVersion to 1.
  3. Saving configuration content MUST append exactly one new immutable version, even when the submitted document is unchanged.
  4. A content save MUST include expectedVersion. The service MUST reject a stale value with 409 version_conflict and MUST NOT create a version.
  5. GET /v1/configs/:id MUST resolve the document at headVersion. A consumer MUST use this route for normal runtime display and data binding.
  6. Historical documents MUST be read only through an explicitly versioned route. Listing history MUST NOT change the runtime head.
  7. Restoring history MUST append a new version copied from the selected historical document. It MUST NOT move the head backward or rewrite history.
  8. Metadata and access changes MUST NOT mutate a version document.
  9. Configurations and grants MUST NOT cross organization boundaries.
  10. A stored document MUST NOT contain credentials. Live business data SHOULD remain outside the document unless it is intentionally configuration-owned static data.

These rules make headVersion a compare-and-swap token as well as the single runtime selection rule.

Resource model

Configuration

FieldMeaning
idOpaque stable identifier assigned by Config Store.
organizationIdOwning ZUI Cloud organization; never client-selectable across organizations.
kindapp for a valid AppSpec v1 or view for a valid ViewSpec v2. Immutable after creation.
nameHuman-facing metadata; changing it does not create or rewrite a content version.
ownerUserIdOpaque ID from the consuming application. It is not required to be a ZUI Cloud account ID.
visibilityprivate or organization. Organization visibility grants read access, not edit access.
headVersionPositive integer naming the only version selected by the unversioned read.
accessEffective caller permission: view, edit, or admin.
createdAt, updatedAtUTC timestamps. updatedAt changes when content or metadata changes.

Version

FieldMeaning
configId, versionImmutable composite identity. Versions are monotonically increasing positive integers.
documentValidated AppSpec or ViewSpec; returned by head and exact-version reads, not history listings.
changeNoteOptional audit-oriented explanation supplied by the consumer.
createdByZUI Cloud organization member/API key and delegated consumer user associated with the write.
createdAtUTC creation timestamp.

Access grant

A grant targets a consumer user or group ID and assigns viewer or editor. The configuration owner and organization service administrators have admin access.

Effective accessRead head/historyCreate versionChange metadata/access
ViewerYesNoNo
EditorYesYesNo
Owner/adminYesYesYes

Authentication and delegated identity

Use a session token for the ZUI Cloud console or an organization API key from a trusted consumer backend. A backend acting for one of its users sends:

Authorization: Bearer zui_pk_...
X-ZUI-User-ID: customer-user-42
X-ZUI-Group-IDs: finance-ops,regional-admins

The IDs are opaque and scoped to the API key's organization. The consuming backend MUST derive them from a trusted authenticated principal. It MUST NOT forward these headers from an untrusted browser request without validation, and it MUST NOT expose the organization API key to the browser.

Create version 1

POST /v1/configs
Authorization: Bearer zui_pk_...
X-ZUI-User-ID: customer-user-42
Content-Type: application/json

{
"kind": "view",
"name": "Renewal risk · My view",
"visibility": "private",
"document": {
"version": 2,
"id": "renewal-risk",
"title": "Renewal risk",
"root": { "type": "stack", "children": [] }
},
"changeNote": "Created from the certified default"
}

The response contains configuration metadata and version 1. The delegated user becomes ownerUserId unless a service administrator explicitly supplies another owner.

Resolve the runtime head

const response = await fetch(`${cloudUrl}/v1/configs/${configId}`, {
headers: {
Authorization: `Bearer ${process.env.ZUI_CLOUD_KEY}`,
'X-ZUI-User-ID': currentUser.id,
'X-ZUI-Group-IDs': currentUser.groupIds.join(','),
},
})

if (!response.ok) throw new Error('Config resolution failed')

const {config, version} = await response.json()
// Required invariant:
console.assert(version.version === config.headVersion)

render(version.document)

The response also returns X-Config-Version. Do not choose the last item from a cached history list and do not ask the browser to select a version for normal display. The unversioned resource is the authoritative head resolver.

Save a new version

POST /v1/configs/cfg_123/versions
Authorization: Bearer zui_pk_...
X-ZUI-User-ID: customer-user-42
Content-Type: application/json

{
"expectedVersion": 7,
"document": { "version": 2, "id": "renewal-risk", "root": { "type": "stack" } },
"changeNote": "Hide the regional comparison"
}

On success, Config Store atomically inserts version 8 and advances headVersion to 8. It never updates version 7.

If another writer already created version 8, the response is:

{
"error": {
"code": "version_conflict",
"message": "The config is at version 8.",
"details": { "currentVersion": 8 }
}
}

The consumer SHOULD reload the head, reconcile the user's changes, and submit a new save against the returned current version. It MUST NOT silently retry the stale document with a changed version number.

Track and inspect history

GET /v1/configs/cfg_123/versions
GET /v1/configs/cfg_123/versions/7

The history route returns newest-first metadata without documents. The exact version route returns one historical document. This separation keeps routine history views small and makes any historical document selection explicit.

To restore version 4 while the current head is 8:

POST /v1/configs/cfg_123/versions/4/restore
Content-Type: application/json

{ "expectedVersion": 8, "changeNote": "Restore approved layout from v4" }

The result is version 9, whose document equals version 4. Versions 1 through 8 remain unchanged and queryable.

Routes

RouteRequired accessContract
GET /v1/configs?kind=&ownerUserId=&limit=ViewAccessible metadata; no document bodies.
POST /v1/configsAuthenticatedCreate a configuration and version 1.
GET /v1/configs/:idViewMetadata plus the current head document.
PATCH /v1/configs/:idAdminChange name, ownerUserId, or visibility; no content write.
GET /v1/configs/:id/versionsViewNewest-first immutable version metadata.
GET /v1/configs/:id/versions/:versionViewOne explicit historical document.
POST /v1/configs/:id/versionsEditAppend content using expectedVersion.
POST /v1/configs/:id/versions/:version/restoreEditCopy history into a new head version.
GET /v1/configs/:id/accessAdminRead owner, visibility, and grants.
PUT /v1/configs/:id/accessAdminUpsert a user/group viewer/editor grant.
DELETE /v1/configs/:id/access/:grantIdAdminRemove a grant.

All errors use { "error": { "code", "message", "details?" } }. An inaccessible configuration returns 404, preventing callers from using the response to discover private IDs in their organization.

List endpoints accept limit; the HTTP layer clamps it to 1–500 and uses 100 when omitted or invalid. The current API does not expose cursors, config deletion, or history pruning. Treat a bounded list as a recent working set, not a complete export, and include long-term retention/offboarding requirements in the deployment design before storing regulated configuration.

Implement a safe editor save loop

Read the authoritative head when opening an editor and retain its headVersion. Submit that exact version with the edited document. A conflict is a product state, not an infrastructure retry:

type ConfigHead = {
config: {id: string; headVersion: number; access: 'view' | 'edit' | 'admin'}
version: {version: number; document: unknown}
}

type VersionConflict = {
error: {
code: 'version_conflict'
details: {currentVersion: number}
}
}

async function saveConfig(
configId: string,
headVersion: number,
document: unknown,
changeNote: string,
currentUser: {id: string; groupIds: string[]},
) {
const encodedId = encodeURIComponent(configId)
const response = await fetch(`${process.env.ZUI_CLOUD_URL}/v1/configs/${encodedId}/versions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZUI_CLOUD_KEY}`,
'X-ZUI-User-ID': currentUser.id,
'X-ZUI-Group-IDs': currentUser.groupIds.join(','),
'Content-Type': 'application/json',
},
body: JSON.stringify({expectedVersion: headVersion, document, changeNote}),
})

if (response.status === 409) {
const conflict = await response.json() as VersionConflict
return {kind: 'conflict' as const, currentVersion: conflict.error.details.currentVersion}
}
if (!response.ok) throw new Error(`Config save failed (${response.status})`)
return {kind: 'saved' as const, value: await response.json() as ConfigHead}
}

On conflict, fetch the head again and let the user compare, merge, or discard their changes. Do not automatically resubmit the stale document against currentVersion; that turns an explicit safety control into last-write-wins.

Validation and conflict behavior

ResponseTypical causeCorrect handling
400 invalid_bodyWrong document version/kind, empty name, unsupported visibility/grant, or malformed JSONFix the caller; do not retry unchanged.
400 invalid_principalDelegated ID over 200 characters, more than 50 groups, or an oversized group IDReject or normalize the application's identity mapping.
403 forbiddenKnown config but caller has less than required accessKeep read and write controls distinct in the UI.
404 not_foundUnknown ID, other organization, or caller has no visibilityReturn a neutral not-found experience; do not probe with another identity.
409 version_conflictAnother writer advanced headVersionReload, compare, and explicitly reconcile.

Names and delegated owner IDs are limited to 200 characters; change notes to 500. kind is immutable after creation. AppSpec must be version 1 with at least one page; ViewSpec must be version 2 with a structurally valid root. The document remains an extensible JSON contract, while registry-aware runtime validation remains the consuming application's responsibility.

Self-host Config Store

Config Store has no module-specific environment variables. It uses the shared ZUI_CLOUD_DATABASE_URL, authentication, body limit, and CORS settings. In the current combined binary, Chromium and dist/web are still startup/runtime dependencies even when an installation initially plans to call only Config Store.

Operationally:

  • SQLite is the authoritative store for configs, immutable versions, and grants. Run one active ZUI Cloud process on local persistent disk.
  • Every content save creates a complete new JSON document. Forecast database growth from document size × save frequency × retention period, not only the number of configs.
  • Metadata and grant changes update relational state without rewriting content versions. Restores append new full versions.
  • Back up Config Store with the SQLite-aware process in the Cloud Private operations runbook. A filesystem copy of only the main .sqlite file while WAL writes are active is not sufficient.
  • The service applies append-only schema migrations at startup. Exercise an upgrade against a restored production-sized database before promotion.
  • There is no public config deletion or history-retention API in this release. Do not delete rows directly in production; establish contractual retention and offboarding procedures before go-live.

Monitor authenticated canary operations in addition to process liveness: read a private canary config as its owner, verify another delegated user receives 404, append a disposable version with the current head, and deliberately exercise a stale expectedVersion in staging. Alert on database capacity, backup freshness, write errors, and unexpected conflict-rate changes.

Integration verification matrix

Before release, automate these cases against a disposable organization:

  • Owner A creates a private config; unrelated user B cannot list or read it.
  • A user grant permits read but not content or metadata writes.
  • A group editor can read and append a version but cannot change grants.
  • Organization visibility permits read but not edit to an otherwise unrelated delegated user.
  • Two writers save from the same head; exactly one advances it and the other receives 409 without a new version.
  • Restoring history creates a new head while every earlier version remains byte-for-byte readable.
  • An API key without a delegated user has service-administrator access only inside its own organization.
  • Documents containing test credential patterns are rejected by the consuming application's pre-save controls; Config Store is not a secret scanner.

Consumer responsibilities

  • Keep the organization API key server-side.
  • Validate that a saved view belongs to an authorized app/page before applying it. A stable ViewSpec.id is useful for that binding.
  • Resolve resource references through the consumer's policy-enforcing runtime; never store SQL, credentials, or executable functions in the document. Avoid live fetched rows; intentionally configuration-owned static data may remain.
  • Use GET /v1/configs/:id for display and data binding. Use exact-version routes only for audit, comparison, or an explicit restore workflow.
  • Treat access to a config document separately from access to the resources it references. Config Store ACLs do not replace row-level data policy.

Continue with PDF Export when the resolved head must become a governed document, or read AppSpec and runtime for host adapter and policy responsibilities.