Skip to content
Scava Docs Join the waitlist

Scava opens soon. These docs describe how it works at launch. Join the waitlist

All chapters

Server code

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 #

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

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.

How requests reach the code #

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

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 #

CallWhat 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

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:

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

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:

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.

AI #

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 }

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.

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.