---
title: Server code
description: Run your own code on the server for webhooks, private logic and SQL, with scheduled jobs, background tasks, logs and per-request limits.
---

# Server code

Most apps need nothing but the browser library. When they do need a server (a webhook from Stripe, logic that must not run in the browser, joining data from several APIs, or SQL), add `server/index.js` to the published folder. Requests to `/api/*` on the app's address run it, isolated from every other app.

## Your first function

```js
// server/index.js
export default {
  async fetch(request, env) {
    const me = env.CROW.viewer;   // { userId, email, name }, or null
    return Response.json({ hello: me ? me.name : "visitor" });
  },
};
```

Publish the folder. `https://<name>.crow-nest.com/api/hello` now answers from your code, and `/server/index.js` is never served to browsers. Requests go through the same access check as the pages, so a private app's API is private too.

## What your code gets

| On `env` | What it is |
| --- | --- |
| `env.CROW.viewer` | The signed-in person (`{ userId, email, name }`) or `null`. Set by Crow Nest; a request can't fake it |
| `env.CROW.db` | The app's [crow.db](/data) collections, with the owner's rights |
| `env.CROW.sql(query, ...bindings)` | The app's own SQLite database |
| `env.CROW.api(name)` | An API declared in `crow.json`, with its secrets filled in |
| `env.CROW.ai.chat(...)` | An AI call, using the app's key setting |
| `env.CROW.later(path, body, options)` | Queue a background task |
| `env.<SECRET_NAME>` | Each of the app's [secrets](/apis-and-secrets), by name |

```js
export default {
  async fetch(request, env) {
    const me = env.CROW.viewer;
    await env.CROW.sql("CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, email TEXT)");
    const rows = await env.CROW.sql("SELECT * FROM orders WHERE email = ?", me.email);
    const notes = await env.CROW.db.collection("notes").list({ limit: 10 });
    const res = await env.CROW.api("stripe").fetch("customers?limit=3");
    const answer = await env.CROW.ai.chat({ messages: [{ role: "user", content: "Hi" }] });
    return Response.json({ rows, notes: notes.docs });
  },
};
```

## SQL

`env.CROW.sql` is a SQLite database that belongs to the app's server code: up to 100 MB, up to 1,000 rows per query (add a `LIMIT`). It's separate from crow.db, which the browser uses; neither can see the other's tables, and no app can reach another app's data. Use `?` placeholders for values.

## Who can call it

Set this in `crow.json`:

```json
{ "server": { "who": "viewers", "publicPaths": ["/api/webhooks/stripe"] } }
```

- `who: "viewers"` (default): signed-in people allowed to open the app. `who: "public"`: anyone (public apps only).
- `publicPaths`: paths that skip the access check, even on a private app. Use them for webhooks from outside services, and check the webhook's signature yourself. A path ending in `*` matches everything under it.

## Scheduled jobs

Run code on a timer with `server.schedules`:

```json
{ "server": { "schedules": [{ "cron": "0 9 * * *", "path": "/api/cron/daily" }] } }
```

- Cron has 5 fields (minute, hour, day of month, month, day of week) in UTC, with lists, ranges, steps and `*`.
- Up to 5 jobs. Personal apps run at most every 15 minutes; companies every minute.
- Each run sends a `POST` to your path with no signed-in user and the header `x-crow-schedule` set to that job's cron. Crow Nest removes `x-crow-*` headers from every outside request, so the header proves the call is a scheduled run.
- A suspended or paused app's jobs don't run. A failed run isn't retried; it shows in the logs.

```js
const url = new URL(request.url);
if (url.pathname === "/api/cron/daily") {
  if (!request.headers.get("x-crow-schedule")) return new Response("Not found", { status: 404 });
  await sendDailySummary(env);
  return Response.json({ ok: true });
}
```

## Background tasks

Do work after responding, or later:

```js
await env.CROW.later("/api/send-email", { to: "sam@example.com" }, { delaySeconds: 60 });
```

This queues a `POST` to your own path with the body as JSON (up to 64 KB), after 0 to 86,400 seconds. The call carries the header `x-crow-task`. If your code answers with a 5xx or throws, it's retried up to 3 times, then dropped and logged. Personal apps can queue 1,000 tasks a day and companies 100,000; after that `later` throws `limit_reached`.

## Logs

`console.log`, `console.warn`, `console.error` and uncaught errors are kept: the newest 500 lines per app, for 7 days, each with the time, level and the request path or job. Any value of one of your app's secrets is shown as `[secret]`.

Read them on the app's page in the dashboard (Server code, with a level filter), or ask your AI, which uses `tool_logs` (the last 100 lines). Only the app's owner can read them.

## Outside requests

`fetch()` to the public internet works. Crow Nest's own addresses, IP addresses, `localhost` and internal names are blocked; use `env.CROW` for the app's own data, AI and APIs.

## Packages

Server code is uploaded as is; Crow Nest doesn't run `npm install`. If you use npm packages, bundle them into `server/` first (esbuild or your build tool). Relative imports between files in `server/` work, and so do `node:` built-ins. Bare imports like `import Stripe from "stripe"` are refused at publish with "Bundle your server code first". Up to 10 MB of server code.

## Limits

| | Personal | Companies and organizations |
| --- | --- | --- |
| CPU per request | 50 ms | 500 ms |
| Outside requests per request | 50 | 200 |
| Schedules | Every 15 minutes at most | Every minute |
| Background tasks a day | 1,000 | 100,000 |

A request over its limit answers `503` with a message saying so; other apps are never affected. Function requests count toward Compute, billed at $2 per million beyond your plan's allowance (see [Plans and billing](/pricing)). See [Plans and billing](/pricing).

## Seeing what happens

The app's page in the dashboard shows server code requests, errors and average CPU time for the last 7 days, and the log lines.
