The theme contract
The theme contract is the contract underneath Konduit: a small, versioned set of cloud-native, GitOps-friendly concepts — zones, apps, the shop, the character — plus the client API that lets a frontend render and drive them. Konduit is the flagship platform theme, always available. Games and custom skins are add-ons over the same contract, and this page is the reference for both halves: what the resources mean, and exactly how a theme talks to them.
The theme contract is in beta. This page describes the settled model and the current transport; both extend over time — see Contract boundaries for what's known to be coming.
Trust model
A theme is a static frontend running in a sandboxed, cross-origin iframe. It never receives a credential — not a token, not a cookie, not an API key.
┌─────────────────────────────┐ postMessage ┌───────────────────────────────┐
│ Theme (sandboxed iframe) │ ───────────────────────▶ │ Broker (Konstruct shell) │
│ no credential, cross-origin │ ◀─────────────────────── │ validates sender + org pin │
└─────────────────────────────┘ {type:"theme-rpc-…"} └───────────────┬───────────────┘
│ user's session
▼
┌───────────────────────────────┐
│ Konstruct API │
└───────────────┬───────────────┘
▼
operators → GitOps → clusters
Every operation a theme makes travels over postMessage to a broker running in the Konstruct shell, which:
- Validates the sender — only a frame the shell itself launched can talk to the broker
- Pins the organization — every call resolves against the organization the theme was launched for; a theme cannot choose or switch its org, and an
orgargument it passes is ignored - Sanitizes path segments — names and identifiers are cleaned before they reach any URL or API call
- Performs the API call with the signed-in user's session — a theme can do nothing that user couldn't already do by hand
Because the credential never leaves the shell, a compromised or careless theme cannot leak a session token — there isn't one in its process to leak.
The vendored client
A theme includes static/theme.js verbatim — copied byte-for-byte from the starter theme and never edited. This file is the entire client: launch detection, discovery, and every contract operation, wrapped around the postMessage exchange above.
⚠️ Warning: The platform verifies
static/theme.jsbyte-for-byte at build and registration time. A theme that ships a modified copy — even a well-intentioned tweak — is treated as forged and fails to register. If you need behaviortheme.jsdoesn't have, wrap it (see Ship It!'s bridge pattern); don't edit it.
There is no launch(), login(), or init() — a theme doesn't authenticate, because it never needed to.
The client API
This is the complete contract. There is nothing else.
theme.isLaunched()
Returns bool. false means the theme was opened standalone — no parent frame, e.g. a curious visitor followed a public link. true means the Konstruct shell launched it with a session behind it.
if (!theme.isLaunched()) {
renderWelcomeWithSampleData(); // no live calls — nothing to authenticate
} else {
renderAppShellImmediately(); // never gate this behind a button
loadRealData(); // discover(), then the rest
}
ℹ️ Note: When launched, render your main view immediately and load data underneath it. Never show a "click to start" button first — the user already clicked once, to open the theme.
theme.discover(org?)
The bootstrap call. Make it first, always — never hardcode a band, a size, or a price.
{
"org": { "name": "acme", "display_name": "Acme Inc" },
"capabilities": ["apps", "zones", "shop", "character"],
"bands": [
{ "key": "small", "cpu": "10", "memory": "10Gi", "monthly_usd": 8 },
{ "key": "medium", "cpu": "20", "memory": "20Gi", "monthly_usd": 16 },
{ "key": "large", "cpu": "30", "memory": "30Gi", "monthly_usd": 24 }
],
"app_sizes": [
{ "key": "s", "cpu": "1", "memory": "1Gi" },
{ "key": "m", "cpu": "2", "memory": "2Gi" },
{ "key": "l", "cpu": "4", "memory": "4Gi" }
],
"rates": { "currency": "USD", "vcpu_hour_usd": 0.01, "gb_hour_usd": 0.005 }
}
theme.zones(org?)
Returns the organization's zones.
[
{
"name": "production",
"display_name": "Production",
"band": "medium",
"free": true,
"cluster": "k-lon1",
"status": {
"phase": "Ready",
"capacity_cpu": "20", "capacity_memory": "20Gi",
"allocated_cpu": "7", "allocated_memory": "7Gi",
"apps": 3
}
}
]
theme.createZone(org, zone)
theme.createZone(org, {
name: "staging",
display_name: "Staging",
band: "medium",
});
theme.apps(org?)
[
{
"name": "hello-world",
"app_name": "hello-world",
"environment": "production",
"zone_ref": "production",
"repo_url": "https://github.com/acme/hello-world",
"repo_name": "acme/hello-world",
"branch": "main",
"size": "s",
"port": 8080,
"replicas": 1,
"env": [{ "name": "LOG_LEVEL", "value": "info" }],
"public_url_enabled": true,
"volume": { "size": "5Gi", "mount_path": "/data" },
"custom_domain": "www.acme.com",
"created_by": "jane@acme.com",
"status": {
"phase": "Live",
"image": "harbor.acme.example/acme/hello-world@sha256:9f2c…",
"build_sha": "a1b2c3d",
"last_build_ref": "build-14",
"url": "https://hello-world-production.lon1.acme.example",
"message": ""
}
}
]
theme.appRepos(org?)
The organization's registered App Repositories — what it may ship. Use this to populate a repository picker; a theme never lets a user type an arbitrary Git URL outside this list.
[
{ "repo_url": "https://github.com/acme/hello-world", "repo_name": "acme/hello-world", "default_branch": "main" }
]
theme.shipApp(app)
theme.shipApp({
app_name: "hello-world",
repo_url: "https://github.com/acme/hello-world",
repo_name: "acme/hello-world",
branch: "main",
zone_ref: "production",
size: "s",
port: 8080,
replicas: 1,
env: [],
public_url_enabled: true,
});
theme.updateApp(org, name, body)
Mutable fields only: replicas, branch, env, public_url_enabled, volume, custom_domain.
theme.updateApp(org, "hello-world", { replicas: 2, public_url_enabled: true });
volume attaches or replaces the app's single persistent volume; omit it to leave storage untouched. An app with a volume is pinned to 1 replica — the API rejects both attaching to a multi-replica app and scaling a volume-backed app past one. Sending a volume with an empty size detaches it and deletes the data; make the user confirm before ever sending that.
theme.updateApp(org, "hello-world", { volume: { size: "5Gi", mount_path: "/data" } });
custom_domain sets a user-owned hostname; empty string removes it. Ownership is proven via DNS before anything routes: the app's status.domain_token is the value for a TXT _konduit-challenge.<domain> record, status.domain_verified flips true once it resolves, and hostnames are unique platform-wide (a second claim gets 409). A theme showing domains MUST display both required records (TXT challenge + CNAME to the platform host) and the verification state — never present an unverified domain as live.
theme.updateApp(org, "hello-world", { custom_domain: "www.acme.com" });
theme.deleteApp(org, name)
theme.redeploy(org, name)
Forces a fresh build from the tracked branch's current HEAD, independent of whether the source changed.
theme.buildLogs(org, name)
Returns the latest build's log output and its phase — poll this while an app is building to stream progress.
theme.metrics(org, name, opts?)
Per-app resource series — the same data Konduit charts. opts takes range
(default "1h") and step (default "30s") as duration strings; the broker
validates both, so anything else falls back to the defaults.
{
"range": "1h",
"step": "30s",
"series": [
{ "name": "cpu", "unit": "cores", "points": [[1700000000, "0.02"]] },
{ "name": "memory", "unit": "bytes", "points": [[1700000000, "52428800"]] },
{ "name": "pods", "unit": "count", "points": [[1700000000, "1"]] },
{ "name": "restarts", "unit": "count", "points": [[1700000000, "0"]] }
]
}
theme.character(org?) / theme.saveCharacter(org, spec)
{
"display_name": "Captain",
"appearance": { "shipit.helmet": "gold", "shipit.suit": "default" },
"xp": 275,
"level": 4,
"quests": ["shipit.q1", "shipit.q2"],
"inventory": ["shipit.gadget.radio"],
"equipped": { "helmet": "shipit.helmet.gold" }
}
character(org) returns null for a user who has never saved one — render your theme's default/starting state rather than erroring.
Hard rules
These apply to every theme, in every profile, no exceptions:
status.phaseis exactly one ofBuilding,Pushing,Deploying,Live,Failed. Don't invent additional phases; map all five through your theme's vocabulary.- Never obscure the truth. A
Failedapp must always be findable, and itsstatus.messagemust be reachable within one click, however deep the metaphor goes. - Always celebrate the ship. A successful deploy is the emotional peak of using the platform. The celebration must be ≤ 6 seconds, skippable, and must never block the user from doing anything else.
- Escape every API-derived string before DOM injection. Names, messages, and env values are user- and repo-controlled — treat them as untrusted input, not as safe HTML.
- Flavor may be fictional; capabilities may not. The metaphor can be as playful as you like, but never depict platform features the contract does not perform — no imaginary rollout strategies, fake CI stages, or dead controls.
- Poll
appsevery 10–20 seconds while any app is non-terminal (Building,Pushing, orDeploying). Don't poll faster than that, and stop once everything is terminal.
Zones: the capacity and billing unit
A zone is where apps live. It replaces the environment concept: the zone is the isolation boundary, and the theme you use expresses what that boundary feels like (an environment in Konduit, a planet in a game, a neighborhood in another).
- Every organization gets its first zone free
- More zones are purchased in bands — fixed capacity sizes from the platform catalog
- A zone materializes as a quota-capped workload namespace, enforced by a
ResourceQuotaandLimitRangecommitted through GitOps - A zone is pinned to one managed cluster
| Band | CPU | Memory |
|---|---|---|
small | 10 | 10Gi |
medium | 20 | 20Gi |
large | 30 | 30Gi |
upgrade (increment) | +10 | +10Gi |
Bands are defined in code, not configuration: a rate card prices them, administrators re-price them for chargeback, but nobody redefines what small means. Underneath the band, the stored quota values remain fluid Kubernetes quantities — the band constrains what is offered and priced, not what the machinery can express.
Apps — the resource unit
Apps always ship to a zone. An app has a size (s = 1 CPU/1Gi, m = 2/2Gi, l = 4/4Gi, per replica) and either fits the zone or doesn't: the sum of size × replicas across the zone's apps must stay within the band (plus upgrades). Fit is checked at registration, re-checked by the operator before every GitOps commit, and backstopped by the zone's ResourceQuota at admission time.
App metadata is recorded for historical purposes today — zones are what you pay for; apps are what consumes them. App metadata may inform billing directly in a later phase.
The shop
Zones, upgrades, and add-ons are bought in the shop. Prices come from a RateCard — a platform resource administrators edit to distribute costs to downstream departments, with no external billing integration required. Themes customize the shop's presentation (vocabulary, imagery, which items are featured), never its price math.
Themes — presentation only
A theme renders the same theme contract under a different metaphor: Konduit's business language, or a game where zones are planets and deploys are launches. Themes carry vocabulary, copy, colors, assets, and optional progression rules. The backend never branches on the active theme.
Character — a separate section
Progression (appearance, XP, levels, quests, unlocks, inventory) lives on a per-user Character, entirely separate from zones and payment. XP is global across every theme — only ever add to it, never reset it. A hero leveled in one skin keeps their level in another. Business themes simply ignore it.
Custom resources
All kinds live in the konstruct.civo.com group, namespaced per organization (platform-level kinds in the platform namespace). ThemedApp is served at beta1; Zone, RateCard, Theme and Character at v1alpha1:
| Kind | Owns |
|---|---|
Zone | band, upgrades, placement, free flag, cost center → materializes the quota-capped namespace |
RateCard | SKU price lines (zone.small, zone.upgrade, …) for chargeback |
ThemedApp | one deployable app — spec (repo, zone, size, port, replicas, env) and status (phase, image, build ref, URL) |
Theme | theme registration: repository to build, capabilities, vocabulary, progression rules — see Themes |
Character | appearance, xp, level, quests, unlocks, inventory, equipped |
As everywhere in Konstruct: the API writes CRs, operators reconcile them through GitOps commits, and CR status is the source of truth.
What's next
- Themes — how frontends implement the theme contract, including your own
- Platform requirements — enabling Konduit on your control plane