Skip to main content
Version: 0.7 (Next)

Platform Themes theme specification

This page is the spec for theme contract themes. Creating a theme teaches you how to build one; this page defines what a working theme is. Requirement words are plain English: must is a hard requirement, should is a strong default you can break with good reason, and may is optional.

Alpha

The specification ships incrementally on the 0.7 line, against the v2 (postMessage) contract.

Themes are views, not silos

Every platform theme resource — zones, apps, the character — is persisted by the platform, independent of any theme. A theme is a lens: leave one theme, open another, and everything you deployed is still there. A large planet in Ship It! is a large track in a racing theme is a large environment in Konduit — the same real zone.

Two rules follow, and they are the heart of this spec:

  1. A theme must not obscure the truth. Metaphor decorates state; it never hides it. A Failed app must always be findable, readable, and actionable, whatever the theme calls it.
  2. A theme must celebrate the ship. Deploying an app is the emotional peak of the platform. Every theme — from the plainest read-only dashboard to the deepest game — acknowledges a successful ship in its own register. A game theme runs a cinematic; a business theme still gives the user a moment that feels good.

Profiles

A theme declares one profile in its manifest. Each profile adds obligations to the previous one.

ProfileWhat it isExample
informationalRead-only. Renders apps, phases, URLs, build logs. No writes.A delivery status wall
interactiveFull zone/app lifecycle under a plain or branded metaphor.Konduit
gameInteractive, plus the game formula: character, progression, shop, universe view, surprises.Ship It!

All profiles must: call discover first and render only declared capabilities; use the launched org only; render truthful state (Failed findable); celebrate a successful ship; never hardcode bands, sizes, prices, or org names.

interactive adds: zone purchase and app ship/update/delete/redeploy flows; the fit rule previewed client-side before shipping; build-log access while an app builds.

game adds: the sections below marked (game) — character, progression, shop, universe view, and surprises.

The theme manifest

Every theme repository must contain theme-manifest.yaml at its root. The manifest is three things at once:

  1. The builder's output — the theme builder emits a seed manifest from its fill-in-the-blank form.
  2. The generator's input — a manifest is a complete brief for generating (or regenerating) the theme's code.
  3. The theme's self-description — registration reads it; reviewers check the running theme against it.

The existing THEME.md header block (version, theme, capabilities, vocabulary) remains required and must be a faithful subset of the manifest — generate it from the manifest, never let the two drift.

Schema by section

themeSpec: v1                # manifest schema version
meta:
name: pit-lane # registration name; lowercase letters, digits, and hyphens
displayName: "Pit Lane"
profile: game # informational | interactive | game
contract: v2 # theme contract version targeted
capabilities: [apps, zones, shop, character, universe]

metaphor:
premise: > # one sentence: the world and the player's role
You run a racing team; your infrastructure is a growing circuit empire.
aesthetic:
style: pixel-art # pixel-art | vector-flat | konstruct-native | terminal-retro
palette: ["#0b1020", "#e10600", "#f5f5f5"]
motion: "snappy; nothing over 300ms except celebrations"
branding:
references: ["Civo", "Konstruct"] # brands woven into the world
tone: "tech-company in-jokes welcome"

vocabulary: # superset of the THEME.md block
zone: { singular: track, plural: tracks, verb: build }
app: { singular: car, plural: cars, verb: field }
deploy: { verb: "send to the grid" }
shop: { name: "Pit Shop" } # (game)
universe: { name: "Circuit Map" } # (game)

character: # (game) omit when profile != game or no character
enabled: true
archetype: "racecar driver"
naming: { default: "Rookie", playerNamed: true } # players should be able to name it
slots: [helmet, suit, livery, gadget] # appearance keys: <theme>.<slot>
cosmetics: # every cosmetic names its source
- { key: pit-lane.livery.civo, slot: livery, name: "Civo Works Livery", source: free }
- { key: pit-lane.helmet.gold, slot: helmet, name: "Gold Visor", source: quest }

zones:
metaphor: "race track"
tiers: # must map 1:1 to discovery bands
small: { name: "kart circuit", fits: "a couple of cars" }
medium: { name: "street circuit", fits: "a full team" }
large: { name: "grand prix circuit", fits: "the whole grid" }
upgradeNarrative: "track extension" # what a capacity upgrade looks like

apps:
metaphor: "car"
phaseWords: # must cover every phase
Building: "in the garage"
Pushing: "rolling out"
Deploying: "formation lap"
Live: "racing"
Failed: "in the gravel trap"

shipMoment: # required for ALL profiles; scale to profile
description: >
A boost upgrade slams onto the car; it screams past the grandstand
under a shower of sparks.
durationSeconds: 5 # should be <= 6
skippable: true # must be skippable
blocking: false # must not block other work in the theme

shop: # (game)
sections:
zones: { source: discovery } # priced from discover's bands[].monthly_usd; never hardcoded
freeItems: # must offer at least 2 free items
- { key: pit-lane.livery.civo }
- { key: pit-lane.gadget.radio }
humanServices: # real-world actions rendered as shop items
- { key: svc.book-dietz, name: "Pit-wall session with dietz",
action: { type: link, url: "https://…" } }
- { key: svc.konstruct-team, name: "Book the Konstruct crew",
action: { type: link, url: "https://…" } }
resources: [] # future: metered add-ons from the rate card

progression: # (game)
xpEvents: # standard event keys — see table below
- { event: first-zone, xp: 50 }
- { event: ship-app, xp: 25 }
- { event: app-live, xp: 15 }
- { event: friday-ship, xp: 50 }
quests: # every quest must exercise a real platform feature
- { key: pit-lane.q1, title: "Homologation",
hook: create-zone, xp: 50, reward: pit-lane.helmet.rookie }
- { key: pit-lane.q2, title: "Telemetry",
hook: build-logs, xp: 30 }

universeView: # (game) — see requirements below
name: "Circuit Map"
composition: >
An isometric map. Every track is placed and rendered at its tier;
live cars circulate on their tracks.
growthRule: >
Every zone and every app ever shipped leaves permanent visible
structure. The map only accretes.
dataOverlay: { toggle: true, defaultOff: true,
contents: [zone-names, app-names, urls, status] }

surprises: # (game)
easterEggBudget: 3 # must implement at least this many; never documented
ambient: # standard triggers, interpreted in-metaphor
- { trigger: friday-ship, effect: "night race — floodlights and fireworks" }
- { trigger: tenth-ship, effect: "a blimp appears over the circuit" }

storyline: # free-form details woven through copy and art
slots:
heroName: "mark"
propNumbers: ["33", "7"]
references: ["civo", "konstruct"]

generation: # provenance; written by the generator, if any
promptVersion: 1
model: ""

Field rules

  • meta.capabilities must list only capabilities the theme actually renders, and must be consistent with the profile: character, shop, and universe require the game profile, and any capability involving writes (zone purchase, shipping) requires at least interactive. An informational theme declares apps and, at most, read-only zones.
  • zones.tiers must key by the discovery band keys (small, medium, large). Capacity numbers and prices are never in the manifest — they arrive from discover at runtime.
  • apps.phaseWords must cover Building, Pushing, Deploying, Live, and Failed. The Failed word may be playful; the state must still read as "needs attention," and status.message must be reachable.
  • character.slots name the appearance keys the theme persists, always prefixed: <meta.name>.<slot>. xp and level are global across themes — add to XP, never reset it.
  • shipMoment applies to every profile. For informational and interactive themes a manifest may reduce it to a short delight (confetti, a satisfying check), but it must exist, must be skippable, and must not block other work.
  • shop.humanServices are shop items whose "purchase" is an external action (action.type: link today). They let a theme sell time with a human — office hours, an expert session — alongside zones.
  • progression.quests[].hook must name a real platform interaction: create-zone, ship-app, scale-app, build-logs, redeploy, public-url. Quests exist to walk the player through the platform's actual features.

Standard XP events

Themes that declare character should award XP through these standard events, at these suggested values, so progression feels consistent across themes:

EventWhenSuggested XP
first-zoneThe org's first zone is created from this theme50
ship-appA ship is submitted25
app-liveA shipped app reaches Live15
scale-appFirst time the player scales replicas20
quest-completeAny quest completesper quest
friday-shipAn app ships on a Friday (player's local time)50

The Friday ship is a big deal. Shipping on Friday is the platform's standing joke turned badge of honor. A game theme must recognize it — extra XP and an extra flourish on the ship moment. Other profiles should acknowledge it in their own register.

Universe view requirements (game)

A game theme must provide one dedicated screen — named in vocabulary.universe — that shows the organization's whole world at once:

  1. Everything, at a glance. Every zone is rendered, visually distinct by tier. Apps appear on or in their zones.
  2. The world only grows. Zones and shipped apps leave permanent visible structure (growthRule). Returning after a month of work, the screen must look meaningfully bigger and more impressive than it did.
  3. Data overlay, one tap, default off. A single control toggles names, URLs, and statuses over the scene. With the overlay off — the default — the view must contain no organization names, app names, URLs, or usernames.
  4. Composed for capture. The scene must look intentional at 16:9 and at 1200×627. A theme may offer an in-view capture button (canvas toBlob download).

Ship moment requirements

For every profile:

  • Trigger on ship submission; resolve honestly (the celebration must not imply Live before the platform reports it — hand off to the phase words).
  • must be skippable and non-blocking; should run ≤ 6 seconds.
  • (game) The moment is the theme's signature. It must be metaphor-specific (shipMoment.description), and it must have an upgraded variant for friday-ship.

The builder blanks

The theme builder's fill-in-the-blank form maps directly onto the manifest. A completed sentence is a seed manifest:

"I want a theme contract theme. My character is a racecar driver; the zones are race tracks of different sizes that fit a certain number of cars; when I ship an app, an upgrade slams onto the car and gives it a boost; the storyline includes a driver named mark, car numbers 33 and 7, Civo and konstruct branding, tech-company fun."

BlankManifest field(s)
the theme idea itselfmetaphor.premise, vocabulary
"my character is a ___"character.archetype (drives character.slots)
"the zones are ___"zones.metaphor, zones.tiers
"shipping an app should ___"shipMoment.description
"the storyline includes ___"storyline.slots, metaphor.branding
profile / aesthetic choicesmeta.profile, metaphor.aesthetic

Everything else in the manifest — cosmetics, quests, tier visuals, surprises — is elaboration a generator (or an author) completes within these constraints.

The checklist

A theme passes when all of these hold. Items marked (game) apply to the game profile.

  1. Repository builds with Cloud Native Buildpacks, listens on PORT, serves the frontend from the build artifact (assets embedded).
  2. theme-manifest.yaml validates; THEME.md header matches it.
  3. Uses the v2 contract: no credential handling; all platform calls go through static/theme.js (copied byte-for-byte from the starter theme; the platform verifies the file's contents and rejects a modified copy at build time).
  4. Escapes every API-derived string (names, status messages, URLs, env values) before DOM injection — no innerHTML with untrusted data.
  5. Flavor may be fictional; capabilities may not. Never depict platform features the contract does not actually perform — no imaginary rollout strategies, CI stages, or controls that do nothing. If a button exists, it does the real thing.
  6. Renders a welcome/sample mode when theme.isLaunched() is false.
  7. Calls discover first; every band, size, and price on screen traces to the discovery document (zone prices from bands[].monthly_usd).
  8. Every status.phase renders through apps.phaseWords; a Failed app is findable within one interaction from anywhere, with status.message visible.
  9. The ship moment fires on ship, is skippable, and does not block.
  10. (interactive+) Fit rule previewed before ship; 409 and Failed handled in-theme.
  11. (game) Character appearance persists under <theme>.-prefixed keys; XP only ever increases; every quest hook reaches a real platform feature.
  12. (game) Shop prices zones from discovery; at least two free items; human services open their links.
  13. (game) Universe view meets all four requirements above, including the overlay-off screenshot rule.
  14. (game) At least easterEggBudget easter eggs exist and are absent from all documentation.
  15. (game) friday-ship awards its XP and upgraded ship moment.

Contract boundaries

The v2 contract allows exactly these operations: discover, zones, createZone, apps, appRepos, shipApp, updateApp, deleteApp, redeploy, buildLogs, metrics, character, saveCharacter. A manifest must not require anything beyond them.

Known evolution items (do not build against these yet): per-app metrics and zone resize through the v2 transport, richer delivery metadata (health, traffic, hostnames), and a per-org theme-state operation for world persistence beyond the character ledger. Until then, a theme may persist small world-state under <theme>.-prefixed character keys.

Example: Ship It! as a manifest

The retro arcade theme, retrofitted. This is a forward-looking exercise — the shipped theme-ship-it repository predates the manifest and does not carry this file (or the universe capability) yet:

themeSpec: v1
meta:
name: shipit
displayName: "Ship It!"
profile: game
contract: v2
capabilities: [apps, zones, shop, character, universe]
metaphor:
premise: A hero claims planets and launches rockets across a growing galaxy.
aesthetic: { style: pixel-art, palette: ["#0a0a1a", "#7cf7ff", "#ffd23f"],
motion: "arcade-snappy; launch cinematic is the exception" }
branding: { references: ["Konstruct"], tone: "retro arcade" }
vocabulary:
zone: { singular: planet, plural: planets, verb: claim }
app: { singular: rocket, plural: rockets, verb: launch }
deploy: { verb: launch }
shop: { name: "Supply Depot" }
universe: { name: "Galaxy" }
character:
enabled: true
archetype: space hero
naming: { default: "Captain", playerNamed: true }
slots: [species, helmet, suit, cape, gadget]
zones:
metaphor: planet
tiers:
small: { name: moon, fits: "a couple of rockets" }
medium: { name: planet, fits: "a small fleet" }
large: { name: gas giant, fits: "the whole armada" }
upgradeNarrative: orbital platform
apps:
metaphor: rocket
phaseWords: { Building: "in assembly", Pushing: "fueling",
Deploying: "on the pad", Live: "in orbit", Failed: "splashed down" }
shipMoment:
description: Countdown, ignition, liftoff — the rocket climbs to orbit.
durationSeconds: 6
skippable: true
blocking: false
universeView:
name: Galaxy
composition: A starfield; every planet placed, rockets orbiting live ones.
growthRule: Planets and every rocket ever launched stay on the map.
dataOverlay: { toggle: true, defaultOff: true,
contents: [zone-names, app-names, urls, status] }
surprises:
easterEggBudget: 3
ambient:
- { trigger: friday-ship, effect: "meteor shower over the galaxy" }
storyline:
slots: { references: ["konstruct"] }

What's next

  1. Creating a theme — the authoring guide and contract reference
  2. The game formula — why the game profile is shaped this way
  3. Themes — the model and trust boundaries