# Server code for an AI-built app - secret keys, outside services, AI

Every app is stored on Scava and delivered from there, but its pages run in the browser of the person using them. Server code is the part of an app that runs on Scava's servers instead, in the background, even when nobody has the app open. Most internal tools need only a page and the shared database. When an app has to keep a secret key, call an AI, talk to a company system, receive calls from other services or send email, give it server code. You add a folder `server/` next to `index.html` with a file `server/index.js`. Scava runs it for the app, in an isolated environment of its own, and connects it to the app's data, files and mail. Server code is available in every plan.

## The smallest example

```js
// server/index.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === '/api/hello') {
      const me = await env.SCAVA.me();
      return Response.json({ message: 'Hello ' + me.email });
    }
    return new Response('Not found', { status: 404 });
  },
};
```

In the page:

```js
const r = await fetch('/api/hello');
const data = await r.json();
```

The shape is the one of a Cloudflare Worker: an object with `fetch(request, env, ctx)` for requests and, optionally, `scheduled(event, env, ctx)` for [schedules](/docs/schedules-webhooks).

## How requests reach the code

- Every request from the app's pages to `/api` or `/api/...` goes to `fetch()`. Everything else is served from the app's files, as before.
- Scava checks the sign-in and the access to the app first. A person who is not signed in does not reach the code.
- Only the app's own pages may call it. Scripts and forms of other websites and of other apps are refused. Requests that change something must come from the app's own address. A signed-in person who opens an `/api` address as a page gets the answer shown.
- Answers get Scava's security headers, which the code cannot loosen. There are no CORS headers, so no other website can read an answer. Cookies named by Scava are not passed on in either direction.
- Scava removes its own headers from the request and does not pass on the visitor's IP address. The person who asks is available with `await env.SCAVA.me()`.
- The files in `server/` are never sent to a browser.

If the code throws, the visitor sees only "The server code of this app failed." The owner sees the message and the place (`server/index.js:12`) in the problems of the app, and the agent gets them with `get_app_problems`.

## What the code can import

Server code is plain JavaScript in ES modules (`import` and `export`).

- Your own files in `server/`: `import { total } from './lib.js'`.
- These built-in modules, written with the `node:` prefix: `node:assert`, `node:buffer`, `node:crypto`, `node:events`, `node:path`, `node:querystring`, `node:stream`, `node:string_decoder`, `node:timers`, `node:url`, `node:util`, `node:zlib`.
- Nothing else. Packages cannot be installed on Scava. If your code needs a package, bundle it into one file first:

```bash
npx esbuild src/server.js --bundle --format=esm --platform=neutral --outfile=server/index.js
```

TypeScript and JSX must be compiled to JavaScript first. `require()` and files ending in `.cjs` are not supported. An `import()` with a computed name is refused. The upload check tells the agent exactly what to change.

## What `env.SCAVA` offers

| Call | What it does |
|---|---|
| `env.SCAVA.me()` | The person who asks: `{ email, name, role, groups }`, or `null` for webhooks and schedules |
| `env.SCAVA.db('name')` | `list()`, `get(id)`, `add(data)`, `update(id, changes)`, `remove(id)`: the same data as `scava.db()` in the browser |
| `env.SCAVA.files('name')` | `list()`, `get(id)`, `put(fileName, data, { type })`, `read(id)`, `remove(id)`, `url(id)`: the same files as `scava.files()` |
| `env.SCAVA.mail({ to, subject, text })` | Sends a plain-text email to people who have access to the app |
| `env.SCAVA.ai({ provider, model, messages })` | Calls an AI provider with the key you bound to it |
| `env.SCAVA.run('report.py', input)` | Runs heavy work in a container; see [Schedules, webhooks and heavy work](/docs/schedules-webhooks) |

### Rights: the owner's or the person's

By default `env.SCAVA.db()` and `env.SCAVA.files()` have the rights of the **owner** of the app, for the app's own data. What the code adds for a visitor counts as created by that visitor, so `"own"` rules keep working.

To act with exactly the rights of the person who asks, pass `{ as: 'me' }`. Then the rules in `scava.json`, groups and fields included, apply to that person. This is the right choice when the code acts for the visitor:

```js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === '/api/approve' && request.method === 'POST') {
      try {
        const { id } = await request.json();
        const vacation = env.SCAVA.db('vacation', { as: 'me' });
        return Response.json(await vacation.update(id, { status: 'approved' }));
      } catch (e) {
        return Response.json({ error: e.message }, { status: e.status || 500 });
      }
    }
    return new Response('Not found', { status: 404 });
  },
};
```

With the rules from [Permission rules](/docs/rules), only members of `leads` can approve. Without a person, `as: 'me'` is refused.

## Secrets and API keys

Never put a key into a file. The upload check refuses files that contain API keys, private keys or `.env` files. Keys go into secrets, which the owner sets on the app page at scava.ai under "Server code": a name like `OPENAI_KEY`, the value, and the service it is for, like `api.openai.com`. Set in the browser, a value never passes through a terminal an AI can read. Neither the owner nor an agent can read a value again after it is saved. Agents must not handle key values; ask the person to run the command.

A secret **bound to a service** works like this:

- `env.OPENAI_KEY` does not hold the key. It holds a placeholder, `scava:key:OPENAI_KEY`.
- You use the placeholder exactly like the key, in an address or in a header such as `Authorization: Bearer ...`.
- When a request leaves for that host, Scava puts the real key in. Only into the address and the headers, never into the body of the request. The key goes only to the hosts you named; a request with the placeholder to another host is refused. If the service repeats the key in a header or a text answer, it is replaced by the placeholder again before the code sees it.
- You can bind a key to several hosts, and a pattern like `*.example.com` is allowed.

```js
const res = await fetch('https://api.openai.com/v1/models', {
  headers: { authorization: 'Bearer ' + env.OPENAI_KEY },
});
```

A secret **not bound to a service** is readable: `env.NAME` is the value itself. Use that only for values the code must compute with, such as the signing secret of a webhook.

Secret names are in capital letters, digits and `_`, start with a letter and do not start with `SCAVA`. Only the owner sets and deletes secrets. Your agent sees the names with `get_server_info`.

## Outside services

Server code may call any service with `fetch()`. Every call is recorded per app and host: the owner sees under "talks to" which services the app talks to, how many calls and with which result. Publishing tells the agent which services the code names that were not there before.

Two restrictions:

- Calls to Scava's own addresses, and to the app's own address, are blocked. Use `env.SCAVA` for data and files.
- Per request the code may make only so many outside calls, depending on the plan; see [Limits](/docs/limits).

The owner's house rules can narrow this: allow only a list of services, or none. Then other calls are refused and a version that names other services is not accepted. Only the owner changes the rules, in the browser. If an agent hits such a refusal, it tells the owner what the app needs and does not try to work around it. See [House rules](/docs/house-rules).

> The browser still can only talk to the app's own address. A page that needs an outside service calls its own `/api/...`, and the server code calls the service.

## AI

```js
const answer = await env.SCAVA.ai({
  provider: 'openai',           // 'openai', 'anthropic' or 'mistral'
  model: 'a-model-of-the-provider',
  messages: [{ role: 'user', content: 'Summarize this in one sentence: ...' }],
});
// { text, provider, model, usage, raw }
```

- The key is the secret you bound to the provider's host, whatever its name: for example `OPENAI_KEY` bound to `api.openai.com`, and the same for `api.anthropic.com` and `api.mistral.ai`. The code never sees it, not even in an error message of the provider. You pay the provider. Scava does not charge for the call.
- Other options such as `max_tokens` are passed to the provider.
- Scava limits the calls per app and day, depending on the plan, and records each call under "talks to" with the service and the result. It does not keep what you sent or what came back.
- Where the data is processed is up to the provider. OpenAI processes in the EU when you use the key of an EU project and bind it to `eu.api.openai.com`. Mistral is a provider in the EU. Scava calls Anthropic's own API, and Anthropic decides where it processes. The owner can set a house rule that allows AI only in the EU, or switches AI off.

## Limits

Server code is limited per account and month, for all apps of the owner together, and per request. At 80 percent and at 100 percent of a monthly limit the owner gets an email. At 100 percent of calls or compute time the server part of the apps pauses until the month ends. At 100 percent of emails the apps cannot send more, and at 100 percent of heavy work no more runs start. The pages and the data keep working. The numbers per plan are in [Limits](/docs/limits).

## Testing

Publishing from a folder runs the test run; `get_app_problems` shows what the browser and the server code reported and gives a command to run it again. `get_server_info` shows whether the server part runs, the usage against the plan, secret names, webhooks, schedules and the outside services used. Apps are tested online, with the test run.
