Crow NestDocs

Build apps

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 envWhat it is
env.CROW.viewerThe signed-in person ({ userId, email, name }) or null. Set by Crow Nest; a request can't fake it
env.CROW.dbThe app's crow.db 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, 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

PersonalCompanies and organizations
CPU per request50 ms500 ms
Outside requests per request50200
SchedulesEvery 15 minutes at mostEvery minute
Background tasks a day1,000100,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). See Plans and billing.

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.