---
title: "Functions (your own server code)"
description: "Everything you could do with your own server and your own database, you can do on KUMODeck."
url: "/docs/guides/functions/"
lang: en
index: "/llms.txt"
---
# 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](/docs/guides/saves/index.md)** | 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](/docs/pricing/index.md)).

## Start from the template

```sh
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`:

```sh
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:

| 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:

```ts
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:

```js
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](/docs/guides/auth/index.md)).

## 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.

```sh
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](/docs/guides/hosting/index.md#server-rendered-apps) together, mixed by time
(`kumodeck logs` is the same as `kumodeck functions logs`):

```sh
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](/docs/claude-code/index.md)).
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

```sh
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](/docs/claude-code/index.md). 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.
