Hosting & deploy
kumodeck deploy <dir> publishes a folder of static files (your built web app or game) as a new version of an environment.
Every version is kept; switching between them is instant. Apps that render on the server (Next.js, Astro, SvelteKit, Nuxt, React
Router, TanStack Start, Hono…) deploy too: see Server-rendered apps.
Hosting is optional. Your app can live anywhere — your own server, itch.io, a CDN — and still use every other part of
KUMODeck (list its origin in web.allowedOrigins if you want to stop other sites from using your publishable key).
Hosted apps are served under your app's own subdomain — or your own domain — with no KUMODeck branding.
Deploy#
kumodeck deploy dist --env production # production (deploy's default is development)
kumodeck deploy dist --env development -m "new boss fight"
kumodeck deployments # list versions (* = live)
kumodeck rollback 12 # make version 12 live again, instantly| Environment | URL |
|---|---|
| production | https://<slug>.kumodeck.app/ |
| development | https://<slug>--dev.kumodeck.app/ |
| local server | https://api.kumodeck.com/play/<slug>/ and https://api.kumodeck.com/play/<slug>--dev/ |
The folder must contain an index.html. Dotfiles and node_modules are skipped (.well-known/ is included);
symlinks are not followed.
Server-rendered apps#
Apps that make their pages on the server — Next.js (with vinext or OpenNext, below), Astro (with its Cloudflare
adapter), SvelteKit, Nuxt, React Router in framework mode, TanStack Start, Hono, SolidStart — deploy the same way you
would to Cloudflare Workers: a Worker plus its static assets.
Off by default: kumodeck deploy turns on serverRendering (and hosting, which it needs) for that environment and says
so in one line, the same way a static deploy turns on hosting. To turn it on yourself:
kumodeck features on serverRendering && kumodeck config push --env development (this also turns on hosting).
kumodeck deploy --env development --dry-run # build, then check with KUMODeck without deploying
kumodeck deploy --env development # build, upload what is new, make it liveRun it in the project folder without a folder argument. The CLI then:
- Detects the project: a
wrangler.jsoncwithmain, or a server framework inpackage.json. Anything else — orkumodeck deploy dist, or adeployDirinkumo.json— is the static deploy above.--app/--staticoverride it. - Runs
wrangler setup --yesif the project has nowrangler.jsoncyet (Cloudflare's own setup adds the adapter). - Builds on your computer (
npm run build, or pnpm / yarn / bun from the lockfile), then bundles withwrangler deploy --dry-run --outdir .kumo/app-build. Nothing is sent to Cloudflare by wrangler, and KUMODeck never builds your code. - Asks KUMODeck for a dry run (sizes, files to upload, databases it will create, warnings), uploads only new files and switches the live version.
wrangler must be installed in the project (npm install -D wrangler); without it the CLI uploads the build as static
files and says so. --json shows the detection, the build commands and what was sent.
- Where it runs:
https://<slug>.kumodeck.app/,https://<slug>--dev.kumodeck.app/and your custom domains — not under/play/. POST requests (forms, server actions) reach the app. - Static or app: an environment serves whichever version is live.
kumodeck rollbackswitches between static and app versions instantly. - Databases and files: D1, KV, R2 and Queue producers in
wrangler.jsoncare created per environment and shared with Functions by binding name. - Your app's database: create the tables and query it with
kumodeck db migrate DBandkumodeck db query DB "SELECT …"(the same askumodeck functions db …). Functions do not need to be on.db migratereads the folder frommigrations_dirof that database in the app'swrangler.jsonc(defaultmigrations). A SQL mistake answersd1_query_errorwith the database's message: fix the SQL. - Logs: what the app prints with
console.log/console.erroris kept for 7 days. Read it withkumodeck logs(together with Functions, mixed by time;--source appfor the app only) or the MCP toolfunctions_logs(Logs). Lines are billed at cost like Functions logs;kumodeck deploy --no-logskeeps none for that version. A version deployed before apps had logs answersapp_logs_unavailable: deploy again. Code that throws while starting answersapp_script_errorwith the exception indetails.error. - Secrets:
kumodeck functions secret put NAMEsets them for Functions and the app together. Do not put secrets invars(the deploy warns about names that look like secrets). - Scheduled jobs: the app's Worker cannot have crons. Put them in Functions.
- Cookies: KUMODeck removes
Domain=from the app'sSet-Cookie, so cookies stay on the app's own host. - Not available yet: image optimization (serve images as they are), ISR, Durable Objects, service bindings, native modules. The deploy Skill lists what to use instead.
- Cost: requests and CPU of the app, billed at cost from your prepaid balance.
Next.js#
Next.js is built with vinext (Cloudflare's Next.js on Vite) — the recommended way — or with
OpenNext (@opennextjs/cloudflare), which stays as the fallback. Both run
without ISR for now. kumodeck deploy uses vinext when it is in package.json and OpenNext when only OpenNext is set
up; with neither, it stops and prints the lines for both (next_adapter_choice). --next vinext / --next opennext
chooses for one deploy.
vinext (recommended) — needs Node.js 22.18 or later:
- Install it:
npm install -D vinext(or pnpm / yarn / bunadd -D vinext). - Run
npx vinext checkand change what it marks ✗. - Run this once (it writes
vite.config.tsandcloudflare.config.ts; the three choices are needed, or it stops and asks):
``sh
npx vinext init --platform=cloudflare --cdn-cache=none --data-cache=none --image-optimization=none
``
kumodeck deploy. The CLI runsvinext checkandvite build(instead of steps 2–3 above) and uploads.cloudflare/output/v0. It does not change your files, and no Cloudflare account is needed.
- If
vinext checkfinds something vinext does not support, the deploy lists it. With OpenNext installed, it deploys with OpenNext this time; change the listed items and deploy again to use vinext. Without OpenNext, it stops (vinext_check_failed).kumodeck deploy --next vinexttries vinext anyway. - Image optimization (
imagesOptimizer,bindings.images()) stops the deploy: vinext fails without it at runtime, so it cannot be left out. Init with--image-optimization=none; images are served as they are. - Keep the assets binding named
ASSETS(asvinext initwrites it).
OpenNext (the fallback) — for apps vinext does not support yet, or when vinext does not work. For OpenNext, step 3
above is opennextjs-cloudflare build (it runs next build); the CLI then copies the pages built at build time into the
assets (.open-next/cache → .open-next/assets/cdn-cgi/_next_cache).
- Install
@opennextjs/cloudflareandwrangler. Ifwrangler.jsoncoropen-next.config.tsis missing, the CLI writes the smallest one it needs and says so (it never overwrites). The assets binding must be namedASSETS. - An
open-next.config.tsyou already have must use the read-only cache from the assets. The CLI stops before building otherwise (it does not edit your files):
```ts import { defineCloudflareConfig } from "@opennextjs/cloudflare"; import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({ incrementalCache: staticAssetsIncrementalCache }); ```
- Set
images: { unoptimized: true }innext.config(no image optimization yet; images are served as they are). kumodeck deploy(kumodeck deploy --next opennextwhen vinext is installed too).
revalidatehas no effect yet; server-rendered pages and pages built at build time work. R2 / KV / D1 caches and queues for ISR (NEXT_INC_CACHE_*,NEXT_TAG_CACHE_*,NEXT_CACHE_*) are refused.services(WORKER_SELF_REFERENCE) andimagesin OpenNext'swrangler.jsoncare not sent; the deploy says so.- Edge middleware works. Node.js middleware gets a warning (it is experimental in OpenNext too); if it fails, go back to Edge.
Both: Route Handlers and Server Actions reach the app. A static export (output: "export", then kumodeck deploy out)
still works. Next.js needs Node.js compatibility: it is on by default with a compatibility_date of 2026-08-04 or later
(vinext init also writes nodejs_compat). With an older date, KUMODeck adds nodejs_compat and says so
(next_nodejs_compat_added): add it to compatibility_flags in wrangler.jsonc too, so local runs match.
If Next.js does not work#
Next.js on KUMODeck is in beta: it has not been checked on the production runtime yet. Every Next.js deploy says so
(the warning vinext_beta for vinext, next_beta for OpenNext; when a deploy fails, the error's details.fallback
points here too). If the app does not work after it is deployed, deploy it again in one of these ways.
If it was deployed with vinext, deploy it with OpenNext — install OpenNext if it is not there yet, then:
npm install -D @opennextjs/cloudflare wrangler
kumodeck deploy --next opennext # development (deploy's default)If it still does not work, try one of these two.
1. Export it as a static site — for apps that do not need server code on each request (no Route Handlers, Server
Actions, middleware or cookies read on the server). In next.config:
const nextConfig = { output: "export", images: { unoptimized: true } };
export default nextConfig;npx next build # writes the site to out/
kumodeck deploy out # development (deploy's default)Server code the app still needs can move to the project's own Functions.
2. Render each page on request — remove ISR: delete export const revalidate = … from pages and layouts and
next: { revalidate: … } from fetch calls (use cache: "no-store" where the data must be fresh), and remove the
NEXT_INC_CACHE_* / NEXT_TAG_CACHE_* / NEXT_CACHE_* bindings from wrangler.jsonc. Then deploy as before:
kumodeck deploy # development (deploy's default)If it still does not work, keep the error's code and details from kumodeck deploy --json when you report it.
Custom domains#
Serve the production app on a domain you own, such as play.mygame.com. Your users, share links and X cards
(the card tags and image URLs) then use that domain — they never see a KUMODeck URL. KUMODeck handles the
certificate and the ownership check; you add two DNS records. Off by default, production only.
kumodeck features on customDomains && kumodeck config push --env production
kumodeck hosting domains add play.mygame.com # prints the DNS records to add
kumodeck hosting domains status play.mygame.com # check again after adding them
kumodeck hosting domains # list
kumodeck hosting domains remove play.mygame.com # asks first (--yes in scripts)add prints the two records exactly as your DNS provider asks for them:
CNAME play.mygame.com <the CNAME target it prints>
TXT _kumo-verify.play.mygame.com kumo-verify=<your account's value>The CNAME points the name at KUMODeck; the TXT proves the domain is yours (the value is the same for every domain
on your account). Add both at the same time where you manage the domain (your registrar or DNS provider).
KUMODeck checks every hour and starts serving once they are found — usually minutes, sometimes a few hours.
Your <slug>.kumodeck.app URL keeps working, so links you already posted do not break.
- Watch two things:
status(pending→active, orfailed) andverified(ownership proven by the TXT, with the time it was last confirmed). While something is missing, the output lists what to do next. - Keep the TXT record after the domain is live: KUMODeck re-checks it every day and stops serving on the domain if it is gone.
- One TXT on a parent domain covers everything under it:
_kumo-verify.mygame.comalso provesplay.mygame.com,www.mygame.comand so on, so further subdomains need only their CNAME. - A domain someone else merely signed up for is not theirs: whoever proves ownership with the TXT gets it. The only "already in use" refusal is when this app already uses the domain for the other purpose (Functions vs. hosting).
- Root domains (
mygame.com): some DNS providers cannot put a CNAME at the root. Use ALIAS / ANAME / CNAME flattening if your provider has it, or a subdomain such asplay.mygame.com. - What changes when it is active: the URL returned by
kumodeck deploy, share links, X card tags and image URLs use your domain (card tags you already put in<head>: get them again withkumodeck share tags --env productionand replace them); sign-in redirects andweb.allowedOriginsaccept it automatically (https only). - Limits: up to 5 per project. Development stays on its
--devURL. - Cost: custom domains are billed daily at cost from your prepaid balance. Remove the ones you no longer use.
- Turning
customDomainsoff stops serving on your domains (listing and removing still work).
The dashboard (Hosting) and the MCP tools hosting_domains_list / hosting_domain_add / hosting_domain_remove do the same.
Changing the URL slug#
The slug is the name in your game's URLs (<slug>.kumodeck.app). You can change it later — once every 30 days.
Undoing your last change within 24 hours does not count.
kumodeck slug # current slug, next change date, old slugs and their redirects
kumodeck slug check sky-racers-2 # free? what changes? (URLs, the two choices below)
kumodeck slug change sky-racers-2 --keep-redirect # or --no-redirect: you must pick one
kumodeck slug redirect sky-racers on # turn the redirect of an old slug on (or off)When you change it, you choose what happens to links that use the old slug — there is no default:
- Keep old links working (
--keep-redirect): old URLs redirect (301) to the new ones, keeping the path and query (so share links still count). $1 a month per old slug, charged daily from your prepaid balance while it is on. It keeps working even if your balance runs out; it stops only when you turn it off. - Let them stop working (
--no-redirect): old URLs show a neutral "Not found" page. No charge.
Either way the old slug stays reserved for your game — nobody else can take it — so you can turn its redirect on or off later. Custom domains do not change.
Changing the slug or a redirect asks you to confirm it is you (your password, or signing in again with Google etc.).
The CLI asks for your password in a terminal; accounts without a password change it in the dashboard (Hosting → URL slug).
The MCP tools project_slug_get / project_slug_check only read: an AI assistant cannot change your slug.
How uploads work#
- The CLI hashes every file (sha256) and sends the manifest.
- The server answers with the hashes it does not have for this project — only those are uploaded (8 in parallel, retried).
finalize+activateswitch the live version atomically.
Redeploying a large app after a small change uploads only the changed files. Development and production share uploads, so promoting the same build costs nothing.
Caching#
| File | Cache-Control |
|---|---|
*.html | no-cache (revalidated every load — a deploy or rollback is visible immediately) |
hashed files under assets/, static/, chunks/ (e.g. app.3f9a1c.js) | immutable, 1 year |
| everything else | max-age=0, must-revalidate (cheap 304s) |
ETags are content hashes; Range and HEAD are supported. /x redirects to /x/ when x/index.html exists;
a 404.html at the root is used for missing paths.
Security headers#
Each app is served from its own subdomain, separate from the API and the dashboard, so apps cannot read each
other's storage or a developer's session. A permissive CSP keeps CDNs, eval and WebAssembly engines working while
blocking plugins and insecure (http:) loads. There is no frame-ancestors rule, so you can embed your app or game on
other sites such as itch.io. Development deployments send X-Robots-Tag: noindex.
Limits#
| Limit | Value |
|---|---|
| Files per deploy | 5,000 |
| Total size | 500 MB |
| Single file | 50 MB |
| Path length | 512 characters |
| Unfinished deploys | 20 per project (expire after 24 h) |
| Server-rendered app: Worker | 64 MiB (uncompressed) |
| Server-rendered app: asset file | 25 MiB |
| Server-rendered app: per request | 1,000 ms CPU, 50 subrequests |
CI#
Skip kumodeck login and pass a secret key; the environment comes from the key:
KUMO_API_URL=https://api.kumodeck.com KUMO_SECRET_KEY=$KUMO_SECRET_KEY npx kumodeck deploy distComing soon SPA fallback routing and per-project headers (COOP/COEP for
SharedArrayBuffer) are planned.