All chapters
Server code
Schedules, webhooks, email and heavy work for an app
Server code can do more than answer the page. It can run on a schedule, be called by other services, send email to the people who use the app, and hand heavy jobs such as PDF reports or video work to a container. All four need the server/ folder described in Server code. The schedules and webhooks are declared in scava.json.
{
"server": {
"schedules": [
{ "name": "morning", "cron": "0 7 * * 1-5", "timezone": "Europe/Vienna" }
],
"webhooks": ["stripe"]
}
}
Schedules #
A schedule runs the function scheduled(event, env, ctx) of server/index.js.
export default {
async scheduled(event, env) {
// event: { id, name, cron, scheduledTime, attempt }
if (event.name === 'morning') {
const open = (await env.SCAVA.db('requests').list()).filter((e) => e.data.status === 'open');
await env.SCAVA.mail({
to: 'team-lead@company.com',
subject: open.length + ' open requests',
text: 'Good morning. There are ' + open.length + ' open requests.',
});
}
},
};
nameuses lowercase letters, digits,-and_.cronhas the usual five fields: minute, hour, day of month, month, day of week. Scava understands*, lists (1,15), ranges (1-5), steps (*/15,8-18/2), names (mon-fri,jan-dec) and the shortcuts@hourly,@daily,@midnight,@weekly,@monthlyand@yearly.timezoneis a time zone name such asEurope/Vienna. The default isUTC.- Each plan has a shortest allowed interval and a number of schedules per app; see Limits. A version that asks for more is refused with the reason.
At least once #
A schedule runs at each planned time while the app is live. A run that fails is tried once more a minute later. Times that pass while Scava is down are not made up afterwards. Both tries carry the same event.id; the second has event.attempt set to 2. Use the id to avoid doing the same job twice, for example by saving it in a collection.
The app page shows the last run of each schedule with its result. The owner can press "Run now" to try a schedule by hand. get_server_info shows the same to an agent.
Webhooks #
A webhook lets another service, such as a payment provider or a form tool, call your app without signing in. Declare the name in scava.json. The address is
https://<address of the app>/_scava/hook/stripe?key=<key>
- The key is created by Scava and shown to the owner on the app page. Paste the full address into the other service. It is also accepted in the header
x-scava-hook-key. - The call arrives in
fetch()with the path/_scava/hook/stripe. The key is removed from the address before your code sees it.env.SCAVA.me()isnull, because no person signed in. - A wrong or missing key gets a 403. A name that is not declared gets a 404.
- The owner can create a new key. The old address stops working at once.
- Agents and co-editors see the address without its key. The key is a password.
If the other service signs its calls, check the signature in your code. Store that signing secret as a readable secret; see Server code.
Email #
await env.SCAVA.mail({
to: ['anna@company.com', 'ben@company.com'],
subject: 'Your vacation was approved',
text: 'Your vacation from 22 December was approved.',
});
// { sent: 2, failed: 0 }
- Plain text only. Subject and text are cleaned and cut when too long.
- Only to people who can open the app: the owner, co-editors, and people the app is shared with by address, domain or group, and who the house rules do not keep out. A mail to anyone else is refused, so an app cannot send email to strangers under Scava's name.
- Links in the text only to the app itself. Other links become "[link removed]".
- At most 50 addresses per call.
- Scava adds a line that names the app and a link to report it. The mails leave from a sender of their own, separate from the sign-in codes.
- Mails per month depend on the plan, for all apps of the owner together; see Limits. There is also a daily cap per app that only runaway code reaches.
Heavy work #
Some jobs do not fit into a request: building a PDF report, converting a spreadsheet, resizing images, cutting a video. Put a script in server/heavy/ and run it from server code.
# server/heavy/report.py
import csv, json, os, sys
data = json.load(sys.stdin) # the input arrives as JSON on stdin
os.makedirs("out", exist_ok=True)
with open("out/summary.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["name", "hours"])
for row in data["rows"]:
writer.writerow([row["name"], row["hours"]])
print(json.dumps({ "count": len(data["rows"]) })) # stdout can be JSON
// in server/index.js
const r = await env.SCAVA.run('report.py', { rows: [{ name: 'Anna', hours: 38 }] }, { timeout: 60000 });
// r = { ok, exitCode, timedOut, ms, stdout, stderr, data, files: [{ name, size, base64 }] }
if (r.ok) {
const csv = Uint8Array.from(atob(r.files[0].base64), (c) => c.charCodeAt(0));
await env.SCAVA.files('reports').put('summary.csv', csv, { type: 'text/csv' });
}
- The script file ends in
.py,.sh,.jsor.mjs, and lives inserver/heavy/. Files in this folder are scripts for the container, not modules of the server code. - Every run starts in a fresh folder that holds only your scripts, and the folder is deleted afterwards. The input is JSON on stdin; files the script writes directly into
out/come back infiles(up to 100 files). The script and input together, and the files coming back, are limited to 20 MB per run. - The container has Python 3.12 with numpy, pandas, matplotlib, reportlab, openpyxl, pillow, pypdf, python-docx, jinja2 and qrcode, Node.js with npm, and ffmpeg.
- The container has no internet of its own. Its web requests (http and https) go through Scava, which records each call under "talks to", counts the data against the monthly limit, refuses Scava's own addresses, follows the house rules of the owner, and inserts keys that are bound to a host. This lets a script run
pip install --user ...ornpm install ...for its own run, and call outside services. - At most two runs at a time per account. A third waits briefly and then fails with a message to try again in a moment.
- Run time per run and per month depend on the plan; see Limits. A run that exceeds its time is stopped and reported as a problem of the app.
- Bound secrets appear in the script's environment as placeholders, like in server code. Readable secrets are not given to scripts.
Use heavy work for jobs that take seconds to minutes. For quick answers use the normal fetch() handler.