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.
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
-
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
PORTenvironment variable (default 8080) and serves the frontend.Bundle your assets into the build outputBuildpacks 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./staticfrom source will build fine and then return 404 for everything in production, which the platform's health probes turn into a crash loop. -
Serve a single-page frontend at
/. No server-side kontract logic is required — the browser talks to the contract API directly. -
Include
KONTRACT.mdat 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:
- Read
tokenfrom the URL fragment (location.hash), then remove the fragment from the address bar (history.replaceState) - Keep the token in memory (or
sessionStorage) and send it on every API call:Authorization: Bearer <token> - 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.
| Concern | Method + path | Notes |
|---|---|---|
| List apps | GET /foreman/apps/{org}?environment= | each app: name, app_name, repo_name, zone_ref, size, replicas, status.phase, status.url |
| Ship an app | POST /foreman/app | {namespace, app_name, repo_url, repo_name, branch, zone_ref, size, port, replicas, env, public_url_enabled} |
| Update an app | PATCH /foreman/app/{org}/{name} | replicas, env, public_url_enabled |
| Remove an app | DELETE /foreman/app/{org}/{name} | |
| Redeploy | POST /foreman/app/{org}/{name}/redeploy | fresh build from branch HEAD |
| Build logs | GET /foreman/app/{org}/{name}/build-logs | poll while building |
| Metrics | GET /foreman/app/{org}/{name}/metrics?range=1h | cpu, memory, pods, restarts series |
| List zones | GET /foreman/zones/{org} | each zone: name, band, upgrades, free, status.capacity_cpu/memory, status.allocated_cpu/memory, status.apps, status.phase |
| Buy a zone | POST /foreman/zones/{org} | {name, display_name, band, cost_center} — price is informational; no payment step |
| Resize a zone | PATCH /foreman/zone/{org}/{name} | band, upgrades; rejects shrink below allocation (409) |
| Character | GET/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:
- Contains a buildpack-detectable frontend (a static site with a tiny Node/Go server is the safest shape) listening on
PORT - Implements the auth handshake exactly as specified above (fragment token → memory →
Authorizationheader) - Calls discovery first and renders only the capabilities it declares
- Maps every contract term through the brief's vocabulary — zones/apps/replicas/shop/character never appear in user-facing copy under their contract names
- Renders zone capacity as the metaphor demands (a pit garage filling with cars), using
allocated / capacityfrom zone status - Ships apps with the fit rule previewed before
POSTand handles409/Failedstates in-theme - Writes progression to the character ledger under
<theme>.-prefixed keys only - Includes
KONTRACT.mdwith version, capabilities, and the vocabulary table - 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
- Themes — the model and trust boundaries
- The kontract — zones, bands, and payment concepts your theme renders