---
title: The server library
description: Every global server code has, the web's standard ones and Sluurp's own, with examples, compared with Node, Deno and Bun.
section: Server
order: 2
---

# The server library

<p class="lead">Functions, hooks, jobs and agents run with the same globals as Node, Deno, Bun and browsers, plus a <code>Sluurp</code> object for what the web platform leaves out. Nothing needs importing, and code written for those runtimes mostly runs unchanged.</p>

Every section below is a global, ready in any server file:

```ts title="functions/hello.ts"
export default async function () {
  const page = await fetch("https://example.com").then((r) => r.text());
  const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(page));
  await Sluurp.files.write("pages/example.html", page);
  return { bytes: page.length, sha256: Sluurp.encoding.hex.encode(hash) };
}
```

## Compared with Node, Deno and Bun

| | Node | Deno | Bun | Sluurp |
|---|---|---|---|---|
| `fetch`, `Request`, `Response`, `Headers` | Yes | Yes | Yes | Yes, public addresses only |
| Streams (`ReadableStream` and the rest) | Yes | Yes | Yes | Yes |
| `CompressionStream` | Yes | Yes | Yes | Yes, plus brotli |
| `URL`, `TextEncoder`, `atob`, `Blob`, `File` | Yes | Yes | Yes | Yes |
| `crypto.subtle` | Every algorithm | Every algorithm | Every algorithm | The common ones; see [Crypto](#crypto) |
| Timers, `structuredClone`, `console` | Yes | Yes | Yes | Yes |
| Files | `node:fs`, the machine's disk | `Deno.readFile`, with permissions | `Bun.file`, the machine's disk | `Sluurp.files`, the app's own, in its database |
| YAML, TOML, CSV | From npm | `@std/yaml`, `@std/toml`, `@std/csv` | `Bun.YAML`, `Bun.TOML` | `Sluurp.YAML`, `Sluurp.TOML`, `Sluurp.CSV` |
| JSON5, JSONL | From npm | `@std/jsonc`, from npm | `Bun.JSON5`, `Bun.JSONL` | `Sluurp.JSON5`, `Sluurp.JSONL` |
| tar archives | From npm | `@std/tar` | `Bun.Archive` | `Sluurp.Archive` |
| Password hashing | From npm | From npm | `Bun.password` | `Sluurp.password` |
| gzip in one call | `node:zlib` | From npm | `Bun.gzipSync` | `Sluurp.compress` |
| hex, base32, base64url | `Buffer` | `@std/encoding` | `Buffer` | `Sluurp.encoding` |
| CSRF tokens | From npm | From npm | `Bun.CSRF` | `Sluurp.CSRF` |
| A database | From npm | From npm | `bun:sqlite` | `ctx`, with rules; see [Functions](/docs/server-functions) |
| Node's modules (`node:fs`, `Buffer`, `process`) | Yes | Most | Most | No |
| Child processes, sockets, workers | Yes | Yes | Yes | No |
| npm packages | Yes | Yes | Yes | Those that use only the web's APIs; see [Packages](/docs/vendoring) |
| How code runs | One long process | One long process | One long process | A fresh sandbox per call: about 1 ms and 300 KB |
| What code can reach | The machine | What you allow | The machine | Your data, public addresses, its own files |

The last two rows are the design. Node, Deno and Bun give code the machine, and you decide how far to trust it. Sluurp gives each call a new sandbox that can reach only your data, the public internet and its own files, so a mistake in one function can't leak into another, or into the server. For speed, see [QuickJS: small, safe, fast enough](/docs/server-functions#quickjs-small-safe-fast-enough).

## Network

`fetch(input, init)` works as it does in browsers. Requests run side by side, and a response's body is a stream read as it arrives; see [the event loop](/docs/server-functions#the-event-loop).

```ts
const [a, b] = await Promise.all([fetch(urlA), fetch(urlB)]);
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
const big = await fetch(url, { signal: controller.signal });
for await (const chunk of big.body) process(chunk);
```

- It reaches public addresses only, never this machine or your private network. A host listed in the project's `fetch.allow` setting, one per line, is the exception.
- Redirects are followed, and each one is checked the same way.
- `Request`, `Response` and `Headers` are the standard classes. `Response.json(data)` makes a JSON response.
- `AbortController`, `AbortSignal.timeout(ms)` and `AbortSignal.any(signals)` cancel requests.

## Streams

`ReadableStream`, `WritableStream` and `TransformStream`, with `pipeThrough`, `pipeTo`, `tee`, `getReader`, `for await` and `ReadableStream.from(iterable)`.

```ts
const gzipped = new Response(text).body.pipeThrough(new CompressionStream("gzip"));
const bytes = new Uint8Array(await new Response(gzipped).arrayBuffer());
```

`CompressionStream` and `DecompressionStream` take `gzip`, `deflate`, `deflate-raw` or, beyond the standard, `br` for brotli.

## Text, bytes and addresses

| | |
|---|---|
| `TextEncoder`, `TextDecoder` | UTF-8 to bytes and back. |
| `atob`, `btoa` | Base64 of Latin-1 text, as in browsers. For bytes, use `Sluurp.encoding.base64`. |
| `Blob`, `File` | Bytes with a type, and a name for a `File`: `.text()`, `.arrayBuffer()`, `.bytes()`, `.slice()`. |
| `URL`, `URLSearchParams` | Parse and build addresses and query strings. |
| `structuredClone` | A deep copy that keeps dates, maps, sets and typed arrays. |

## Crypto

`crypto.getRandomValues(array)` and `crypto.randomUUID()` use the operating system's randomness.

`crypto.subtle` has:

| | Algorithms | Methods |
|---|---|---|
| Hashes | SHA-1, SHA-256, SHA-384, SHA-512 | `digest` |
| MACs | HMAC | `sign`, `verify` |
| Signatures | ECDSA (P-256 with SHA-256, P-384 with SHA-384), Ed25519, RSASSA-PKCS1-v1_5, RSA-PSS | `sign`, `verify` |
| Encryption | AES-GCM, 128- or 256-bit keys, 12-byte IV | `encrypt`, `decrypt` |
| Key derivation | PBKDF2, HKDF | `deriveBits`, `deriveKey` |

Keys come in through `importKey` and go out through `exportKey` as `raw`, `pkcs8`, `spki` or `jwk`. `generateKey` makes HMAC, AES-GCM, ECDSA and Ed25519 keys.

```ts
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
const iv = crypto.getRandomValues(new Uint8Array(12));
const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode("secret"));
```

Not here: RSA key generation (make keys elsewhere and import them), RSA-OAEP, AES-CBC, AES-CTR, ECDH and X25519.

## Time and the console

`setTimeout`, `setInterval`, `setImmediate`, their `clear…` twins, `queueMicrotask` and `performance.now()`. Timers fire while a call is still running; when it returns, its timers end with it.

`console.log`, `info`, `warn`, `error` and `debug` write to the server's log, where the admin's **Logs** screen shows them.

## Sluurp.files

Files server code keeps, by path. They live in the app's database, so they're in its backups and replicas, and a path can't reach anything outside the app. For files people upload, use a [file field](/docs/files).

```ts
await Sluurp.files.write("exports/orders.csv", Sluurp.CSV.stringify(rows));
const csv = await Sluurp.files.text("exports/orders.csv");
```

| | |
|---|---|
| `write(path, data)` | A string, bytes, a `Blob` or a `Response`. Gives the size written. |
| `read(path)` | The bytes, or `null` when there's no such file. |
| `text(path)`, `json(path)` | Read as text, or parsed as JSON; `null` when missing. |
| `exists(path)` | Whether there's a file there. |
| `stat(path)` | `{ path, size, modified }`, or `null`. |
| `list(folder)` | Every file under a folder, with sizes and times. Leave the folder out for all of them. |
| `rename(from, to)` | Moves a file, or a folder with everything in it. Gives how many moved. |
| `remove(path)` | Removes a file, or a folder with everything in it. Gives how many went. |

Paths use `/`. A leading `/` and doubled slashes are ignored, and `..` is refused.

## Sluurp.CSV

```ts
const rows = Sluurp.CSV.parse(text, { header: true }); // [{ name: "Ana", city: "Cluj" }, …]
const out = Sluurp.CSV.stringify(rows);                  // with a header row from the keys
```

- `parse(text, { header, separator })` gives arrays of strings, or objects when `header` is true. Quoted fields, doubled quotes and line breaks inside quotes all work, and a byte-order mark is skipped.
- `stringify(rows, { header, separator })` takes arrays or objects. Fields that need quoting get it, and lines end with `\r\n`, as spreadsheets expect.

## Sluurp.YAML, TOML, JSON5 and JSONL

Each has `parse(text)` and `stringify(value)`:

```ts
const config = Sluurp.YAML.parse("name: shop\nports: [80, 443]");
const toml = Sluurp.TOML.stringify({ server: { port: 8080 } });
const loose = Sluurp.JSON5.parse("{ unquoted: 'keys', trailing: [1, 2,], }");
const events = Sluurp.JSONL.parse(logText); // one value per line; a bad line is named by number
```

## Sluurp.Archive

Reads and writes tar files, gzipped or not.

```ts
const archive = new Sluurp.Archive({ "readme.txt": "Hello", "data/rows.json": JSON.stringify(rows) }, { compress: "gzip" });
await Sluurp.files.write("backups/export.tar.gz", await archive.bytes());

const read = new Sluurp.Archive(await Sluurp.files.read("backups/export.tar.gz"));
for (const [name, file] of await read.files("data/*.json")) console.log(name, await file.text());
```

- `new Sluurp.Archive(files, { compress: "gzip" })` makes one from an object or a `Map` of names to strings or bytes.
- `new Sluurp.Archive(bytes)` reads one. A gzipped one is recognised.
- `files(glob)` gives the files as `File`s by name. `*` matches within a folder and `**` across folders.
- `bytes()` and `blob()` give the archive.

## Sluurp.compress and decompress

Whole buffers in one call: `await Sluurp.compress(data, format)` and `await Sluurp.decompress(data, format)`. The format is `gzip` (the default), `deflate`, `deflate-raw` or `br`. The data may be a string, bytes or a `Blob`. For streams, use `CompressionStream`.

## Sluurp.password

```ts
const hash = await Sluurp.password.hash(password); // "$argon2id$…"
const ok = await Sluurp.password.verify(password, hash);
```

Argon2id, as Sluurp's own sign-in uses, with a new salt each time. `verify` takes the same time whether or not the password matches.

## Sluurp.encoding

`hex`, `base64`, `base64url` and `base32`, each with `encode(data)` and `decode(text)`. `encode` takes a string (as UTF-8) or bytes. `decode` gives bytes.

```ts
Sluurp.encoding.hex.encode("hi");        // "6869"
Sluurp.encoding.base64url.encode(bytes); // no padding, URL-safe
Sluurp.encoding.base32.decode("MZXW6YTBOI======");
```

## Sluurp.CSRF

Tokens that prove a form came from your own page:

```ts
const token = Sluurp.CSRF.generate(secret, { sessionId, expiresIn: 60 * 60 * 1000 });
const valid = Sluurp.CSRF.verify(token, { secret, sessionId });
```

A token is signed with HMAC-SHA-256 and carries when it was made. `verify` checks the signature, the session and the age, and returns `false` rather than throwing.
