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.
- A configuration MUST have one stable
id, onekind, and one organization. - Creating a configuration MUST atomically create immutable version
1and setheadVersionto1. - Saving configuration content MUST append exactly one new immutable version, even when the submitted document is unchanged.
- A content save MUST include
expectedVersion. The service MUST reject a stale value with409 version_conflictand MUST NOT create a version. GET /v1/configs/:idMUST resolve the document atheadVersion. A consumer MUST use this route for normal runtime display and data binding.- Historical documents MUST be read only through an explicitly versioned route. Listing history MUST NOT change the runtime head.
- Restoring history MUST append a new version copied from the selected historical document. It MUST NOT move the head backward or rewrite history.
- Metadata and access changes MUST NOT mutate a version document.
- Configurations and grants MUST NOT cross organization boundaries.
- 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
| Field | Meaning |
|---|---|
id | Opaque stable identifier assigned by Config Store. |
organizationId | Owning ZUI Cloud organization; never client-selectable across organizations. |
kind | app for a valid AppSpec v1 or view for a valid ViewSpec v2. Immutable after creation. |
name | Human-facing metadata; changing it does not create or rewrite a content version. |
ownerUserId | Opaque ID from the consuming application. It is not required to be a ZUI Cloud account ID. |
visibility | private or organization. Organization visibility grants read access, not edit access. |
headVersion | Positive integer naming the only version selected by the unversioned read. |
access | Effective caller permission: view, edit, or admin. |
createdAt, updatedAt | UTC timestamps. updatedAt changes when content or metadata changes. |
Version
| Field | Meaning |
|---|---|
configId, version | Immutable composite identity. Versions are monotonically increasing positive integers. |
document | Validated AppSpec or ViewSpec; returned by head and exact-version reads, not history listings. |
changeNote | Optional audit-oriented explanation supplied by the consumer. |
createdBy | ZUI Cloud organization member/API key and delegated consumer user associated with the write. |
createdAt | UTC 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 access | Read head/history | Create version | Change metadata/access |
|---|---|---|---|
| Viewer | Yes | No | No |
| Editor | Yes | Yes | No |
| Owner/admin | Yes | Yes | Yes |
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
| Route | Required access | Contract |
|---|---|---|
GET /v1/configs?kind=&ownerUserId=&limit= | View | Accessible metadata; no document bodies. |
POST /v1/configs | Authenticated | Create a configuration and version 1. |
GET /v1/configs/:id | View | Metadata plus the current head document. |
PATCH /v1/configs/:id | Admin | Change name, ownerUserId, or visibility; no content write. |
GET /v1/configs/:id/versions | View | Newest-first immutable version metadata. |
GET /v1/configs/:id/versions/:version | View | One explicit historical document. |
POST /v1/configs/:id/versions | Edit | Append content using expectedVersion. |
POST /v1/configs/:id/versions/:version/restore | Edit | Copy history into a new head version. |
GET /v1/configs/:id/access | Admin | Read owner, visibility, and grants. |
PUT /v1/configs/:id/access | Admin | Upsert a user/group viewer/editor grant. |
DELETE /v1/configs/:id/access/:grantId | Admin | Remove 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
| Response | Typical cause | Correct handling |
|---|---|---|
400 invalid_body | Wrong document version/kind, empty name, unsupported visibility/grant, or malformed JSON | Fix the caller; do not retry unchanged. |
400 invalid_principal | Delegated ID over 200 characters, more than 50 groups, or an oversized group ID | Reject or normalize the application's identity mapping. |
403 forbidden | Known config but caller has less than required access | Keep read and write controls distinct in the UI. |
404 not_found | Unknown ID, other organization, or caller has no visibility | Return a neutral not-found experience; do not probe with another identity. |
409 version_conflict | Another writer advanced headVersion | Reload, 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
.sqlitefile 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
409without 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.idis 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/:idfor 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.