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 in | What | Who writes it |
|---|---|---|
| Your database (D1, here) | what must be protected: scores and rankings, coins, credits, items, purchases, a verified badge, anything shared between users | only your server code |
| Saves | what the user may change freely: settings, progress notes, drafts, favorites, a game's save | the 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/healthkumodeck 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, limitsThe 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:
| Environment | URL |
|---|---|
| production | https://<slug>.kumodeck.dev/ |
| development | https://<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.jsonc | What you get | Notes |
|---|---|---|
d1_databases | a private SQLite database per environment | 10 GB per database; add more bindings to shard. Tables and SQL are up to you |
kv_namespaces | a private key-value store | |
r2_buckets | private file storage | |
queues.producers | a private queue you can send to | consuming messages in your own code is not available yet |
durable_objects + migrations | stateful objects, WebSocket rooms (SQLite) | a deploy that removes a class (and its data) is refused |
triggers.crons | scheduled runs (UTC, 5-field cron) | on KUMODeck they arrive as a request with props.kumo.event = "scheduled" (the template handles both) |
vars | plain settings | |
kumodeck functions secret put NAME | secrets | the 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 theweb.allowedOriginsofkumo.config.json. When one of those changes, KUMODeck updates it without a redeploy. If it could not,kumodeck functions statussays browser access is out of date — runkumodeck functions deployagain. The shared/play/…address is never included (every project's pages run on it) - sites you list in
ALLOWED_ORIGINSinwrangler.jsoncvars(comma-separated, then deploy).https://*.example.comallows 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 productionLogs#
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 scriptsEach 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.
| Error | Meaning | Next step |
|---|---|---|
functions_disabled | Functions are off in this environment (and no server-rendered app is live) | kumodeck functions enable, or deploy your app with kumodeck deploy |
functions_not_deployed | nothing is deployed yet | kumodeck functions deploy |
logs_disabled | the live version was deployed with --no-logs (or before logs existed) | kumodeck functions deploy; logs start from that deploy |
app_logs_unavailable | the 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 environment | kumodeck 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 project | wait 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 server | wait a few seconds, then read once |
When something fails#
Errors come with a hint (the next step) and, in --json, a machine-readable code:
| Error | Meaning | Next step |
|---|---|---|
d1_query_error (400) | the database rejected your SQL (syntax, missing table or column, constraint); details.error is the database's message | fix the SQL; retrying the same SQL fails the same way |
migration_failed | a migration file failed; details lists what was applied, the failed file and how many later files did not run | fix 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 exception | reproduce with kumodeck functions dev (npx wrangler dev), fix, deploy again |
d1_binding_not_found | no 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_suspended | stopped for this environment; details.reason says why | balance: 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 queuesRequests, 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.