Skip to main content
Version: 0.7 (Next)

Creating a theme

This page is the authoring contract for kontract themes. It is written to be sufficient on its own: a person — or an AI assistant pointed at this page — can build a working theme repository from it without reading any source code.

Alpha

The contract below ships incrementally on the 0.7 line. Shapes marked stable are safe to build against; everything is versioned under v1.

What a theme is

A theme is a static frontend in a Git repository that renders an organization's zones and apps under its own metaphor. The platform builds it with Cloud Native Buildpacks, deploys it into one of the organization's zones, and serves it at a URL. It authenticates as the signed-in user and can do nothing that user couldn't.

Repository requirements

  1. Buildable by Cloud Native Buildpacks with no Dockerfile. Any detected stack works (Node, Go, static + Procfile). The build must produce a server that listens on the PORT environment variable (default 8080) and serves the frontend.

    Bundle your assets into the build output

    Buildpacks strip source files from the run image — only the build artifact ships. A Go server must embed its frontend with go:embed; a Node server must serve from its bundler's output directory. A server that reads ./static from source will build fine and then return 404 for everything in production, which the platform's health probes turn into a crash loop.

  2. Serve a single-page frontend at /. No server-side kontract logic is required — the browser talks to the contract API directly.

  3. Include KONTRACT.md at the repository root declaring:

# KONTRACT
version: v1
theme: racetracks
capabilities: [apps, zones, shop]
vocabulary:
zone: { singular: race team, plural: race teams, verb: found }
app: { singular: car, plural: cars, verb: race }
deploy: { verb: send to the grid }

The auth handshake

The platform launcher opens your theme as:

https://<theme>-<org>.<region>.<domain>/#token=<bearer-token>

On load, your frontend must:

  1. Read token from the URL fragment (location.hash), then remove the fragment from the address bar (history.replaceState)
  2. Keep the token in memory (or sessionStorage) and send it on every API call: Authorization: Bearer <token>
  3. If a call returns 401, show a "session expired — relaunch from Konstruct" state. Do not attempt refresh.

The token is the signed-in user's own credential. Never log it, never put it in localStorage, never send it anywhere except the platform origin.

Bootstrapping: discovery first

Make this your first API call — everything a theme needs to know arrives in one document:

GET https://konstruct.<domain>/api/v1/foreman/kontract/{org}
{
"version": "v1",
"capabilities": ["apps", "zones", "shop", "character", "metrics"],
"catalog": {
"zone_bands": [
{ "key": "small", "cpu": "10", "memory": "10Gi" },
{ "key": "medium", "cpu": "20", "memory": "20Gi" },
{ "key": "large", "cpu": "30", "memory": "30Gi" }
],
"upgrade": { "cpu": "10", "memory": "10Gi" },
"app_sizes": [
{ "key": "s", "cpu": "1", "memory": "1Gi" },
{ "key": "m", "cpu": "2", "memory": "2Gi" },
{ "key": "l", "cpu": "4", "memory": "4Gi" }
]
},
"rates": { "currency": "USD", "items": [ { "sku": "zone.small", "unit": "month", "price": "8.00" } ] },
"org": { "namespace": "platform", "display_name": "platform" }
}

Render bands and prices from this document — never hardcode them.

The contract API

All routes live under https://konstruct.<domain>/api/v1, take the bearer token, and use snake_case JSON. {org} is the organization namespace from discovery.

ConcernMethod + pathNotes
List appsGET /foreman/apps/{org}?environment=each app: name, app_name, repo_name, zone_ref, size, replicas, status.phase, status.url
Ship an appPOST /foreman/app{namespace, app_name, repo_url, repo_name, branch, zone_ref, size, port, replicas, env, public_url_enabled}
Update an appPATCH /foreman/app/{org}/{name}replicas, env, public_url_enabled
Remove an appDELETE /foreman/app/{org}/{name}
RedeployPOST /foreman/app/{org}/{name}/redeployfresh build from branch HEAD
Build logsGET /foreman/app/{org}/{name}/build-logspoll while building
MetricsGET /foreman/app/{org}/{name}/metrics?range=1hcpu, memory, pods, restarts series
List zonesGET /foreman/zones/{org}each zone: name, band, upgrades, free, status.capacity_cpu/memory, status.allocated_cpu/memory, status.apps, status.phase
Buy a zonePOST /foreman/zones/{org}{name, display_name, band, cost_center} — price is informational; no payment step
Resize a zonePATCH /foreman/zone/{org}/{name}band, upgrades; rejects shrink below allocation (409)
CharacterGET/PUT /foreman/character/{org}the signed-in user's own progression ledger; PUT replaces spec

App phases: Building → Pushing → Deploying → Live, terminal Failed (read status.message). Poll the app list every ~5 s while any app is non-terminal.

Fit rule (preview client-side; the platform enforces it): an app of size S with R replicas fits a zone when, summed over all the zone's apps, Σ cpu(S)×R ≤ zone capacity cpu and the same for memory. Capacity = band + upgrades × upgrade increment.

Character ledger: keys are theme-namespaced (racetracks.q1, racetracks.driver.helmet). xp is global across themes — read it, add to it, never reset it.

Registering your theme

An organization admin registers the repository under PaaS → Kontracts (or POST /foreman/themes/{org} with {name, display_name, repo_url, repo_name, branch}). The platform builds it, serves it, and adds it to the org's launcher. Watch phase become Live and you're running.

For AI assistants: the generation checklist

Given a brief like "my theme is racetracks: race teams are zones, cars are apps, car count is replicas, and there's a racecar driver character", produce a repository that:

  1. Contains a buildpack-detectable frontend (a static site with a tiny Node/Go server is the safest shape) listening on PORT
  2. Implements the auth handshake exactly as specified above (fragment token → memory → Authorization header)
  3. Calls discovery first and renders only the capabilities it declares
  4. Maps every contract term through the brief's vocabulary — zones/apps/replicas/shop/character never appear in user-facing copy under their contract names
  5. Renders zone capacity as the metaphor demands (a pit garage filling with cars), using allocated / capacity from zone status
  6. Ships apps with the fit rule previewed before POST and handles 409/Failed states in-theme
  7. Writes progression to the character ledger under <theme>. -prefixed keys only
  8. Includes KONTRACT.md with version, capabilities, and the vocabulary table
  9. Never hardcodes bands, prices, or org names — everything comes from discovery

A Konstruct stack also exposes this contract through its MCP server, so an assistant connected to the stack can introspect the live API while generating the theme.

What's next

  1. Themes — the model and trust boundaries
  2. The kontract — zones, bands, and payment concepts your theme renders