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

The entry point is `src/index.js` by default; `main` can also point to a TypeScript file such as `src/index.ts`. It must default-export an object literal with a `fetch` method. Durable Object classes and helper functions can be declared above it. The CLI bundles the handler and its imports with Bun, which strips TypeScript syntax before Porffor compiles the JavaScript. To see type errors, run a separate type check.

```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 });
  },
};
```

## Handler arguments

The arguments depend on `compatibility_date` in [`sproutboat.jsonc`](/docs/concepts/configuration):

| `compatibility_date` | Signature |
| --- | --- |
| `2026-09-28` or later | `fetch(request, env, ctx)`, the Workers signature. `scheduled` and `queue` also receive `env` second. |
| Earlier | `fetch(request, ctx)`. `env` is only a global. |

`sproutboat init` writes `2026-09-28` and a `fetch(request, env, ctx)` handler, so a new project starts with the Workers signature. Projects created by earlier CLI versions keep the date in their `sproutboat.jsonc`; set it to `2026-09-28` or later to switch.

`env` is also available as a global in both modes.

:::warning
Before the compatibility date that changes each binding, calls are synchronous: `env.KV.get(key)` returns the value directly. From `2026-10-24` D1 calls return Promises, as on Workers (KV from `2026-10-25`, R2 from `2026-10-26`; see [Bindings](/docs/bindings)). `await` on a binding call is allowed at every date and returns the same value, so always await them.
:::

## 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

**JavaScript and Web APIs.** Standard synchronous JavaScript, plus `URL`, `URLSearchParams`, `Request`, `Response`, `Response.json`, `Response.redirect`, `Headers`, `FormData`, `TextEncoder` and `TextDecoder`, `JSON`, `Math`, regular expressions, `Date.now()` and `new Date().toISOString()`, `atob` and `btoa`.

**Request and response bodies.** Bodies are buffered. `text()`, `json()`, `arrayBuffer()`, `bytes()`, `blob()` and `formData()` return their value directly; `formData()` parses URL-encoded and multipart bodies.

**`structuredClone`** copies through JSON, so `Map`, `Set`, `Date` and typed arrays do not survive it.

**Crypto.** `crypto.randomUUID`, `crypto.getRandomValues`, and part of `crypto.subtle`: `digest` (SHA-256, SHA-384, SHA-512) and HMAC `importKey`, `sign` and `verify`. That covers HS256 JWTs and signed session cookies. `crypto.scryptVerify(password, salt, expected, { N, r, p })` is a Sproutboat extension for checking scrypt hashes created by Node or Bun, for example during a migration.

**Cache API.** `caches.default` and `caches.open(name)` with `match`, `put` and `delete`. Calls return directly. Entries are keyed on the URL, only `GET` requests can be stored, and expiry comes from `s-maxage` or `max-age`. `Vary` is ignored. Store text bodies only.

**`request.cf`.** `clientIp` is the client's address. When the connection comes from a proxy listed in `SB_TRUSTED_PROXIES`, `clientIp` comes from `X-Forwarded-For`, and `httpProtocol`, `tlsVersion` and `tlsCipher` are filled from the proxy's headers.

**`ctx.waitUntil(promise)`** runs work after the response is sent, in the same process, for up to 25 seconds.

**Node built-ins.** `node:path`, `node:url` and `node:querystring` work through built-in shims. See [Runtime compatibility](/docs/concepts/compatibility) for tested npm packages.

## What does not

| Not available | Notes |
| --- | --- |
| Dynamic `import()`, `require()` | Static imports only: everything has to resolve at build time. |
| `process`, `Bun`, `Deno`, `Buffer` | Runtime globals are unavailable in the handler. |
| Other Node built-ins | Only the three shims above. |

| Streams | `ReadableStream`, `WritableStream` and `TransformStream` work in memory. HTTP bodies remain buffered, and stream BodyInit is unsupported. No server-sent events or chunked responses; poll instead. For large files, use R2's [direct transfer URLs](/docs/bindings/r2#large-files). |
| Most of `crypto.subtle` | ECDSA, RSA, AES and key wrapping are unavailable, so JWT algorithms other than HS256 and libraries such as `better-auth` do not work yet. |
| `WebSocket`, `XMLHttpRequest` | Not in the `http-sync-v0` capability profile. |
| `fetch()` to a private address | Loopback, private networks and the cloud metadata endpoint are refused; see [Outbound fetch](/docs/bindings/outbound-fetch#private-addresses). |
| Filesystem | No `fs`. Serve files through [static assets](/docs/bindings/static-assets) or store them in [R2](/docs/bindings/r2). |
| Parsing date strings | `new Date("2024-01-02")` is unreliable on the current Porffor version. Use timestamps. |

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

## Next

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

Autocomplete on `env` and the handler signature for your compatibility date.

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

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

## Proxy and iterators

CLI 0.17.0 uses Porffor alpha-17 and accepts `new Proxy` and `Proxy.revocable`. Native regression checks cover common object traps, callable and constructible proxies, revocation and a frozen-property invariant. `URLSearchParams` and `FormData` provide live keys, values and entries iterators. See [Runtime compatibility](/docs/concepts/compatibility) for tested packages and remaining iterator limits.
