---
title: The handler
description: "The fetch handler contract, and what JavaScript actually runs in production."
---

`src/index.js` must default-export an object literal with a `fetch` method. Durable Object classes and helper functions may be declared above it. It may import from other files in the project and from your own `node_modules`: the CLI bundles with Bun before Porffor sees anything.

```js
export default {
  fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === "/health") return new Response("ok");
    return Response.json({ method: request.method, path: url.pathname });
  },
};
```

:::warning
Two rules differ from Cloudflare Workers:

- **`env` is a global, not a parameter.** The handler signature is `fetch(request, ctx)`, with `ctx.waitUntil` for work that outlives the response. Do not write `fetch(request, env, ctx)`.
- **Bindings are synchronous.** `env.KV.get(key)` returns the value directly. `await` is allowed and harmless but does nothing.
:::

## Extra handlers

The same default export can also carry `scheduled` and `queue`:

```js
export default {
  fetch(request) { /* ... */ },
  scheduled(event) { /* event.cron, event.scheduledTime */ },
  queue(batch) { /* batch.queue, batch.messages, message.ack(), message.retry() */ },
};
```

## What works

Standard synchronous JavaScript and Web platform pieces: `URL`, `Request`, `Response`, `Response.json`, `Headers`, `TextEncoder` / `TextDecoder`, `JSON`, `Math`, `String` / `Array` methods, regular expressions, `Date.now()` and `new Date().toISOString()`, `atob` / `btoa`, `structuredClone`.

Crypto: `crypto.randomUUID`, `crypto.getRandomValues`, and a subset of `crypto.subtle`: `digest` (SHA-256/384/512) and HMAC (`importKey`, `sign`, `verify`). Enough for JWTs (HS256) and hand-rolled sessions; no ECDSA or AES yet. `crypto.scryptVerify(password, salt, expected, { N, r, p })` is a Sproutboat extension for checking scrypt hashes made elsewhere (Node/Bun `scrypt`) during a migration.

`ctx.waitUntil(promise)`, the second argument to `fetch`/`scheduled`/`queue`: work that outlives the response, drained in-process and capped at 25s.

## What does not

| Not available | Notes |
| --- | --- |
| Dynamic `import()`, `require()` | Static imports only: everything has to resolve at build time. |
| `process`, `Bun`, `Deno`, `Buffer`, `node:*` | Rejected at `check` time, in your code and in dependencies alike. |
| `new Proxy` | Porffor compiles it and then ignores every trap, so a trapped property reads back `undefined` with no error. Rejected at build time, so it fails loudly instead of silently. This is why itty-router and other Proxy-based routers do not work. |
| Streams | `ReadableStream` / `WritableStream` are missing upstream, so a response body is one whole string. No SSE or chunked responses; poll instead. |
| Most of `crypto.subtle` | `digest` and HMAC work (see [What works](#what-works)); ECDSA, AES-GCM and key wrapping do not, so most JWT libraries beyond HS256 and `better-auth` still fall short. |
| `WebSocket`, `XMLHttpRequest` | Not in the `http-sync-v0` capability profile. |
| `fetch()` to arbitrary hosts | Allowed only for hosts listed in `outbound` — see [Outbound fetch](/bindings/outbound-fetch). |
| Filesystem | No `fs`. Static files go through the [assets binding](/bindings/static-assets). |
| Parsing date strings | `new Date("2024-01-02")` is unreliable on the current Porffor pin. Use timestamps. |

:::tip
`sproutboat check` reports most of these before you build.
:::

**npm packages work, within those limits.** Small pure-JavaScript libraries are fine. Anything that reaches for a platform API, ships native code, or uses a Proxy is not. Note that zod does not currently run, which rules out most libraries that depend on it.

## Next

**[Generated types](/concepts/types)**

Get autocomplete on `env` and catch the Workers-style handler mistake at compile time.

**[sproutboat.jsonc](/concepts/configuration)**

The full config shape: bindings, vars, secrets, assets, triggers.
