KUMODeck
日本語

Functions (your own server code)

Everything you could do with your own server and your own database, you can do on KUMODeck. Functions runs server code you write, with a private SQL database, key-value store, file storage, queues, Durable Objects and scheduled jobs for each environment. You never create a cloud account, paste resource IDs or manage servers.

The ready-made building blocks (sign-in, saves, …) are tools your code can call. They don't limit what you can build: write your own API, business or match logic, webhooks from other services, calls to any third-party API.

Your database or saves? ("save it in a DB" can mean either.) Pick by who may change the data:

Protect in D1, not in saves. Anything a user must not be able to change themselves — scores and rankings, coins, credits, items, purchases, badges, anything shared between users — goes in the project's own Functions + D1. saves holds only what the user may freely write (settings, drafts, a solo game's progress).

Put it inWhatWho writes it
Your database (D1, here)what must be protected: scores and rankings, coins, credits, items, purchases, a verified badge, anything shared between usersonly your server code
Saveswhat the user may change freely: settings, progress notes, drafts, favorites, a game's savethe user's browser (a changed value hurts nobody)

When unsure: if a user changing it by hand would be a problem, keep it here. Your code cannot read or write a user's saves, so keep the protected copy in your database from the start.

Off by default. Nothing runs and nothing is billed until you turn Functions on for an environment. You pay only what it uses, at cost, from your prepaid balance (see Pricing).

Start from the template#

cp -r templates/functions-starter/functions ./functions && cd functions
npm install
cp .dev.vars.example .dev.vars        # put your development secret key (sk_dev_…) in it
npx wrangler d1 migrations apply DB --local
kumodeck functions dev                   # = npx wrangler dev → http://localhost:8787/health

kumodeck functions dev runs everything locally (database, KV, files and Durable Objects included) and does not touch KUMODeck. When it works, ship it from the folder that has kumo.json:

kumodeck functions enable                # needs prepaid credit
kumodeck functions deploy                # bundles with your local wrangler, uploads, creates the empty database DB
kumodeck functions db migrate DB         # create the tables: apply migrations/ to this environment's own database
curl https://<slug>--dev.kumodeck.dev/health
kumodeck functions status                # URL, version, databases, secrets, crons, limits

The first deploy creates the database, so migrate after it: the deploy says which databases are new and prints the db migrate line to run (in --json: newDatabases and next; in a terminal it offers to run it for you). Until you migrate, the tables do not exist and the first call fails with no such table. db migrate reads the folder from migrations_dir of that database in wrangler.jsonc (default migrations), and skips migrations already applied (the same d1_migrations table as wrangler). kumodeck db migrate DB and kumodeck db query DB "SELECT …" are short for the same commands.

Every command takes --env development|production (default: development). Your code is served at:

EnvironmentURL
productionhttps://<slug>.kumodeck.dev/
developmenthttps://<slug>--dev.kumodeck.dev/

These URLs and every response carry your app's name only — your users (called players in the API) never see KUMODeck.

What you can use#

Write a normal wrangler.jsonc. KUMODeck reads only the binding names and gives each environment its own resources, so the same file works locally and on KUMODeck.

In wrangler.jsoncWhat you getNotes
d1_databasesa private SQLite database per environment10 GB per database; add more bindings to shard. Tables and SQL are up to you
kv_namespacesa private key-value store
r2_bucketsprivate file storage
queues.producersa private queue you can send toconsuming messages in your own code is not available yet
durable_objects + migrationsstateful objects, WebSocket rooms (SQLite)a deploy that removes a class (and its data) is refused
triggers.cronsscheduled runs (UTC, 5-field cron)on KUMODeck they arrive as a request with props.kumo.event = "scheduled" (the template handles both)
varsplain settings
kumodeck functions secret put NAMEsecretsthe value goes straight to the runtime; KUMODeck stores only the name

Not available: service bindings to other workers, Hyperdrive, Workers AI / Vectorize / Browser Rendering (no per-project cost metering for them yet). Reach any outside service with fetch(); raw TCP sockets are blocked.

Call KUMODeck from your code#

When you enable Functions, KUMODeck issues a secret key for that environment and passes it to your code as KUMO_SECRET_KEY (with KUMO_API_URL). The template's src/kumo.ts is a tiny client for it:

const player = await new Kumo(this.env).players.verify(request.headers.get('authorization'));
if (!player || player.banned) return new Response('sign in first', { status: 401 });

Your app sends the user's token with its request:

await fetch(`${FUNCTIONS_URL}/scores`, {
  method: 'POST',
  headers: { 'content-type': 'application/json', authorization: `Bearer ${await kumo.auth.getAccessToken()}` },
  body: JSON.stringify({ score })
});

players.verify calls POST /v1/admin/players/verify-token and only accepts users of the same environment. Any other secret-key API (config, share stats, …) works the same way as from your own server.

Caught a cheat or an abuser? Ban the user right there: players.ban(player.id, { reason, durationHours }) (leave out durationHours to ban until you lift it) and players.unban(playerId). It is the same ban as the dashboard's (Banning players).

Calling from a browser (CORS)#

Your pages call the Functions from the browser with a signed-in user's token, so the template does not answer every site ("*"). It allows:

  • your own site — KUMODeck passes your code KUMO_ALLOWED_ORIGINS: the environment's site address, your verified custom domains (production) and the web.allowedOrigins of kumo.config.json. When one of those changes, KUMODeck updates it without a redeploy. If it could not, kumodeck functions status says browser access is out of date — run kumodeck functions deploy again. The shared /play/… address is never included (every project's pages run on it)
  • sites you list in ALLOWED_ORIGINS in wrangler.jsonc vars (comma-separated, then deploy). https://*.example.com allows the subdomains of example.com, not example.com itself; "*" allows any site (not recommended)
  • localhost outside production (your dev server)

kumodeck functions status and kumodeck functions deploy show the sites KUMODeck set. A version deployed by an older CLI may still allow the shared /play/ address; status says so, and deploying again removes it.

Your own domain#

Serve your Functions on your own domain: kumodeck functions domains add api.<your domain> (add --env production for the live one). Add both DNS records it prints (a CNAME and a TXT _kumo-verify.<host>) where you manage the domain; keep the TXT after it is live. Check with kumodeck functions domains status <host>, remove with kumodeck functions domains remove <host>. Functions must be enabled in that environment.

kumodeck functions domains add api.example.com --env production
kumodeck functions domains status api.example.com --env production

Logs#

When a deployed function or page misbehaves, read what your code printed with console.log / console.warn / console.error. One command reads your Functions and your server-rendered app together, mixed by time (kumodeck logs is the same as kumodeck functions logs):

kumodeck logs                                  # the last hour, oldest first, in your local time
kumodeck logs --since 2d --level error,warn    # up to 7 days back; levels: debug, log, info, warn, error
kumodeck logs --source app                     # only the server-rendered app (--source functions: only Functions)
kumodeck logs --search "score" --env production
kumodeck logs --all --json                     # every page as JSON (newest first), for scripts

Each line shows the time, where it came from (fn = Functions, app = the server-rendered app; source in --json), the level, what ran (fetch for a request, scheduled for a cron, queue) and the message. A message longer than 8 KB is cut and marked. --since / --until take 15m, 1h, 2d or a date-time; --limit and --cursor page through (across both). Your AI agent can read the same with the MCP tool functions_logs (argument source) — it needs the separate Read server function logs permission (read:logs) on the consent screen (Use from an AI agent). Locally, kumodeck functions dev prints the same output in your terminal.

What goes into logs is up to you. Logs show only what your own code prints, kept for 7 days and then deleted automatically. Do not log personal data or secrets: emails, passwords, access tokens, API keys, payment details, or anything a user would not want others to see. Anyone who can read this project's logs (you, your CLI keys, and AI agents you allowed to read logs) can see those lines. Log the ID of a user, not their email; log that a call failed and its status, not the token you sent.

Log lines are part of KUMODeck's usage fee, at cost: $0.60 per million lines. If your code prints nothing, they cost nothing. To keep no logs for a version (for example a very chatty function whose output you never read), deploy it with kumodeck functions deploy --no-logs (for the app: kumodeck deploy --no-logs); the next deploy without the flag turns them back on.

ErrorMeaningNext step
functions_disabledFunctions are off in this environment (and no server-rendered app is live)kumodeck functions enable, or deploy your app with kumodeck deploy
functions_not_deployednothing is deployed yetkumodeck functions deploy
logs_disabledthe live version was deployed with --no-logs (or before logs existed)kumodeck functions deploy; logs start from that deploy
app_logs_unavailablethe live version of the app was deployed with --no-logs (or before apps had logs)kumodeck deploy; logs start from that deploy
app_not_deployed--source app, but no server-rendered app is live in this environmentkumodeck deploy, or read Functions with --source functions
rate_limited (429)too many log reads right now; the limit is shared by everyone on KUMODeck, not just this projectwait details.retryAfter seconds (it can be 300; the CLI says how long), then read once. Do not retry in a loop; narrow with --since / --level / --search
rate_limit_unavailable (503)log reads are paused for a moment on the serverwait a few seconds, then read once

When something fails#

Errors come with a hint (the next step) and, in --json, a machine-readable code:

ErrorMeaningNext step
d1_query_error (400)the database rejected your SQL (syntax, missing table or column, constraint); details.error is the database's messagefix the SQL; retrying the same SQL fails the same way
migration_faileda migration file failed; details lists what was applied, the failed file and how many later files did not runfix that file and run kumodeck functions db migrate DB again (applied files are skipped)
functions_script_error (400)your code threw while starting (for example at import time); details.error is the exceptionreproduce with kumodeck functions dev (npx wrangler dev), fix, deploy again
d1_binding_not_foundno database with that binding name in this environment (the message lists the ones that exist)check the name, or deploy first: the first deploy creates the database
functions_suspendedstopped for this environment; details.reason says whybalance: add credit and it resumes within a minute; otherwise answer the email you got

Limits, costs and stopping#

kumodeck functions limits --cpu-ms 500 --subrequests 100   # per request (defaults 200 ms / 50; max 30 s / 1000)
kumodeck functions disable                                  # stop serving; code and data are kept
kumodeck functions delete --purge                           # remove the code and delete databases, files and queues

Requests, CPU time, database rows and storage are billed as KUMODeck's usage fee, at cost, with no markup and no free tier. As a rough guide, a small app (1 million requests, 5 ms CPU each, a 100 MB database) costs about $0.60 a month in usage. The fixed monthly base fee for running Functions is shared between everyone who uses Functions, in proportion to use, and settled against the actual cost at the end of the month. You get an email (and an MCP notice) when your prepaid balance runs low, and you can turn on automatic top-up. If the balance is used up and nothing else covers it (an automatic top-up that has not failed), Functions stop answering (503) until funds are added; your data is kept.

From your AI agent#

Everything above is available as MCP tools (functions_status, functions_enable, functions_deploy, functions_db_query, …) — see Use from an AI agent. Ask "add a table for daily scores and migrate development", and the assistant writes the migration, runs it locally, and applies it with your confirmation.

Your data#

What you store in your own database, files or KV is yours to manage — including deleting a user's data when they ask.