# Deployment Overview

Choose the right deployment strategy for your Courier application — standalone server, framework adapter, single or multi-replica.

## Deployment Models

Courier supports two deployment models for realtime features (SSE and WebSocket):

| Model | Best For | Packages |
|-------|----------|----------|
| **Framework adapter** | Apps already using Express, Fastify, Hono, or NestJS | `@noverax/courier-express`, `-fastify`, `-hono`, `-nestjs` |
| **Standalone server** | Next.js (WS needs separate process), dedicated realtime service | `@noverax/courier-server` |

> **Next.js limitation**
>
>   Next.js App Router cannot handle WebSocket upgrades. Use `sseRoute()` for SSE, and run a separate `@noverax/courier-server` for WebSocket support.

## Single vs Multi-Replica

| | Single Replica | Multi-Replica |
|---|---|---|
| **WS broadcast** | Nothing to add — the handler delivers to its own connections | `createRedisPubSub()`, passed to the server as `pubsub` |
| **Stream history** | `createMemoryStream()`, read by your stream handlers | `createRedisStream()`, read by your stream handlers |
| **Presence** | `createMemoryPresence()`, used by your channel handlers | `createRedisPresence()`, used by your channel handlers |
| **Use case** | Development, single-process prod | K8s, Docker Swarm, any horizontal scaling |

A single process needs no adapter to broadcast. The stream and presence adapters are building blocks your handlers call — the server does not read or write them on its own — and the Redis ones share their state across replicas. [Redis Adapters](/docs/deployment/redis) shows a stream that resumes from them and a room that tracks who is in it.

## Decision Tree

1. **Do you need realtime?** (SSE or WebSocket)
   - No → standard `apiRoute()` / `courierHandler()` is enough
   - Yes → continue

2. **Which framework?**
   - Next.js → `sseRoute()` for SSE + `@noverax/courier-server` for WS
   - Express / Fastify / Hono / NestJS → framework adapters handle both SSE and WS natively
   - No framework → `@noverax/courier-server` standalone

3. **How many replicas?**
   - Single → no pub/sub adapter; memory stream and presence adapters where your handlers need them
   - Multiple → Redis adapters + sticky sessions

## Health Checks

Every Courier server (standalone or framework adapter) provides two health endpoints:

- **`GET /health`** — always returns `200 { "status": "ok" }` (liveness probe)
- **`GET /readyz`** — returns `200` when ready, `503` during shutdown (readiness probe)

Use these with Kubernetes, Docker health checks, or load balancer configuration.

## Logs

Courier writes `info` and `debug` lines to stdout and `warn` and `error` lines to stderr, so a log router that tells the two streams apart sees Courier's failures as errors. A failure carries the error's stack and `cause` chain. `COURIER_LOG_LEVEL` sets the lowest level printed — `debug`, `info` (the default), `warn`, `error` or `silent` — and a container's log gets plain text with the level as a word, since colour is written only to a terminal (`FORCE_COLOR=1` keeps it, `NO_COLOR` removes it everywhere). For a log pipeline — Datadog, Loki, Elasticsearch, Cloud Logging — set `COURIER_LOG_FORMAT=json`: each line is then one JSON object with `time`, `level`, `scope`, `pid`, `message` and, for a failure, `error` with its stack and cause chain.

A browser that cancels a call is not a failure, and is logged at `debug`: an upload it stopped sending, a query it stopped waiting for, a stream it stopped reading. The proxy stops its upstream request and answers such a call with status `499`, which only an access log ever sees.

## Behind a Reverse Proxy

The proxy route refuses a request another site's page made — the check that stops another site's page from calling your API with your users' cookies. A current browser says where a request comes from in `Sec-Fetch-Site`, and where it does, that decides, whatever name the app thinks it has. Where it does not — an older browser, or a site served over plain HTTP — the page's `Origin` is compared with the origin the request was sent to. Behind nginx, a cloud load balancer or a CDN that terminates TLS, the app itself sees plain HTTP, and often the upstream's name in `Host`, so it needs the proxy to say what the browser used.

The same name decides which host the request was addressed to, which every route checks against `allowedHosts` to refuse a DNS-rebinding page with `421`. Production serves every name until `allowedHosts` lists them; list the public names your proxy routes to the app to refuse the rest as well — see [Host validation](/docs/proxy-mode/overview#host-validation).

The Node adapters (Express, Fastify, NestJS and the standalone server) read `X-Forwarded-Proto` and `X-Forwarded-Host` — but only from a proxy on a loopback or private address (`127.0.0.1`, `::1`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fc00::/7`). A caller on a public address cannot claim another site's name with them. Next.js reports every route's URL on the host it was started with, so its adapter reads `X-Forwarded-Host` and `X-Forwarded-Proto`, which Next fills from `Host` when no proxy did, as Next's own Server Actions do. SvelteKit and the other Web-standard adapters take the URL from their framework, which applies its own proxy settings.

```nginx title="nginx.conf"
location / {
  proxy_pass http://127.0.0.1:3000;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```

When the proxy reaches the app from a public address, or pages on other origins call the API on purpose, list those origins instead:

```ts
courierHandler({ allowedOrigins: ["https://app.example.com", "https://*.example.com"] })
```

### The user's address

An API a call goes to through your server sees your server's address. Courier tells it the user's in `X-Forwarded-For`, as every proxy does: the chain the proxies in front recorded, with the address of the one that connected appended — the same header nginx's `$proxy_add_x_forwarded_for` writes. A caller that connects from a public address is the whole chain, since what it claims about addresses before it is its own word; a connection from this machine or a private network is a proxy, and its chain is kept. Next.js, React Router and edge functions do not report the connection: there the chain goes on as the proxy in front, or Next.js itself where none is, recorded it. Each API reads the user's address from the chain with its own trust, as it reads any proxied request's.

Courier counts a user's connections by one address — the WebSocket limit per address, the limits you set per caller. It reads that address from the right of the chain, past this machine and private networks, since each proxy appends what it saw and an address a user forges stands left of it. A DDoS filter, a CDN or a load balancer on public addresses is one more proxy to read past: name its addresses and ranges, and the header it names the user in, if it has one.

```bash title=".env"
COURIER_TRUSTED_PROXIES=192.0.2.0/24,2001:db8:aa00::/40
COURIER_CLIENT_ADDRESS_HEADER=DDG-Connecting-IP
```

Courier believes that header only on a request one of those proxies passed on — the nearest address on the chain past your own network is one of theirs, or they connected themselves. Name only your own proxies and your provider's published ranges: Courier believes whatever address they report. An entry that is neither an address nor a range, or a range of every address, is left out, and the log says so once.

An API that reads the user's address from one header — `X-Real-IP`, `X-Client-IP`, one of its own — or believes the first address of the chain, as the `request-ip` library and many others do, can be sent the address Courier reads, alone. Name its headers in `forward.clientAddress`, and the request's id under every header the API logs it by in `forward.requestId`:

```ts
defineApi({
  baseURL: process.env.API_URL,
  forward: {
    clientAddress: ["x-client-ip", "x-real-ip"],
    requestId: ["x-request-id", "x-correlation-id"],
  },
  endpoints,
})
```

`X-Forwarded-For` still carries the chain; name `x-forwarded-for` too to send the address alone there. A user can write a chain of their own, and a proxy that appends keeps it in front of the address it saw: an API that believes the chain's first address then names whoever the user said, and one sent the address alone does not.

## Next Steps

- [Standalone Server](/docs/deployment/standalone-server) — full API and CLI reference
- [Redis Adapters](/docs/deployment/redis) — multi-replica setup
- [Replication Guide](/docs/deployment/replication) — per-framework examples
- [Docker](/docs/deployment/docker) — Dockerfile, Compose, and Swarm
- [Kubernetes](/docs/deployment/kubernetes) — manifests, ingress, HPA
- [Cloudflare Workers](/docs/deployment/cloudflare) — edge deployment
