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
// 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 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 |
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:
{ "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:
{ "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
POSTto your path with no signed-in user and the headerx-crow-scheduleset to that job's cron. Crow Nest removesx-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.
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:
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). 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.