Distribution profiles
A distribution profile lets you ship Coilbox alongside a game (or otherwise brand/narrow it) without forking or rebuilding. You drop a single profile.json file next to the app, Coilbox reads it once at startup and applies it: window title, hidden features, a preset game filter, a branded welcome screen, a rearranged home page, theme colours, hidden settings sections, extra sidebar links, custom Markdown pages, and a GitHub-releases update source for the game.
If no profile is present, Coilbox behaves exactly as normal.
New here? Read portable-mode.md first — it explains the
.coilboxfolder this file lives in and how to package everything up. For the exact route/nav ids thehide,hideSettings,welcomeandlinksfields below refer to, see routes.md.
Where the file goes
Profiles ride on portable mode. Put a .coilbox folder next to the Coilbox executable and place profile.json inside it. The presence of that profile.json is exactly what turns portable mode on (a bare .coilbox folder is no longer enough, since the Windows installer keeps sidecars there too):
<YourGameFolder>/
coilbox(.exe) # or Coilbox.app on macOS (the folder beside it)
.coilbox/
profile.json # <- the distribution profile
content/ # optional: a bundled engine, game and maps
data/ # (portable app data, created on first run)
cache/- On macOS,
.coilboxsits besideCoilbox.app, not inside it. - In development (
bun tauri dev), the binary runs fromtarget/debug/, so the file lives attarget/debug/.coilbox/profile.json.
The profile is only read when Coilbox is running in portable mode (i.e. a .coilbox folder exists). A normal per-user install ignores it.
Bundled content
A content/ folder beside profile.json bundles the engine, the game and maps, so a new player has nothing to download. It is not a profile field. The folder being there turns it on, and a package without it behaves as before.
Coilbox reads the folder in place and never writes into it. It searches it after the player's own content folder, so the player's copy of an archive wins. It copies only the engine, once, into the player's download folder on first run, and the home page's setup card says "Setting up engine" while it does. Mistakes in the folder show in the health checklist in Settings > Distribution profile.
Bundle the engine, the game and maps has the layout, the measured sizes and the reasons.
Minimal example
{
"version": 1,
"title": "Splinter Faction - Coilbox"
}Full example
{
"version": 1,
"title": "Splinter Faction - Coilbox",
"mode": "dark",
"accent": "orange",
"hide": ["downloads.browse", "downloads.games", "content.games"],
"hideSettings": ["engines", "engine-settings", "downloads"],
"gameFilter": { "regex": "^Splinter *Faction" },
"welcome": {
"css": ".coilbox-welcome{display:flex;flex-direction:column;align-items:center;gap:20px;padding:64px 32px;text-align:center;font-family:system-ui}.coilbox-welcome img{width:200px}.coilbox-welcome h1{color:hsl(var(--primary));font-weight:800}.coilbox-welcome .cta a{padding:10px 20px;border-radius:8px;font-weight:600;text-decoration:none}.coilbox-welcome .cta a.primary{background:hsl(var(--primary));color:hsl(var(--primary-foreground))}",
"html": "<img src=\"https://splinterfaction.info/images/logo.webp\" alt=\"Splinter Faction\"><h1>Splinter Faction</h1><p>Welcome, commander.</p><div class=\"cta\"><a class=\"primary\" href=\"#/play/skirmish\">Play Skirmish</a></div>"
}
}Custom pages
A distribution can add its own screens — a rules page, a "getting started" guide, a credits page — by dropping Markdown files into a pages/ folder inside .coilbox. Each .md file becomes an in-app page; no profile.json entry is needed (the folder is auto-discovered).
<YourGameFolder>/
.coilbox/
profile.json
pages/
about.md
rules.md
bg.jpg # an asset a page referencesEach file starts with an optional frontmatter block — a ----fenced set of key: value lines — that configures the page:
---
path: rules
title: House Rules
icon: info
group: Info
order: 10
background: pages/bg.jpg
---
# House Rules
1. Be excellent to each other.
2. No sharing accounts.
See the [about page](#/pages/about) for more.| Key | Meaning |
|---|---|
path | Route slug. The page is served at #/pages/<path> (always namespaced under pages/ so it can't collide with a built-in screen). Lowercase a-z 0-9 - /; omitted → derived from the filename. |
title | Breadcrumb and sidebar label. Omitted → the slug. |
icon | Sidebar icon — same name list as links. |
nav | false keeps the page reachable by link/route but adds no sidebar item (a hidden-but-linkable page). Default true. |
group | Sidebar group heading; pages sharing a group collect under it. Omitted → a top-level item. |
order | Sort order among pages (sidebar + routes). |
background | A full-page background image. .coilbox-relative (e.g. pages/bg.jpg) or an inline data:/https: URL. |
Notes:
- Assets are
.coilbox-relative, the same convention as the splash/logo images. A Markdown imageor abackground: pages/bg.jpgresolves to a file under.coilbox/. Audio/video files referenced as images render inline players. - Link between pages (or from a welcome screen) with
#/pages/<path>, or a plain[label](../other.md)link — handy withnav: falsefor pages reached only from elsewhere. See File references for the full link/route scheme. - Compose pages with
@references: include shared fragments (@.coilbox/pages/_foo.md), link to routes (@route/singleplayer), and embed live GUI (@widget/onboarding,@widget/build-tree/<game>). - Content is trusted bundler-authored Markdown (safe by default: no raw HTML/JS). Malformed or duplicate
paths are skipped rather than breaking the app.
File references (@)
Profile content is composable through a single @<namespace>/<rest> token. The first segment after @ is a namespace, so there's no ambiguity between (say) a file named route and the route namespace:
| Token | Means |
|---|---|
@.coilbox/<path> | A file under the portable .coilbox/ folder. A path that escapes the folder (.., absolute) is rejected — it can only read files you shipped. |
@route/<app-route> | An in-app route, e.g. @route/singleplayer, @route/downloads/games. |
@widget/<name>[/<arg>] | An embedded live Coilbox component (see widgets below). |
Anything that can't be resolved (a missing file, an unknown widget, an escaping path) renders a visible error/placeholder, never a silent blank — so a typo in a bundle is obvious rather than mysterious.
File-reference profile fields
The welcome html/css fields accept a @.coilbox/<path> reference instead of an inline fragment, so you can keep the markup in its own file:
{ "welcome": { "html": "@.coilbox/welcome.html", "css": "@.coilbox/welcome.css" } }The referenced .html file's raw HTML is injected as-is — the one place raw HTML is allowed (it's your own trusted, script-free file, same as the inline fragment).
Including files into a page
A custom page can transclude another Markdown file: a line whose sole content is a @.coilbox/<path>.md reference is replaced by that file's contents (recursively), so pages can share a common header, footer, or rules block:
# Getting started
@.coilbox/pages/_shared-intro.md
Now pick a faction below.Includes are cycle-guarded and depth-capped; a missing file or an include loop shows an error marker line instead of hanging.
Linking between pages and routes
Markdown links resolve intelligently:
[Rules](../rules.md)→ the page#/pages/rules(a.mdlink maps to its page by filename; a nested page can be linked with@route/pages/...).[Play](../@route/singleplayer)→ navigates in-app.[Discord](https://discord.gg/…)→ opens in the system browser (never navigates the app's own window away).[Our guide](../@.coilbox/docs/guide.pdf)→ opens the bundled file in whatever program the OS opens that file type with. Pictures, PDFs, plain text, HTML, audio and video open. Any other type, and anything the OS turns down, is shown in the system file manager instead, with its folder open and the file selected. Coilbox will not open a file the OS would run rather than show, such as a.exeor a.sh, so a link cannot be used to start a program. A link to a bundled file is never followed in the app's own window, because that would draw the file over Coilbox with no way back.
Widget catalogue
Drop a live piece of Coilbox GUI into a page by putting a @widget/<name> token on its own line:
# Welcome
@widget/onboarding
## Maps
@widget/map-pack
## Factions
@widget/faction-button/Beyond All Reason| Widget | Renders |
|---|---|
@widget/onboarding | The first-run "Set up Coilbox" + get-started download cards. |
@widget/welcome | The branded welcome screen. |
@widget/map-pack | The curated map-pack download banner. |
@widget/build-tree/<game> | A game's full build-tree graph (all factions, tabbed) in the page. |
@widget/faction-button/<game> | Per-faction buttons that open the build-tree drawer. |
The <game> arg matches a game's name or shortname (case-insensitive); on a single-game install it can be omitted. While the game is being scanned, or if it isn't installed, the widget shows a skeleton/notice rather than a blank.
Verifying it's active
Open Settings > Distribution profile. It shows whether a profile is loaded, where it came from (file / default), and a summary of everything it's changing. If no profile is loaded it reads "No distribution profile loaded — standard Coilbox".
The "Home page" row covers the home key: which layout drew the page, how many zones it drew, and whether that was your zones list or the default that moves with Coilbox. It reads "Default" for a profile that has no home key. A layout name this build does not ship reads as "Default layout" here, because that is the page you are looking at, and the name you wrote is named in the validation row instead.
"Validate profile", below the summary, adds a home row naming anything Coilbox dropped or could not read. That row is absent when there is nothing wrong, and for a profile with no home key at all. See when a home key is wrong.
Writing and iterating on a profile
Settings > Distribution profile carries two authoring controls. Which one you see depends on whether a profile is loaded.
No profile yet: Create profile.json. Writes a starter profile into .coilbox/ beside the app, filled in from how Coilbox is set up right now: the title, colour scheme, accent, advanced mode and fullscreen you can see on screen, plus a gameFilter when exactly one game is installed. It never overwrites a profile that is already there. Coilbox has to restart to pick the new file up, because a .coilbox folder with no profile.json in it is not a portable install yet, so the button offers the restart.
Profile loaded: Reload profile. Re-reads profile.json and applies it to the running app, so the edit loop is a reload rather than a restart. Everything applies, including the parts that otherwise only run at startup: theme, hidden nav, top bar, links, custom pages, welcome, home zones and splash. You stay on the page you were on, so you can sit on the screen you are styling and reload after each edit. Anything in progress elsewhere in the app resets, exactly as a restart would reset it.
Both controls disappear when the profile sets "authoring": false, which is what you ship.
Fields
Every field is optional except version. Unknown fields are ignored, and a malformed file falls back to defaults rather than breaking the app.
version (number, required)
Schema version. Currently 1.
title (string)
Overrides the OS window title and the in-app title. Defaults to "Coilbox".
{ "version": 1, "title": "Splinter Faction - Coilbox" }icon (string)
Replaces the Coilbox icon on the running app: the dock icon on macOS, and the window and taskbar icon on Windows and Linux.
{ "version": 1, "icon": "icon.png" }The value is the path to a PNG file, relative to the .coilbox/ folder. Paths can't escape .coilbox/ (no .. or absolute paths). Use a square image. Coilbox's own is 512 pixels a side.
What it cannot change, because the operating system reads these from the app file and not from the running app:
- the icon in Finder, Explorer, the Start menu and a pinned shortcut
- the app name in the macOS menu bar and under the dock icon
- the dock icon after the app quits, if the player keeps it in the dock
The player sees the Coilbox icon for a moment at launch, before the profile is read. Some Linux desktops running Wayland take the taskbar icon from the app's .desktop file and ignore this one.
macOS draws the image as it is, with no rounded mask, so give it the shape and margin you want in the dock.
If the file is missing or is not a PNG, the Coilbox icon stays and Settings > Distribution profile says why.
mode (string)
Forces the colour scheme: "light", "dark", or "system". Applied on every launch (it overrides a value the user may have set previously — including one carried over from a vanilla Coilbox install). The user can still switch it for the current session under Settings > Appearance; it reverts to the profile next launch. Omit to leave the colour scheme entirely under the user's control. Coilbox puts the user's own value aside the first launch a profile forces it and hands it back on the first launch that does not, so running a distribution and then an ordinary Coilbox install leaves the ordinary install the colour the user picked. A change made inside the branded run stays with that run.
{ "version": 1, "mode": "dark" }accent (string)
Forces a built-in accent colour — the --primary brand hue. This recolours the whole shell cohesively (primary, ring, sidebar, in both light and dark) using picoframe's vetted palette. Same force-each-launch / session-override behaviour as mode. The full set of accents:
neutral (the default — no hue, the plain grey shell), blue, green, rose, violet, orange, red, amber, yellow, teal, cyan, sky, indigo, purple, pink, plus two animated/gradient accents rainbow and opal.
{ "version": 1, "accent": "orange" }These are picoframe's accent axis. The neutral base tint (zinc, slate, gray, stone, …) is a separate control and isn't set from a profile — reach for
theme(below) if you need to touch base tokens.
Prefer
accentover hand-rollingthemecolours for the brand accent — it's one line and stays consistent across light/dark. Usetheme(below) only for tokens the named accents don't cover.
hide (string[])
Hides top-level navigation items (sidebar + welcome launcher) by id, and makes their routes redirect home. Currently these nav ids can be hidden:
| id | Sidebar item |
|---|---|
downloads.browse | Downloads > Browse Rapid |
downloads.games | Downloads > Games |
library.games | Library > Games |
content.setupPacks | Coilbox hub's "Share a pack" button |
multiplayer.stats | Records > Player stats |
conquest.list | Play > Conquest |
runlite.list | Play > Warpath |
career.overview | Records > Career |
campaign.builder | Campaign Builder > Builder |
analytics.matchStats | The match statistics on a replay's page (the chart, build orders and recorded events, and the roster's totals and APM), the Replays page figures and the player dossier's match figures |
analytics.spatialLayers | The layers on a replay's map: start positions, buildings ordered, building density and order density, with their toggles. The start boxes stay |
analytics.mapInsight | The "How this map is played" section on a map's page: one picture of every match on that map in the library. Hiding multiplayer.stats hides it too, because it is built from the same records |
analytics.run | The button that runs the engine over a replay to analyse it, and everything that steers a run. An analysis that is already stored still shows |
{ "version": 1, "hide": ["downloads.games", "library.games"] }The four
analytics.*ids hide parts of a page, not nav items, so a distribution can switch the replay analytics off piece by piece.analytics.runis separate from the rest because it is the only part that costs anything: it spends CPU time running the engine over a replay. All four work today. Where a replay's numbers come from says what each part of the replay analytics shows.
Adding a new hideable nav item is a one-line change in that plugin's
index.ts(useVisible: () => !isProfileHidden("<id>")), so this list can grow on request. See routes.md for the full list of pages and nav ids.
Renamed ids still work. Content became Library, so
content.maps,content.games,content.blueprints,content.archivesand thecontentsettings section are nowlibrary.*andlibrary. A profile written against the old names keeps working, inhide,hideSettingsandartalike, and the profile health check names the replacement rather than reporting the id as matching nothing.content.setupPacksdid not change, because it never named a nav item.The paths moved too, so
#/content/mapsis now#/library/mapsand so on. A@route/content/...ref and a hand-written#/content/...link both still resolve, because the old paths redirect, but they cost a redirect on the way. Prefer the new ones.
hideSettings (string[])
Hides settings sections from the Settings navigation by id. Any app settings section can be hidden:
| id | Settings section |
|---|---|
general | General |
content-folders | Content Folders |
engines | Engines |
engine-settings | Engine Settings |
downloads | Downloads |
lobby-servers | Lobby servers |
uberstress | uberstress |
mapconv | mapconv |
updates | Updates |
game-updates | Game updates |
profile | Distribution profile |
frame.appearance | Appearance (theme/accent) |
{ "version": 1, "hideSettings": ["engines", "engine-settings", "downloads"] }Hiding the built-in Appearance section pairs well with forcing mode / accent — it removes the theme controls so the brand's look is fixed:
{ "version": 1, "mode": "dark", "accent": "orange", "hideSettings": ["frame.appearance"] }Note: hiding is presentational — a hidden section is still reachable by a direct
#/settings/<id>link.
gameFilter (object)
Narrows game lists to a single game. When set, the multiplayer Battles list, the game picker and the Coilbox hub browse screen only show matching games. Matched case-insensitively against the game name.
{ "version": 1, "gameFilter": { "regex": "^Splinter *Faction" } }{ "version": 1, "gameFilter": { "names": ["Splinter Faction"] } }regex— a case-insensitive regular expression tested against the game name.names— exact (case-insensitive) game names.
Provide either or both; an entry matches if the regex matches or any name matches.
factionLogos (object)
Sets the emblem shown for each faction, by side name. Coilbox shows these wherever a player picks or sees a faction: skirmish setup, battle rooms, a game's page, Conquest, Warpath and scenario setup.
{
"version": 1,
"factionLogos": {
"arm": "factions/arm.png",
"core": "factions/core.png"
}
}- The key is the side name as the game names it. Case does not matter, so
armandArmare the same side. - The value is a path relative to the
.coilbox/folder, or an inlinedata:orhttps:URL. These are the same rules assplash.image.
An emblem set here always wins. Coilbox otherwise takes a faction's emblem from the game archive's side pictures or the branding catalog, and this field overrides both. A side you do not list keeps the emblem it would have had.
welcome (object)
Replaces the default home launcher with a branded landing page. Declarative HTML + CSS only (no JavaScript).
{
"version": 1,
"welcome": {
"html": "<img src=\"https://example.com/logo.webp\"><h1>My Game</h1><p>Welcome!</p>",
"css": ".coilbox-welcome{padding:48px;text-align:center}"
}
}Instead of inlining the markup, point html/css at a file in your .coilbox/ folder with a @.coilbox/<path> reference — much easier to edit and version than a one-line JSON string, and the only place raw HTML is allowed (it's your own trusted, script-free file):
{
"version": 1,
"welcome": { "html": "@.coilbox/welcome.html", "css": "@.coilbox/welcome.css" }
}<YourGameFolder>/
.coilbox/
profile.json
welcome.html # your landing-page markup
welcome.css # its styleshtml— injected into the welcome page body. Style it via.coilbox-welcome …. Either an inline fragment (above) or a@.coilbox/<path>.htmlfile reference to keep the markup in its own file.css— injected alongside the HTML. Inline, or a@.coilbox/<path>.cssreference.Local media: reference files bundled in your
.coilbox/folder by relative path, and Coilbox resolves them at load time — in both the HTML and the CSS. So<img src="images/logo.webp">,background: url(images/hero.gif), and even@font-face { src: url(fonts/brand.woff2) }all pull from.coilbox/images/…,.coilbox/fonts/…, etc. Images, animated GIFs, audio (<audio src="…">), video (<video>) and web fonts are supported. A@.coilbox/<path>file reference works in the same places and means the same file. Absolute URLs (https:,data:), app-absolute paths (/…) and@route/,@widget/references are left untouched, so remotehttpsimages still work directly with no embedding, and anavigatemarker'shrefstill reads as a route.Stylesheets: a
<style>block and a<link rel="stylesheet" href="theme.css">both work, and both work wherever you put them, before your markup or after it. A relativehrefresolves against your.coilbox/folder like any other asset, so the stylesheet's own relativeurl()s resolve beside it. They are the only two. A<base>, a<meta>or a<script>is removed wherever you write it, because a<base href>re-points every relative URL in Coilbox and a<meta http-equiv="refresh">navigates the app away from itself, neither of which is a thing your markup should be able to do to the page around it. Your markup never runs JavaScript, so a<script>would do nothing anyway.In-app links: because Coilbox uses hash routing, an
<a href="#/play/skirmish">navigates inside the app without a reload. Useful routes include#/play/skirmish,#/library/maps,#/play/replays,#/battles,#/settings— the full list is in routes.md.Links that go nowhere: an
<a>Coilbox cannot follow is ignored, and says why in the developer console. Anhttps:orhttp:link opens in the system browser, amailto:link in the mail client and atel:link in the dialler, so Coilbox stays on screen behind whatever opens. A#/…hash link moves around inside the app. Everything else is a mistake it will not act on, because following it would replace Coilbox with whatever is at the other end and only a restart would bring it back. A relativehref="images/logo.webp"is the picture rather than a page, anhref="@route/play/replays"needs thenavigatemarker below to mean anything, and an app-absolutehref="/play/replays"is a full page load, so write that one as#/play/replays.Actions: the welcome HTML can't run JavaScript, but any element carrying a
data-coilbox-actionattribute is wired to a built-in action when clicked — the interactive hook available without scripting.data-coilbox-action="quit"closes Coilbox. Use it to add your own exit control to a branded landing page (handy for fullscreen builds). This works regardless of thequitflag.data-coilbox-action="navigate"goes to an in-app route named in adata-coilbox-routeattribute (or the element'shref), using the same@route/<path>/.md//pathscheme as custom pages — so@route/singleplayerand/downloads/gamesboth resolve to the same route a page link would. A route that doesn't resolve is ignored (no crash). This makes a "Play now" button possible on the welcome screen from any element, not just an<a href="#/…">. A#/…hash link is not one of those spellings and does not need to be: it navigates on its own, with or without the marker.
The same markers work in the
homekey's markup, and mean the same thing there.json{ "html": "<button data-coilbox-action=\"quit\">Exit</button> <button data-coilbox-action=\"navigate\" data-coilbox-route=\"@route/singleplayer\">Play now</button>" }
The HTML is trusted (it ships inside your distribution): apart from rewriting relative asset URLs (above), it is injected as-is, so only put content you control in it.
onboarding (string)
Where the first-run onboarding sits on the branded home. The onboarding is the "Set up Coilbox" card (offers to create a content folder / download an engine) plus the get-started card (curated game/map download suggestions). Both self-hide once setup is complete, the player has a game, and they have at least three maps.
Your welcome is always shown and is never replaced by the onboarding — this field only positions the cards relative to it:
"below"(default) — under the welcome."above"— over the welcome."off"— hidden entirely, leaving the welcome as the whole home.
{ "version": 1, "welcome": { "html": "…" }, "onboarding": "off" }Without a welcome, the cards sit at the top of Coilbox's own home page above the tool grid: "off" hides them there too, and "above" and "below" have no welcome to position against, so both leave them where they are. An omitted or unrecognized value is treated as "below".
Leaving onboarding out of a home.zones list hides the cards as well, because omitting a zone hides it. See Leaving out onboarding.
start (object)
Names a campaign or a scenario you bundle as where a new player starts. The home page shows a "Start here" card for it in the place the continue card takes, on a fresh install where there is nothing to resume yet.
{ "version": 1, "start": { "campaign": "b2f6c1d0-7e3a-4c55-9a41-0d8f5e2a6c17" } }| Field | Meaning |
|---|---|
campaign | The id of a campaign in .coilbox/campaigns/. Required. |
mission | The id of one mission in that campaign. Leave it out to start on the campaign's first mission. |
scenario | The id of a scenario in .coilbox/scenarios/. Use it in place of campaign, never beside it. |
Give campaign or scenario, not both. campaign and mission are the id values inside the campaign's exported .json, not its title or file name. See Bundling a campaign in a distribution for how the campaign gets into the package.
What the card does:
- It shows the campaign's title and the mission's title, with the campaign's icon. A campaign with no icon shows its background image instead, and one with neither gets a plain icon.
- It opens the mission's briefing.
- It stays until the player wins that mission. A mission they tried and lost keeps the card. After that the
continuecard goes back to offering whatever the player was last doing. - While it is there it comes first, ahead of anything the player could resume. Those move to the
resumecards beside it. - If the engine, the game or the map the mission needs is not installed, the card says what has to be downloaded first. The briefing it opens does the download and holds the mission back until it is done.
Starting on a scenario
A scenario you bundle on its own, with no campaign around it, can be the starting point too.
{ "version": 1, "start": { "scenario": "0c9d4a7e-52b1-4f3a-9e68-7a1d2c3b4e5f" } }scenario is the id inside the scenario's exported .json. See Ship a scenario in a distribution for how the scenario gets into the package.
What the card does for a scenario:
- It shows the scenario's name, with its game and map under it.
- It opens the Scenarios page with that scenario ready to play.
- It stays until the player wins the scenario. Coilbox reads the result from the replay when the game closes. If the replay does not say who won, coilbox asks the player and records a win when they press I won.
- If the engine, the game or the map is not installed, the card says what has to be downloaded first. It then opens the Scenarios page, where the scenario's row has the download button.
A win only counts when the player plays the scenario from the Scenarios page. A start that names both a campaign and a scenario shows no card.
The card is part of the continue zone, so it needs that zone on the page. A home.zones list that leaves continue out, or a welcome that replaces the page, shows no card. A welcome can link to the mission itself with #/campaign/<campaign id>/<mission id>.
If start names a campaign or scenario that is not bundled, or a mission the campaign does not have, the home page shows no card and is otherwise unchanged. The health checklist in Settings > Distribution profile has a start row that says which it was. A campaign or scenario that exists only on your own machine counts as not bundled, because your players will not have it.
With the mission's engine, game and map in bundled content, a new player's card is ready to play with nothing to download. The health checklist then has a second row that names any game or map start leads to that is neither in the bundle nor on your machine.
Leave start out and nothing changes.
Teaching your game covers writing the missions and bundling the campaign that start names.
home (object)
Rearranges Coilbox's own home page: which layout it uses, what it paints behind the page, and which zones it shows in what order.
Leave home out and you get the home page as Coilbox ships it. A distribution with no home key renders the same page as an unbranded install, byte for byte. That has been checked three times on this feature by comparing the rendered markup, not reasoned about, so it is safe to add the other profile fields without thinking about the home page at all.
home and welcome are different tools:
welcomereplaces the home page with your own markup. Nothing of Coilbox's page is left.homekeeps Coilbox's page, with its resume cards, tool grid and map suggestion, and lets you reorder it and slot your own markup into it.
A profile with both gets the welcome, and home is ignored: a page that has been replaced has no zones left to arrange. Wholesale replacement is still the escape hatch when you want the page entirely to yourself.
{
"version": 1,
"home": {
"layout": "stacked",
"background": "@.coilbox/art/backdrop.jpg",
"zones": [
{ "zone": "greeting", "title": "Ironhold", "tagline": "Hold the line." },
{ "zone": "continue" },
{ "zone": "cards", "art": { "runlite.list": "@.coilbox/art/warpath.png" } },
{ "html": "@.coilbox/home/community.html" }
]
}
}| Field | Meaning |
|---|---|
layout | The named arrangement of the zones. Omit it to track the Coilbox default. |
background | A @.coilbox/<path> reference to a backdrop image, or false for none. |
zones | The page, in order. Omit it to track the Coilbox default. |
The zones
Six zones make up the page. Each one is self-contained and draws nothing when it has nothing to say, so a page never has a hole in it.
| Zone id | Draws | Draws nothing when |
|---|---|---|
onboarding | The "Set up Coilbox" card (content folder and engine) and the get-started download suggestions. | Setup is done, the player has a game and at least three maps, the player dismissed the card, or onboarding is "off". |
greeting | The page heading and the line under it. Greets by lobby name once logged in. | Never. An app always has a title, though the line under it depends on your other zones. See What the greeting says depends on the rest of your list. |
continue | One card for the thing you were last doing: a Warpath run, the next campaign mission, a conquest, a battle you can still rejoin, or your last skirmish setup. | There is nothing to resume, which includes every fresh install unless start names a mission. |
resume | Up to three runners-up the continue card did not take, plus a "log in as" card when you are logged out with a saved login. | There are no runners-up. |
cards | Every sidebar destination as a card, in the sidebar's own groups. A group's external links share one card at its end. | The build has no navigation left outside Home. |
suggested | One curated map to download that the player does not already have, chosen by date. Sits in the tool grid's Downloads group, or at the top of the page when the player has no maps at all and the onboarding zone is offering none. | The player already has every curated map, or the catalog offers none that can be installed and pictured. |
The ids in the first column are what you write in zones.
Each entry is an object naming one zone, plus whatever that zone takes:
| Entry key | Applies to | Meaning |
|---|---|---|
zone | any entry | The zone id from the table above. |
html | an entry with no zone | Your own markup, as a block between zones. See Markup around and between zones. |
before | any entry | Markup at the head of that entry. |
after | any entry | Markup at the foot of that entry. |
title | greeting | The heading, replacing the one Coilbox would have chosen. |
tagline | greeting | The line under the heading. |
art | cards | Per-tool card pictures. See Card art for one tool. |
A key written on an entry that does not take it is ignored, so title on cards does nothing, and html on an entry that names a zone does nothing either. Coilbox says which key and which entry, on the console and in the profile panel, and says it whatever the value is: the fix is to move the key, not to correct it.
The zone list
zones present means that list is the page, in that order:
{
"version": 1,
"home": {
"zones": [
{ "zone": "greeting" },
{ "zone": "cards" },
{ "zone": "onboarding" }
]
}
}Two rules follow from that, and they are the whole contract:
- Omitting a zone hides it. There is no separate on/off flag, and no way to say "everything except one". The example above shows three zones and hides
continue,resumeandsuggested. - A zone Coilbox adds later will not appear on your page. Writing
zonespins the page to the zones you knew about. Leavingzonesout tracks Coilbox and picks up whatever comes next.
That is the same pin-or-track trade layout makes, and you opt into it by writing the key. If all you want is one zone gone, you still have to write the other five out.
Leaving out onboarding
Leaving onboarding out of your zones list hides the "Set up Coilbox" and get-started cards, the same way leaving out any other zone hides it. Those cards are how a fresh install offers to create a content folder and download an engine, so a player on a machine with neither sees a home page that does not mention it.
Coilbox does not make an exception for this, and here is the reasoning so you can judge it:
- The player is still told. Singleplayer says "No engine found. Add a content folder with an engine in Settings > Content Folders first.", and Settings > Distribution profile reports the folders Coilbox can see. What omitting the zone costs is the offer on the home page, not the diagnosis.
- The state is reachable three other ways already:
"onboarding": "off", the card's own Dismiss button, and awelcomethat replaces the page. An exception here would close one door of four. - "Omitting a zone hides it" is the one rule that makes the zone list explainable in a sentence. A zone that came back when Coilbox decided the install was unhealthy would make it "omitting a zone hides it, sometimes".
There is one thing that goes the other way. On an install with no maps at all, the suggested map card moves to the top of the page, ahead of the resume cards, because without a map nothing can be played. It only does that while the onboarding zone is offering no maps of its own, since the get-started card lists several with a packs banner under them and one page should not ask twice. Dropping onboarding, or setting it "off", is one way to get the promoted card. So is a player who dismissed "Set up Coilbox" and has no engine, where the zone is on your page and drawing nothing.
The card gives that place up at the player's first map, and not at the three the get-started card counts to. The two numbers measure different things: the get-started card is asking whether the player has a library, and the top row is asking whether a map is worth more right now than the run they were in the middle of. Once they have a map there is something to come back to. The card carries on offering from the Downloads group either way, a map a day, so what ends at one map is where it sits and not what it offers.
So keep onboarding in your list unless you mean to remove the offer, and use "onboarding": "off" when you do, since that says what you meant and works whether or not you write zones.
What the greeting says depends on the rest of your list
The greeting is the one zone that talks about the others, so leaving a zone out changes the line under the heading:
| Your list has | The line under the heading |
|---|---|
continue or resume, with something waiting | "Pick up where you left off." |
cards | "Choose a tool to get started.", or "No tools available yet." if the grid draws no card |
| none of those three | Nothing. The heading stands on its own. |
Every one of those sentences is about a zone. Left on a page that does not carry it, each points at something the player cannot see: a resume that is not offered, or a grid you chose not to show described as an install with no tools in it. So Coilbox drops the sentence rather than the zone.
Write tagline when you want a line there anyway. It replaces whatever Coilbox would have said, on any page, including one with none of those zones.
Markup around and between zones
Every entry takes before and after markup, and an entry with html instead of zone is a block of your own markup sitting between zones. Together they cover a sentence at the head of a zone and a community feed at the foot of the page.
{
"version": 1,
"home": {
"zones": [
{ "zone": "greeting", "before": "<p class=\"kicker\">Ironhold Command</p>" },
{ "zone": "cards", "after": "@.coilbox/home/roster.html" },
{ "html": "@.coilbox/home/community.html" }
]
}
}- Inline markup and file references are interchangeable, in all three keys. A value starting with
@is a file reference, anything else is markup. Keep a one-line intro inprofile.jsonand a long block in its own file. - Markup renders whether or not the zone beside it drew anything. A
beforeoncontinuestill shows on a fresh install with nothing to resume. Tying it to the zone would make it mean "sometimes", and would leave no way at all to say "always". So write markup that reads correctly next to an empty zone. - The markup goes through the same trusted path as
welcome.html: relative asset URLs resolve against your.coilbox/folder,<style>blocks are rewritten the same way,data-coilbox-actionmarkers work, and there is no JavaScript. - Coilbox puts your markup in a
div.coilbox-home-markupand styles it in no other way. A customhtmlentry gets no margin at all, because the layout has no idea whether the block is a hairline or a feed. Bring your own spacing. beforeandafterwork on a custom entry too, and are worth reaching for when itshtmlis a file:{ "html": "@.coilbox/home/feed.html", "before": "<p>From the forum</p>" }keeps the sentence inprofile.jsonnext to the reference, and leaves the file to whatever generates it.- A reference that cannot be read renders a visible error in place, rather than blanking.
One layout consequence worth knowing. Coilbox normally puts the continue card and the resume rail on one row, and folds the suggested map card into the tool grid's Downloads group. An entry carrying before or after markup stops pairing, so those two zones stack separately instead. Put your markup on a neighbouring entry, or on a custom html entry, if you want the pairing kept.
The suggested card is promoted into that same row for a player with no maps at all whom onboarding is offering none, so markup stops that too: markup on continue or resume leaves no row to promote into, and markup on suggested keeps the card next to the words you wrote about it.
Card art for one tool
The cards entry takes an art map from tool id to a picture:
{
"zone": "cards",
"art": {
"runlite.list": "@.coilbox/art/warpath.png",
"play.replays": false
}
}- A file reference wins that card outright. Only
@.coilbox/names a file, so a bareart/warpath.pngis rejected. falsegives the icon-only card, an icon beside a label with no picture.- A tool you do not list is untouched and walks the rest of the resolution chain: art from the player's own install (a last-played minimap, a campaign panorama, a game's loading art), then a bundled illustration, then a pattern generated from the tool id and your accent colour. The last of those always succeeds, so no card is ever blank and you never have to fill the map in.
Tool ids are the nav ids in routes.md, the same ids hide uses. Warpath is runlite.list and Singleplayer is play.skirmish, so check the table rather than guessing from the label.
Any image is safe to put under a card: the label sits in a band measured to stay legible over a pure black picture and a pure white one, in both colour schemes.
Today
artcan only be written on azonesentry, so changing one tool's picture means writing the whole page out and pinning it. That cost is filed as #1071.
The backdrop
background paints one image behind every zone:
{ "version": 1, "home": { "background": "@.coilbox/art/backdrop.jpg" } }It is a mood layer rather than a hero image. Coilbox composites it over the theme background at 5% strength, which is the strongest setting where the worst possible image still leaves every piece of text on the page readable. If you want art at full strength, use welcome, which replaces the page and lets your own CSS decide.
false removes the backdrop entirely and leaves the flat theme background. Omitting it gives a soft wash built from your own --primary and --foreground, so a distribution that only sets accent already gets a backdrop in its colours.
Coilbox checks the file is there at startup. If it is not, you get the default wash and a line in the profile panel, rather than the flat background a typo used to give you, which was indistinguishable from writing false.
The backdrop applies to Coilbox's page only. A profile with a welcome is on the other arm and paints its own background in welcome.css.
Pinning a layout
layout names the arrangement of the zones. Coilbox ships one, stacked, which is a single column down the page.
{ "version": 1, "home": { "layout": "stacked" } }Naming it pins it. If a later Coilbox redesigns the home page, it ships as a new layout plus a change of default, and a distribution that pinned stacked keeps the screen it was built against. Leaving layout out tracks the default and moves with it. Neither is the right answer for everyone, which is why the name exists.
When a home key is wrong
You write this JSON by hand and nothing checks it before you ship. So a mistake never blanks the page: every bad value falls back to what Coilbox would have done, and says what it dropped and why on the webview console.
A release build does not expose that console, so the same list is in the app: Settings > Distribution profile, "Validate profile". The home row names every entry it dropped and every @.coilbox/ file it could not read, in the words the console got. The summary above it says which layout drew the page, how many zones it drew, and whether that was your list or the tracking default, so a zone list that was thrown away for being empty or all-bad shows as a zone count you did not write.
| You wrote | You get |
|---|---|
home as a string, number, boolean or array | The stock page. |
layout that is not a string, or a name this build does not ship | The default layout, and a list of the names it does ship. |
zones that is not an array, or is empty | The stock page. An empty page is indistinguishable from a crash. |
| A zone entry that is not an object | That entry dropped, the rest of the list kept. |
| An unknown zone name | That entry dropped, named in the warning. |
| The same zone twice | The first kept, the second dropped. |
An entry with neither zone nor html | That entry dropped. |
| Every entry bad | The stock page. |
title, tagline, before or after that is not a string | That key ignored, the entry drawn as Coilbox would draw it. |
A key on an entry that does not take it, such as title on cards or html beside a zone | Nothing, and a line saying so rather than one about its value. A @.coilbox/ file it names is not read either. |
art that is not an object | The whole map ignored, every card walks the chain. |
An art value that is neither a @.coilbox/ reference nor false | That tool dropped from the map and walking the chain, the rest of the map kept. |
background that is neither a @.coilbox/ reference nor false | The default wash. |
A @.coilbox/ file that is not there | A visible error block for markup, a card that falls back to the icon for art, and the default wash for the backdrop. |
The rule behind the table is that one mistake costs the thing it was written for and nothing else.
Every row of the table reaches the profile panel. How loudly a mistake shows on the page itself varies with what it cost you: markup you wrote to be read gets an error block where it should have been, while a backdrop that is decoration at 5% strength gets the default wash and says the rest in the panel.
A worked example
A complete distribution using all of this lives in docs/examples/branded-home. Copy its .coilbox folder next to the Coilbox executable and start the app.
branded-home/
.coilbox/
profile.json
home/
community.html # the custom zone at the foot of the page
art/
backdrop.svg # the page backdrop
warpath.svg # the Warpath card's picture{
"version": 1,
"title": "Ironhold",
"mode": "dark",
"accent": "amber",
"background": "hsl(28 18% 7%)",
"home": {
"layout": "stacked",
"background": "@.coilbox/art/backdrop.svg",
"zones": [
{ "zone": "onboarding" },
{
"zone": "greeting",
"title": "Ironhold",
"tagline": "Hold the line.",
"before": "<p style=\"margin:0;font-size:0.75rem;letter-spacing:0.12em;text-transform:uppercase;opacity:0.65\">Ironhold Command</p>"
},
{ "zone": "continue" },
{ "zone": "resume" },
{
"zone": "cards",
"art": {
"runlite.list": "@.coilbox/art/warpath.svg",
"play.replays": false
}
},
{ "zone": "suggested" },
{ "html": "@.coilbox/home/community.html" }
]
},
"links": [
{
"label": "Discord",
"href": "https://discord.gg/example",
"icon": "discord"
}
]
}What each part of it is doing:
- The zone list is Coilbox's own order with a custom block added at the end, so nothing is hidden. Move
onboardingdown the list, or drop a zone, and the page follows. greetingtakes its own heading and line, and carries an inlinebeforefor the kicker above it.cardsgives Warpath its own picture and takes Replays back to the icon-only card. Every other tool is left to the resolution chain.- The
htmlentry is a file reference, so the community block is a real HTML file you can edit without touching the JSON. It carries its own<style>block, an<img src="art/warpath.svg">that resolves against.coilbox/, and adata-coilbox-action="navigate"button that goes to@route/warpath. continue,resume,cardsandsuggestedare bare, so Coilbox keeps its pairings: the resume row is one row, and the map card sits in the Downloads group.
src/home/homeDocs.test.ts resolves this exact file, and every JSON sample above, through the real schema on every test run. If the contract changes under them, the tests fail rather than the documentation quietly going stale.
links (object[])
Adds external links to the sidebar and the home launcher — e.g. a Discord invite or a wiki. Each entry:
| Field | Required | Meaning |
|---|---|---|
label | yes | Sidebar/launcher text, e.g. "Discord". |
href | yes | URL opened in the system browser. Must be http(s) / mailto / tel. |
icon | no | Icon name from the list below; unknown or omitted uses a generic link icon. |
group | no | Sidebar group heading. Links sharing a group merge into one section; omitting it uses a default Links group. These groups sit below the built-in navigation. |
{
"version": 1,
"links": [
{ "label": "Discord", "href": "https://discord.gg/xxxx", "icon": "discord" },
{ "label": "Wiki", "href": "https://wiki.example", "icon": "docs", "group": "Community" },
{ "label": "Donate", "href": "https://example.com/donate", "icon": "heart", "group": "Community" }
]
}Malformed entries (missing label / href, or an href scheme the browser opener won't open) are skipped; the rest still load.
Icon names: discord, forum / forums, chat / message, globe / website / web, docs / book / wiki, news / blog, rss / feed, heart / donate, support / help, users / community, mail / email / contact, link, game / play, calendar / events, star, info, hash / channel, bell / updates, trophy. Anything else falls back to a generic external-link icon. (lucide ships no brand marks, so discord uses a generic chat glyph.)
lobby (object)
Controls the multiplayer lobby-server presets a distribution ships: a preferred "official" server, which stock presets appear, and the chat channels a player joins on login. It narrows and brands the Settings > Lobby servers list without ever locking the player out — they can still add their own servers and remove their own logins.
| Field | Required | Meaning |
|---|---|---|
official | no | The preferred server. Either a built-in id (a string, e.g. "recoil-official") to promote an existing preset, or an inline server object (below). Shown with an Official badge, listed first, and not removable. |
presets | no | Allow-list of built-in server ids to keep. Omitted → all built-ins shown (the default). Present → only these appear (plus official); [] hides every stock preset, leaving just the official one. Never restricts the player's own custom servers. |
channels | no | Channels seeded into the auto-join list the first time a login connects to the official server. A seed, not a lock: the player can leave them afterwards and they stay gone. Each entry is a channel name string, or { "name": "...", "key": "..." } for a keyed channel. |
An inline official server object:
| Field | Required | Meaning |
|---|---|---|
host | yes | Hostname, e.g. "lobby.example.org". |
name | no | Display name; defaults to the host. |
port | no | TCP port. Defaults to 8200. |
tls | no | Connect over TLS. Defaults to false. |
allowSelfSigned | no | Accept a self-signed cert. Defaults to false. |
A single-server distribution — one official server, no stock presets:
{
"version": 1,
"lobby": {
"official": {
"name": "Scary Lobby",
"host": "lobby.scary.example",
"tls": true,
"allowSelfSigned": true
},
"presets": [],
"channels": ["main", "newbies", { "name": "clan", "key": "s3cret" }]
}
}Or just promote and brand an existing built-in as official while keeping one other preset available:
{
"version": 1,
"lobby": { "official": "recoil-official", "presets": ["bar-ssl"] }
}Built-in ids: recoil-official, spring-official, techa, metal-factions, bar-ssl, bar-tachyon. An unknown official id (or an inline object with no host) is ignored, and the rest of the block still applies.
bar was BAR's plaintext port and is no longer offered, because the same server answers on 8201 with TLS. A profile still naming bar resolves to bar-ssl, so it keeps working.
splash (object)
Shows a brand splash over the whole window at startup: a centered image that fades in on a solid background, holds, then fades out. Plays on every launch; the user can turn it off (Settings > General > Display > "Startup splash") and can dismiss it early by clicking it or pressing Escape.
{
"version": 1,
"splash": {
"image": "logo.webp",
"background": "hsl(240 6% 7%)",
"duration": 3000
}
}image— the centered image. Either a path relative to the.coilbox/folder (read locally, so it works offline — put e.g.logo.webpnext toprofile.json), or an inlinedata:/https:URL used as-is. Paths can't escape.coilbox/(no..or absolute paths).background(optional) — solid backdrop CSS colour. Defaults to the profile's top-levelbackgroundif set, else the theme background.duration(optional) — total time in ms (fade in + hold + fade out). Defaults to3000.
Honours prefers-reduced-motion (skips the fades). If the image can't be loaded the splash is silently skipped — it never blocks startup.
sound (object)
Ships your own background music. Coilbox plays it while the player is in Coilbox, and pauses it while a game is running or the window is out of focus.
{
"version": 1,
"sound": {
"tracks": ["sounds/theme.ogg", "sounds/menu.ogg"],
"enabled": true,
"volume": 40,
"shuffle": true
}
}| Field | Type | Meaning |
|---|---|---|
tracks | string[] | Paths relative to the .coilbox/ folder. Without any, the rest of sound does nothing. |
enabled | boolean | Start playing on launch. Default false, so music waits for the player to turn it on. |
volume | number | Starting music volume, 0 to 100. Default 50. |
shuffle | boolean | Play the tracks in a random order rather than as listed. Default false. |
With tracks set, Settings > Sound offers "Bundled" as the music source and picks it by default. enabled and volume are starting values. The player can change the source, the volume or turn music off there, and their choice wins on every launch after.
background (string)
A solid CSS colour painted behind everything from the first frame until the app has rendered. A dark distribution otherwise briefly flashes the default white page while it loads; setting this paints your colour instead. It's also the splash's default backdrop, so one colour covers the boot screen and the splash seamlessly.
{ "version": 1, "mode": "dark", "background": "hsl(240 6% 7%)" }The colour is cached so it applies before the first paint on subsequent launches (the very first launch of a fresh install still shows one brief flash). A vanilla install (no profile) is unaffected.
theme (object)
Fine-grained override of Coilbox's colour tokens app-wide, for anything the named accent doesn't cover. Each key is a CSS custom property; each value is an HSL triple (H S% L%, no hsl() wrapper). These re-point picoframe's design tokens, so the whole shell recolours. For a simple brand accent, prefer accent above.
{
"version": 1,
"theme": {
"--primary": "22 92% 52%",
"--ring": "22 92% 52%",
"--sidebar-primary": "22 92% 52%",
"--background": "20 14% 8%"
}
}Common tokens: --primary, --primary-foreground, --ring, --accent, --sidebar-primary, --background, --foreground, --border. See node_modules/@picoframe/frame/src/theme.css for the full list and the light/dark defaults.
Coilbox also ships built-in named
accents (neutral, blue, green, rose, orange, … — the full axis) selectable under Settings > Appearance. Athemeoverride takes precedence over the user's accent choice.
release (object)
Points Coilbox at a GitHub repository whose releases ship the game's archive (.sdz / .sd7) — for games distributed outside the rapid ecosystem (e.g. straight from GitHub). On launch Coilbox checks the repo's latest release; if none of that release's archives are already installed, a Game updates settings section and a topbar badge offer a one-click download that installs the archive and rescans content.
{ "version": 1, "release": { "repo": "your-org/your-game" } }repo— the GitHub repository as"owner/name".
"Latest" is whatever GitHub marks as the latest release. If that release also carries an asset named exactly profile.json, Coilbox installs it into .coilbox/ alongside the game archive; because the profile is read once at startup, the app then prompts for a restart to apply the updated profile.
updater (boolean)
Turns off Coilbox's check for new releases of itself. On by default. Set it to false when you ship and update the Coilbox binary yourself, so your players are never offered an upstream build you didn't test with your game.
{ "version": 1, "updater": false }With the updater off there's no check at launch, so no "Update available" pill in the top bar and no update toast. Settings > Updates still shows the running version (useful when a player reports a bug) but drops the check and install buttons. To remove that section from the settings nav as well, add "hideSettings": ["updates"].
This is separate from release: your own game archive keeps updating from your repo either way.
hub (boolean)
Turns off the Coilbox hub. On by default. Set it to false if you're shipping a modded game to your own community and don't want a button pointing players at a public gallery of other people's content.
{ "version": 1, "hub": false }Between off and everything there's gameFilter. A profile that sets one pins the hub to that game: the browse screen lists only items for it, plus items not tied to any game at all (some kinds carry no game name, and one that works anywhere works here too). The screen says which game it's pinned to, so a short list doesn't read as an empty hub, and it drops its own game filter box rather than offering a choice that's already been made.
The pin is applied to what the hub sends back, not asked for as a query, because the hub stores a game as a versioned name like SplinterFaction 0.1.78 and a name pinned in a profile would go stale at the next release. That means a pinned Coilbox reads the hub's pages itself and counts what's left, so the item count and the page buttons are about the list on screen. It reads at most 20 pages and says so if there were more.
A coilbox:// link can still point at an item for another game. Coilbox shows that item's page and still imports it, saying which game it's for: the pin narrows what Coilbox advertises, it isn't a permission check on a link somebody was handed deliberately.
hubUrl (string)
Points the Coilbox hub at a server you run yourself, instead of the built-in https://coilbox-hub.vercel.app. Separate from hub, which only turns the feature on or off.
{ "version": 1, "hubUrl": "https://hub.example.com" }A player can still override this with a setting of their own. This only changes the default they start from.
hubAssetsUrl (string)
Points Coilbox at your own copy of the hub's pictures, instead of the built-in https://tomjn.github.io/coilbox-assets/. Set it with hubUrl when you run your own hub, or your hub's items are drawn with pictures from the public one.
{ "version": 1, "hubUrl": "https://hub.example.com", "hubAssetsUrl": "https://assets.example.com/" }A trailing slash is optional. An empty string counts as not set. There is no player setting for this one.
hubImportCounts (boolean)
Stops Coilbox telling the hub when an import from a hub link finished. On by default.
{ "version": 1, "hubImportCounts": false }This is the only thing Coilbox sends to the hub without being asked to, and it exists because the hub can't work it out for itself: it sees the container being fetched, but Coilbox asks the player to confirm before applying anything, so a fetch that ends at the confirm dialog isn't an import. The request is a POST to the item's address carrying the item's id and nothing else, with no body, no account and no identifier of any kind, and Coilbox never reads what comes back.
This is the only switch for it. There's no per-player one: what it would withhold is an item id the hub had just served, with no account and nothing that points back at the player, so there's nothing to ask anybody about. Setting this to false means nothing is sent. Turning the hub off with hub also stops it, since there's then no hub to import from.
hubAssetUploads (boolean)
Removes the switch that lets a player send pictures of your game's units and maps to the hub. Offered by default.
{ "version": 1, "hubAssetUploads": false }Coilbox can read the game and map archives on a player's machine, make pictures of what's inside them, and upload those pictures to the hub under that player's account. That is off until the player turns it on, on every install, so a profile that says nothing here changes nothing: the default is already that nothing is sent.
What this setting decides is whether they're offered the choice at all. Set it to false if you don't want pictures made from your game's archives published. The switch disappears from Settings > Coilbox hub, replaced by a line saying you've switched it off, and the upload path refuses even if the player had turned it on before your profile arrived.
Turning the hub off with hub covers this too, since there's then no hub to upload to.
The refusal isn't a UI one. The check reads this file and the player's saved answer from disk inside the plugin that holds the hub access token, so nothing the app's own frontend does can get past it.
authoring (boolean)
Removes the profile authoring tools from Settings > Distribution profile. On by default. Set it to false in the profile you ship, so a player can't reload or replace your branding by accident.
{ "version": 1, "authoring": false }The rest of the section stays: a player reporting a problem can still see what the profile is changing, and still run the validation checks. Only the Reload and Create buttons go.
mapLists (object[])
Curated map packs offered for bulk download on the Maps download page — a tournament set, a galactic-conquest galaxy, "space maps", etc. Each pack has an id, a title, an optional blurb, and a maps[] array; Download all queues every not-yet-present map in the pack through the normal download queue.
{
"version": 1,
"mapLists": [
{
"id": "tournament-2026",
"title": "Tournament 2026 map set",
"blurb": "The official pool for this season.",
"maps": [
{
"id": "supreme-isthmus",
"title": "Supreme Isthmus v2.1",
"filename": "supreme_isthmus_v2.1.sd7",
"download": { "kind": "map", "springName": "Supreme Isthmus v2.1" }
}
]
}
]
}Each map's download is { "kind": "map", "springName", "searchUrl"? } (fetched by springname via pr-downloader) or { "kind": "url", "url", "filename", "subdir"? } (a direct mirror file); filename enables "already downloaded" detection. This is the same shape and mechanism the branding catalog's suggested.mapLists uses — see Branding catalog; catalog packs are listed first, then a profile's, deduped by id.
A map may also carry blurb, a line of description, and thumb, an array of image URLs of which the first that loads wins. Neither is needed for the pack itself, which shows no pictures. Both are what the home page's suggested map card shows on the day it picks that map. A map with no blurb gets a stock line. A map with no thumb is never suggested at all: the card is a picture of one named map, and there is no honest substitute to draw, so the day's pick moves on to the next map that has one.
excludedMaps (object[])
Keeps maps out of warpath and galactic conquest, the two modes that pick maps on the player's behalf. Adds to the branding catalog's own list rather than replacing it, so a distribution can exclude more maps than the catalog does and never fewer.
{
"version": 1,
"excludedMaps": [
{
"id": "house-rules-banned",
"match": { "names": ["Duck", "Comet Catcher Redux"] },
"reason": "Not part of the league map pool."
}
]
}match is tested against the installed map's spring name: regex is case-insensitive, names are case-insensitive exact matches and win over regex within a rule. Prefer regex when you mean a family of maps, since a map's version is part of its spring name. reason is shown to the player on the map's detail page.
An excluded map stays visible in Content and stays playable in skirmish and multiplayer. A player can opt further maps out themselves on the map detail page, but cannot re-enable one your profile or the catalog excluded. Same shape as the branding catalog's excludedMaps, documented in Branding catalog.
fullscreen (boolean)
Opens Coilbox fullscreen until the player chooses otherwise. Off by default.
{ "version": 1, "fullscreen": true }This sets the starting state and nothing more. The player can still leave fullscreen with F11, the top-bar button or the Fullscreen switch in Settings > General. Coilbox remembers that choice, and it wins over this field on every launch after.
To force fullscreen instead, use fullscreenLocked.
fullscreenLocked (boolean)
Forces Coilbox fullscreen on every launch and removes the ways to turn it off. Off by default. Use it for a kiosk build, such as a game on a show floor.
{ "version": 1, "fullscreenLocked": true, "quit": true }While it is true:
- the window opens fullscreen, whatever
fullscreensays - the top-bar fullscreen button is gone
- F11 does nothing
- the Fullscreen switch in Settings > General is gone
The player's own fullscreen choice is ignored but not erased. Take the field out and their choice comes back on the next launch.
Coilbox sets fullscreen when it starts and does not watch the window after that. If the operating system takes the window out of fullscreen, Coilbox does not put it back until the next launch.
It differs from the other two fullscreen settings:
fullscreensets the starting state, and the player can change it.layout.fullscreenButtonset tofalsehides the button and turns off F11, but does not force fullscreen. The Settings switch stays.
The lock does not hide the quit button. Set quit to true alongside it, so a player has a way out of a window that fills the screen.
advanced (boolean)
Starts Coilbox with Advanced mode on, which shows the developer and modding tools in the sidebar: uberstress, mapconv, animation and the archive explorer. Off by default.
{ "version": 1, "advanced": true }This sets the starting state, like fullscreen. The player can turn Advanced mode off in Settings > General, and their choice wins on every launch after.
quit (boolean)
Adds a Quit button to the bottom of the sidebar that closes Coilbox. Off by default. It's an escape hatch for fullscreen or kiosk (fullscreenLocked) builds, where a player may otherwise have no obvious way out — so unlike the fullscreen toggle, this button is not removed by the kiosk lock.
{ "version": 1, "fullscreen": true, "quit": true }For a fully branded exit instead of (or in addition to) the sidebar button, put a data-coilbox-action="quit" element in your welcome HTML.
layout (object)
Controls the app-frame chrome — the sidebar mode, the breadcrumb, the top-bar history/fullscreen buttons, the menu-button branding, and top-bar logos. These are locks (authoritative every launch, like title/hide, with no user toggle) except sidebarCollapsed, which is a seed. Omit layout and the chrome behaves as normal.
{
"version": 1,
"layout": {
"popover": true,
"sidebarCollapsed": true,
"hideBreadcrumb": true,
"historyButtons": false,
"fullscreenButton": false,
"menu": {
"label": "Splinter Faction",
"icon": "game",
"image": "menu-logo.webp"
},
"center": { "image": "wordmark.webp", "href": "https://example.com" }
}
}| Field | Type | Meaning |
|---|---|---|
popover | boolean | true forces the sidebar into popover mode (a menu button opens it as an overlay); omitted/false is a persistent sidebar. A lock — the user has no toggle. |
sidebarCollapsed | boolean | Seed the sidebar to start collapsed. The user can still expand it, and their choice persists. Only meaningful when popover is off. |
hideBreadcrumb | boolean | Hide the breadcrumb region in the top bar entirely. |
historyButtons | boolean | Force the top-bar back/forward buttons on (true) or off (false). |
fullscreenButton | boolean | false hides the top-bar fullscreen button and makes F11 inert. It does not force fullscreen, which is what fullscreenLocked does. Default true. |
menu | object | Branding for the popover menu button (see below). |
left / center / right | object | A logo/text in the corresponding top-bar slot (see below). |
menu (popover menu-button branding). When the sidebar is in popover mode (popover: true) a menu button in the top bar opens it; this rebrands that button. It is only visible in popover mode.
| Field | Type | Meaning |
|---|---|---|
label | string | The button's accessible name and tooltip. |
labelVisible | boolean | Show the label/logo beside the icon. Defaults to true when label or image is set; set false for an icon-only button that still has your label as its tooltip. |
icon | string | Icon name (same list as links) for the closed state. |
iconOpen | string | Icon name for the open state. |
image | string | A logo shown in place of the label text. Same source rules as splash.image (.coilbox-relative path, or inline data:/https:). Wins over label as the visible content; label stays the accessible name. |
left / center / right (top-bar logos). Each places a logo or text in that top-bar slot. Same shape:
| Field | Type | Meaning |
|---|---|---|
text | string | Text shown when no image resolves. |
image | string | Logo image. Same source rules as splash.image. Wins over text when it resolves; falls back to text if it can't be loaded. |
href | string | Makes the logo a link opened in the system browser (http(s)/mailto/tel; other schemes are ignored). |
ai (object)
Overrides the branding catalog's per-game AI block: which AIs are hardest, which is standard, which must never play, which are mini-games, and which garrison a neutral conquest world.
{
"version": 1,
"ai": {
"ranking": ["BARb", "CircuitAI", "AAI", "SimpleAI"],
"standard": "AAI",
"never": ["Sandbox", "NullAI"],
"minigame": ["ChickensAI", "ScavengersAI"],
"neutral": ["ChickensAI"]
}
}See the catalog documentation for what each field does. The merge is per field: a field set here wins, a field left out falls through to the catalog entry, and anything neither sets falls back to the built-in defaults. An empty array counts as absent, so an override cannot blank a field.
Use this when you package a build and want your own difficulty curve, for example to hide a bot your players should never face, or to name the AI your tutorial expects.
conquest (object)
Supplies system/faction names — and whole lore factions — for Galactic Conquest galaxies generated in this distribution, so they read as your game rather than generic space. Every field is optional and overrides the branding catalog's per-game defaults.
{
"version": 1,
"conquest": {
"starNames": ["Uros", "Ophvor", "Loz"],
"factions": [
{ "name": "Sovereign Syndicate", "color": "#00c853", "side": "Core", "aggression": 0.4 }
]
}
}starNames/starPrefixes/starSuffixes— full system names (used first), then syllables synthesized names are built from.placeNames/placePrefixes/placeSuffixes— the same three for the Cities and Territories map styles. Each falls back to the matchingstar*field, then to built-in place names.factionNames— full faction names, used when nofactionspresets are given.factions— lore factions assigned in order (the player first); each is{ name, color?, side?, aggression? }.
See the Names and factions section for how the pools are drawn and the full merge order (profile over catalog over built-ins).
onlyOwnMaps (boolean)
Hides the four generated map styles (Galaxy, Theatre, Cities and Territories), so a distribution for a World War 2 game is never played across a galaxy. Use it with hand-made maps you bundle.
{ "version": 1, "onlyOwnMaps": true }With the key set to true, the Generate a map drawer in Conquest does not offer a game that has a hand-made map, and the Warpath setup offers only the hand-made maps for the game picked. The key applies to every game the distribution offers. Use gameFilter to narrow the games.
The generated styles stay for a game with no hand-made map to offer, whether the map is bundled, carried in the game archive or imported by the player. In Warpath only a map with a start and a goal counts. A player is never left with nothing to pick.
A game archive can ask for the same thing with onlyOwnMaps in its coilbox/maps/index.json, as Ship a map describes. Either one is enough.
Anything other than true or false leaves the styles on. The health checklist in Settings > Distribution profile has an onlyOwnMaps row that reports a value like that, and confirms the key when it is true.
Leave onlyOwnMaps out and nothing changes.
Bundling a hand-made map
A distribution can bundle a hand-made map. Put the map folder inside .coilbox/galaxies/, the same folder that holds bundled galaxy files. No profile.json entry is needed.
<YourGameFolder>/
.coilbox/
profile.json
galaxies/
two-shores/ # a hand-made map folder
map.json
picture.png
provinces.png
heightmap.png- Any folder directly inside
galaxies/that holds amap.jsonis a map. The folder can have any name. - A bundled map is read-only. A player cannot replace it or remove it, and cannot import a map with the same id.
- If a player already imported a map with the same id, the bundled map is the one coilbox lists.
- The Conquest page lists it with a Bundled label, and Warpath offers it when its manifest has a
warpathstart and goal.
A game can also carry maps in its game archive, under coilbox/maps/. See Ship a map.
To stop players being offered a generated map as well, set onlyOwnMaps.
The hand-made maps guide covers what goes in the folder.