# SvelteKit

Serve Courier's proxy from a SvelteKit endpoint, with @noverax/courier-sveltekit.

`@noverax/courier-sveltekit` serves the proxy that `createProxyCaller` and `createProxyQueryCaller` call: one route answers every endpoint, reads over `GET` and writes over `POST`, and streams server-sent events.

## In one command

```bash
npx @noverax/create-courier
```

In a SvelteKit app it adds what this page sets up by hand — the endpoint, the registration in `hooks.server.ts`, and Studio's plugin — with a typed example API, its mock, and a page at `/courier`. See [Create Courier](/docs/create-courier).

## Install

```bash
npm install @noverax/courier-sveltekit @noverax/courier @sveltejs/kit
```

## Usage

```ts
// src/hooks.server.ts
import { registerSources } from "@noverax/courier"
import { sources } from "$lib/api"

registerSources(sources)
```

```ts
// src/routes/api/request/+server.ts
import { courierHandler } from "@noverax/courier-sveltekit"

const handler = courierHandler()
export const GET = handler
export const POST = handler
```

`courierHandler()` is zero-arg — sources are read at request time from the core global registry populated by `registerSources()` in `hooks.server.ts`. The handler uses Web Standard `Request`/`Response` natively. A read that may be cached arrives as GET, and every other call as POST.

## In production

SvelteKit answers some calls before the proxy sees them. With `@sveltejs/adapter-node`:

- **Set `ORIGIN`** to the URL the app is served at, such as `ORIGIN=https://app.example.com node build`. Without it SvelteKit cannot tell the protocol it is served over and reports an `https:` request URL, which a browser's `http:` origin never matches, so the proxy refuses every `POST` with 403. Behind a proxy that sets forwarding headers, `PROTOCOL_HEADER` and `HOST_HEADER` do the same job.
- **Raise `BODY_SIZE_LIMIT`**, which is 512 KB by default and answers any larger body with its own 413 `Payload Too Large` — a 900 KB JSON call or any upload over it — whatever the proxy allows. The proxy reads a JSON call up to its `maxBodyBytes`, 1 MB, and an upload up to its `maxUploadBytes`, 10 MB; `BODY_SIZE_LIMIT=11M` covers both, and a larger `maxUploadBytes` needs a larger limit.
- **Callers without an `Origin`**: SvelteKit's CSRF check refuses a `multipart/form-data` or `text/plain` `POST` whose `Origin` does not match, and one with no `Origin` at all, such as a server-side script, with a plain-text 403. Browser pages on the app's own origin pass it.

In `vite dev`, a chunked body larger than the limit resets the connection instead of answering 413, and a client that declares an oversized upload can see the connection close before it has sent it all.

## The client

Pair with `createProxyCaller` on the client, given the routes Courier writes into `courier.routes.ts` from the endpoints you mark `.expose()` — see [Exposing Endpoints](/docs/proxy-mode/expose):

```ts
import { createProxyCaller } from "@noverax/courier"
import { apiRoutes } from "./courier.routes"

// The routes name the key this source has in your `sources` map —
// it is what the proxy addresses, and it travels as `?_s=<name>`.
const caller = createProxyCaller(apiRoutes, {
  proxyPath: { http: "/api/request" },
})
const users = await caller.users.getAll()
```

## The user's session

An API that keeps a `session` gets the user's session with every call a hook, a `load` function, a form action or an endpoint makes, once the request being served can be found — and a proxy caller in universal code, rendered on the server, reaches the proxy in process with it:

```ts title="src/hooks.server.ts — SvelteKit 2.20 and later"
import { getRequestEvent } from "$app/server"
import { registerRequestContext, registerSources } from "@noverax/courier"
import { svelteKitRequestContext } from "@noverax/courier-sveltekit"
import { sources } from "$lib/api"

registerSources(sources)
registerRequestContext(svelteKitRequestContext(getRequestEvent))
```

```ts title="src/hooks.server.ts — any SvelteKit 2"
import { requestContextHandle } from "@noverax/courier-sveltekit/handle"

export const handle = requestContextHandle()
```

`getRequestEvent` follows the request through every `await` where the platform has `AsyncLocalStorage`, and only until the first one where it has none. The `handle` hook needs `AsyncLocalStorage` itself — Node, Bun, Deno, Cloudflare with `nodejs_als`. Either way the session cookies the API sets go to `cookies.set()` exactly as the API wrote them, a `load` function's included. See [User Sessions](/docs/server-side/session).

## Cancelled calls

When the browser cancels a call — an aborted upload, a query it no longer needs — the proxy stops its own upstream request. SvelteKit's request signal does not report a client that leaves after its body has arrived, so under `@sveltejs/adapter-node` the handler watches the client's socket, from `platform.req`, instead. The Vite dev server exposes no connection: during `vite dev` a cancelled call still runs to completion upstream.
