---
title: Functions and "use server"
description: Endpoints in a folder, and functions written in a page that run on the server.
section: Server
order: 1
---

# Functions and "use server"

<p class="lead">Server code is JavaScript or TypeScript that runs in a sandbox as the calling user, so every rule still applies.</p>

## "use server"

Mark a function in a page with `"use server"` and call it from the browser like any other function. In the code sent to the browser, its body is replaced with a call to `/api/rpc`.

```ts title="islands/add-note.tsx"
export async function addNote(title: string, at: Date) {
  "use server";

  // who may call it (default: anyone)
  "allow: @request.auth.staff = true";
  const made = await server.collection("notes").create({ title });
  return { made, next: new Date(at.getTime() + 86_400_000), tags: new Set(["new"]) };
}

// in the browser
// Date and Set arrive as real Date and Set
const { next, tags } = await addNote("Milk", new Date());
```

- Arguments and return values travel as [devalue](https://github.com/sveltejs/devalue), so Dates, Maps, Sets, BigInts, `undefined` and repeated objects arrive intact.
- Without `"allow: …"`, anyone can call it. It runs as the caller, so collection rules still apply.
- Called from a server component during rendering, it runs directly, with no HTTP request.
- The body is extracted and run on its own, so top-level imports of the page aren't available inside it.

## Endpoints

Each file in `functions/` becomes an endpoint at `/api/fn/<name>`, versioned and rolled back with the app.

```ts title="functions/register-summary.ts"
// the app's own modules
import { average } from "../lib/marks.ts";
import terms from "../data/terms.json" with { type: "json" };

export const rule = "@request.auth.id != null";
export const method = "GET";

export default async function (ctx) {
  const page = await ctx.collection("grades").list({ filter: `student = "${ctx.auth.id}"` });
  return { average: average(page.items.map((g) => g.value)), term: terms.autumn };
}
```

`export const runAs = "system"` bypasses the caller's rules, e.g. for aggregates everyone may see even though individual rows are private. The function then *is* the policy, so return aggregates, never rows.

## Hooks and jobs

A file in `hooks/` runs before every write to its collection, whatever the source of the write. Its reads bypass rules since it's policy, not user code, so it can enforce things rules can't, like counts or cross-row checks.

```ts title="hooks/classes.ts"
export async function beforeCreate({ record, db, reject }) {
  const held = await db.count("classes", `responsible = "${record.responsible}"`);
  if (held >= 2) reject(`already responsible for ${held} classes`);

  // the record to write
  return { ...record, name: record.name.trim() };
}
```

```ts title="jobs/nightly.ts"
export const schedule = "0 2 * * *";
export default async function (ctx) { /* … */ }
```

## The sandbox

Each call gets a fresh QuickJS context, with 64 MB of memory and 5 seconds of computation. It has `ctx`, and the web's standard globals as Deno and Bun have them: `fetch`, `URL`, `TextEncoder`, `crypto`, timers, `AbortController` and `structuredClone`. `fetch` reaches public addresses only, never this machine or your private network. There's no file system and no `process`. Imports of the app's own files are bundled in ahead of time.

## The event loop

Server code runs on an event loop, as it does in Node. `fetch` starts a request and returns straight away, so requests made together run side by side:

```ts title="functions/prices.ts"
export default async function () {
  const [eur, usd] = await Promise.all([
    fetch("https://example.com/rates/eur").then((r) => r.json()),
    fetch("https://example.com/rates/usd").then((r) => r.json()),
  ]);
  return { eur, usd };
}
```

A response's body is a stream, read as it arrives. That suits large downloads, server-sent events, and long polls:

```ts title="functions/follow.ts"
export default async function () {
  const response = await fetch("https://example.com/events");
  let lines = 0;
  for await (const chunk of response.body) {
    lines += new TextDecoder().decode(chunk).split("\n").length - 1;
    if (lines > 100) break;
  }
  return { lines };
}
```

The 5 seconds count only the time spent running JavaScript. Time spent waiting, on a request or a timer, doesn't count, so a function may wait on a slow service for minutes. A loop that never ends is still stopped. A function that awaits a promise nothing can settle fails at once, rather than hanging.

While a function waits it holds no thread, so thousands can wait at once. JavaScript runs on its own pool of threads, one per core, apart from the threads that answer requests. A function that computes for a while never slows the rest of the app. `AbortController` cancels a request. When the client that called a function goes away, the function is stopped.

Hooks and views are meant to be quick. Their `fetch` waits in place and gives up after 5 seconds.

## QuickJS: small, safe, fast enough

Server code runs in [QuickJS](https://bellard.org/quickjs/), a small JavaScript engine built into the binary. It isn't V8 (Node, Deno, Chrome), JavaScriptCore (Bun, Safari) or SpiderMonkey (Firefox). Those engines compile hot code to machine code as it runs (a JIT); QuickJS interprets it. What that means for you:

- **Heavy computation is slower.** Tight loops and long calculations are about 2 to 18 times slower than in V8. Keep them out of server code, or let the database do them.
- **Each call starts fresh and small.** A new sandbox takes about 3 ms, so one call can't leave state for the next or reach another's. That's safer than one long-lived process running everyone's code.
- **Modern JavaScript, not every API.** ES2023 syntax works (async and await, classes, modules, optional chaining, `BigInt`), but not WebAssembly, threads (`Worker`, `SharedArrayBuffer`) or Node's modules.

For what server functions are for, it's plenty. They check input, read and write a few records through `ctx` (and the database work runs in SQLite, in native code), shape the answer, and perhaps call another service. That takes a few milliseconds.

Measured on one Windows laptop, the fastest of three runs, in milliseconds:

| | Node 24 | Deno 2.9 | Bun 1.4 | Sluurp (QuickJS) |
|---|---|---|---|---|
| A recursive function, `fib(25)` | 0.6 | 0.6 | 0.7 | 11 |
| 5,000 records to JSON and back, five times | 9.5 | 7.5 | 7.2 | 52 |
| Sort 100,000 numbers | 28.7 | 28.8 | 15.3 | 50 |
| Build, join, replace and split 50,000 strings | 5.0 | 5.1 | 5.7 | 42 |
| Filter, group and average 2,000 records, 50 times | 1.4 | 1.5 | 1.2 | 14 |
| A whole call to a function that returns `{ hello }`, over HTTP | – | – | – | 4.9 |

For a comparison of engines on standard benchmarks, see [QuickJS's own benchmarks](https://bellard.org/quickjs/bench.html). Code that needs a JIT, a thread or a native module belongs in a service of its own. It can use your data through the [REST API](/docs/api) and send Sluurp [events](/docs/hooks-and-events#events-from-outside).
