Config reference (kumo.config.json)
Master data for one environment. Push it with kumodeck config push [file] --env <env> or edit it in the dashboard.
The whole document is validated before anything changes; errors come back with a JSON path
(multiplayer.modes[0].maxPlayers: …). Every section is optional, and every feature that costs money per use or
changes what your users (called players in the API) see (external sign-in, sharing tools) is off until you enable it here.
{
"version": 1,
"resetTimeZone": "UTC",
"features": { "saves": true, "hosting": true },
"multiplayer": { "modes": [] },
"auth": { "redirectUrls": [], "providers": {} },
"web": { "allowedOrigins": [] },
"share": {}
}Common types#
| Type | Rule |
|---|---|
| key | lowercase snake_case: ^[a-z][a-z0-9_]{0,47}$. Keys are unique within their section |
| text | a string (1–120 chars) or a map of language code → string: { "en": "Coins", "ja": "コイン" }. Language codes look like en, ja, pt-BR |
| long text | same as text, up to 500 chars |
Top level#
| Field | Type | Default | |
|---|---|---|---|
$schema | string | — | ignored (for editor tooling) |
version | 1 | 1 | format version |
resetTimeZone | IANA zone | "UTC" | e.g. Asia/Tokyo. Unknown zones are rejected |
features#
Every feature is off until you turn it on. Only guest sign-in and reading the config always work. For a feature that is off,
players and secret keys get 403 feature_disabled (with details.feature and details.configPath), and nothing is written —
so nobody can run up your bill by calling an API your app does not use. The multiplayer WebSocket is closed with
4003 feature_disabled; a hosted app whose hosting is off is not served. Turn features on per environment
(development and production have their own config): kumodeck features on <name> edits this file, then kumodeck config push --env <env>.
kumodeck features shows what is on.
| Feature | What it opens | Needs |
|---|---|---|
emailLogin | email sign-up / sign-in / verification / password reset, linking an email to a guest | |
saves | cloud saves (kumo.saves) | |
stats | submitting and reading stats (kumo.stats). The per-minute submit cap (integrity.stats.maxSubmitsPerMinute) always applies | |
multiplayer | rooms, matchmaking, the realtime WebSocket | |
realtimeChannels | realtime channels (kumo.realtime): named channels that deliver messages instantly; nothing is stored. The WebSocket connects when either this or multiplayer is on | |
voice | voice calls in channels created with voice (up to 100 people: peer to peer up to 8, through a relay server from 9; nothing is recorded) | realtimeChannels |
hosting | kumodeck deploy and serving your app on KUMODeck | |
statsVerification | value rules and the verification webhook in integrity.stats | stats |
Unknown names are rejected on push (a typo would otherwise leave a feature off without telling you).
A feature that is not offered right now cannot be turned on (403 feature_unavailable).
Banning a user (dashboard, or POST …/players/:playerId/ban) always works; it needs no feature.
Some features keep their own switch elsewhere in this file: sign-in providers → auth.providers.<name>.enabled,
the X tools → share.<tool>.enabled. KUMODeck Functions are turned on with kumodeck functions enable.
The dashboard (logged in as you) can always read your data while a feature is off, but using a feature from there (deploying) needs it on, just like from a secret key.
{ "features": { "hosting": true, "saves": true, "multiplayer": true } }stats[] (max 128)#
| Field | Type | Default | |
|---|---|---|---|
key | key | required | |
title | text | — | |
aggregation | sum | max | min | latest | sum | how reports combine |
maxPerSubmit | number > 0 | — | largest value accepted in one report (anti-tamper floor) |
multiplayer.modes[] (max 16)#
| Field | Type | Default | |
|---|---|---|---|
key | key | required | used in rooms.quickMatch(key) / rooms.create(key) |
title | text | — | |
minPlayers | 1–64 | 2 | quick match starts when this many are waiting |
maxPlayers | 1–64 | 4 | must be ≥ minPlayers |
fillTimeoutSeconds | 0–300 | 15 | start with fewer players after this long; 0 = wait forever |
transport | server | p2p | server | p2p = players exchange game messages directly over WebRTC (peer-to-peer mode). No referee: do not use it for competitive or prize matches |
p2p.maxPlayers | 2–8 | 4 | mesh size limit; the mode's maxPlayers is capped to it and minPlayers must not exceed it. Joining a full mesh fails with p2p_room_full |
p2p.relayOnly | boolean | true | route everyone through the TURN relay so players never see each other's IP address. Players under 13 are always relayed, whatever this says |
With no modes at all, a built-in default mode (2–8 players, 15 s) is available. Once you define any mode, only
defined modes work.
auth#
Sign-in settings (guide). Guest and email sign-in always work; every external provider is off until you enable it.
| Field | Type | Default | |
|---|---|---|---|
redirectUrls | URL[] (max 32) | [] | pages of your app allowed as return addresses for email links and OAuth. Exact URL (any query), or a trailing * for a path prefix: https://mygame.example.com/*. Localhost in development and your KUMODeck-hosted URLs are always allowed |
providers.google.enabled | boolean | false | Sign in with Google (needs your OAuth client in the dashboard) |
providers.discord.enabled | boolean | false | Sign in with Discord (needs your OAuth client in the dashboard) |
providers.apple.enabled | boolean | false | Sign in with Apple (needs your Services ID and key in the dashboard) |
providers.x.enabled | boolean | false | Sign in with X (needs your own X app's Client ID and Secret in the dashboard; where KUMODeck's shared X app is available, Use without setup works too; sign-in is free) |
Provider credentials are never stored in this file (it is pushed from your machine and kept in history); they are entered in the dashboard and stored encrypted.
{ "auth": { "redirectUrls": ["https://mygame.example.com/*"], "providers": { "x": { "enabled": true } } } }web#
| Field | Type | Default | |
|---|---|---|---|
allowedOrigins | origin[] (max 32) | [] | web origins allowed to use your publishable key. Empty = any origin |
Your app can be hosted anywhere: on KUMODeck, on your own server, on itch.io. With the default (empty list) the SDK works
from any origin. List origins to stop other sites from using your publishable key (you pay for usage): browser requests
from other origins get 403 origin_not_allowed. Your KUMODeck-hosted URL and, in development, localhost are always
allowed. Entries are origins without a path: https://mygame.example.com, https://*.itch.zone (any subdomain),
http://localhost:5173, capacitor://localhost (your own app).
{ "web": { "allowedOrigins": ["https://mygame.example.com", "https://*.itch.zone"] } }share#
Tools that help your app or game spread on X (guide). Everything is off by default; turn on only what you use.
| Field | Type | Default | |
|---|---|---|---|
images.enabled | boolean | false | render 1200×600 card images (default card + score / challenge cards) |
images.title | text (≤ 60) | — | app name drawn on the card |
images.background | path in your deployment | — | PNG / JPEG (≤ 2 MB, 2:1 works best) behind the text |
images.font | path in your deployment | — | extra TTF / OTF (≤ 10 MB). The built-in font covers Latin only; add a font for Japanese etc. |
images.theme | { background, accent, text } | #0f1115 / #ffcc33 / #ffffff | #rrggbb colors |
tags.enabled | boolean | false | no longer used: KUMODeck does not add tags to your pages (a push says so in one line). Put the tags from kumodeck share tags in your <head> |
tags.title / tags.description / tags.imageAlt | text | — | card text |
tags.image | path in your deployment or https:// URL | — | fixed card image (otherwise the rendered default card) |
tags.site | @handle | — | twitter:site |
links.enabled | boolean | false | kumo.share() issues share ids (?ks=) and challenges can be restored |
links.text | text (≤ 200) | — | default post text; {score} are replaced |
links.hashtags / links.via | string[] (max 5) / handle | [] / — | added to the X post |
tracking.enabled | boolean | false | count visits / new users / plays per share id (aggregates only) |
inAppBrowser.enabled | boolean | false | allow "open in your browser" as the same guest from X's in-app browser |
The card tags need an image: either tags.image or images.enabled.
{
"share": {
"images": { "enabled": true, "title": "Space Cats", "background": "/card-bg.png" },
"tags": { "description": "A tiny space game", "site": "@spacecats" },
"links": { "enabled": true, "text": "I scored {score} in Space Cats. Can you beat it?", "hashtags": ["spacecats"] },
"tracking": { "enabled": true }
}
}audience#
general (default), mixed or kids: who uses your app or game. kids treats every user as under 13.
mixed treats users whose age is unknown as children
until they declare an age.
integrity#
Server-side checks for reported stats. They run before a stat is stored; rejected values come
back in rejected from POST /v1/stats and are logged as signals (nothing is acted on automatically).
| Field | Type | Default | |
|---|---|---|---|
stats.rules[] (max 128) | { stat, min?, max? } | [] | allowed range of a stat's value (e.g. a best time under 1 s is impossible → min: 1000) |
stats.maxSubmitsPerMinute | 1–600 | 60 | reports per user per minute |
stats.webhook.url | https URL | — | your server decides (http://localhost allowed in development) |
stats.webhook.timeoutMs | 100–5000 | 1500 | |
stats.webhook.onFailure | accept | reject | accept | what happens when your server is down or slow |
The webhook receives POST { type: "stats.verify", environmentId, playerId, stats, submittedAt } with a
Kumo-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + "." + body)> header (the secret is shown in the dashboard
and at GET /v1/admin/integrity/webhook-secret). Answer { "approve": true } or { "results": { "<stat>": true } }.
Other sections#
| Section | Defines | API |
|---|
See the REST API for request and response shapes.
Validation rules (cross-references)#
- Duplicate keys within a section are rejected
minPlayers≤maxPlayers