> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Behind a reverse proxy

> Which paths a reverse proxy in front of Veriqa has to pass, what the WebSocket of the sign-in page needs, and a ready NGINX configuration.

A self-hosted Veriqa speaks HTTP and expects TLS to end at a proxy in front of it. This page lists
what that proxy has to pass through. Trusting the proxy on the Veriqa side — the `ForwardedHeaders`
section — is covered in
[self-hosted, step 6](/docs/quickstart/self-hosted#6-put-it-behind-a-reverse-proxy).

## Give Veriqa a host name of its own

The simplest and the recommended layout is a dedicated host name — `auth.your-domain.com` — whose
every request goes to Veriqa. Nothing has to be listed, nothing collides with your application, and
a path added by a later release works without touching the proxy. That address is also the
`issuer` your applications validate tokens against: an instance has one issuer, so applications on
different domains — a staging site and a production one, for example — all point at the same
address rather than each proxying Veriqa under its own domain.

If you mount Veriqa into the host name of your application instead, there are two ways. Pass every
path of the table below: Veriqa then serves them from the root of the host name, so `/_content/`
and `/hubs/` are taken on it. Or keep the root to your application and put the whole of Veriqa
[under a path prefix](#under-a-path-prefix).

## Paths

| Path prefix | Who calls it | What it is |
| - | - | - |
| `/.well-known/` | your application | Discovery document and signing keys |
| `/connect/` | browser and your application | The OIDC endpoints and the sign-in page with its actions |
| `/auth/` | browser | Pages outside the OIDC path: the confirmation page, the page that collects a missing phone number, email or profile field on the device that confirmed the sign-in, and the pages of the Email channel that mail links lead to |
| `/hubs/` | browser, WebSocket | Live status of the sign-in page and of the page that collects missing details (SignalR) |
| `/api/transaction/` | browser and your application | Status polling when the WebSocket is not available; creating a confirmation transaction and reading its result |
| `/_content/` | browser | Static files of the sign-in page |
| `/api/channels/` | messaging platforms | Channel webhooks — must be reachable from the internet in `Webhook` mode |
| `/r/` | browser | Short links of the hop mode — only when it is on; the prefix is `Veriqa:HopMode:HopPath` |
| `/api/link-handle/` | your application | Exchange of a [link handle](/docs/scenarios/channel-linking#two-bots-handing-the-user-over-to-your-bot) for the identity of the signed-in user — only when a post-completion button carries `{handle}` |
| `/api/collected-claims` | your application | Deletion of collected claims — only with a [collected claims store](/docs/guides/storage#collected-claims-store) |

`/health/live`, `/health/ready` and `/health/channels` are for your own monitoring and need not be
published.

<Warning>
  A list limited to `/.well-known/`, `/connect/`, `/hubs/`, `/api/transaction/` and `/_content/` is
  enough for a plain sign-in and looks complete. It stops being enough the day you ask for a phone
  number or an email, enable the Email channel or switch a bot to `Webhook`: those pages and
  webhooks live under `/auth/` and `/api/channels/`.
</Warning>

## WebSocket

The sign-in page learns that the user has confirmed through the `/hubs/auth` hub. The proxy must
let the connection upgrade — pass the `Upgrade` and `Connection` headers and use HTTP/1.1 towards
Veriqa — and must not cut an idle connection sooner than a sign-in may last. When the upgrade does
not get through, the page falls back to polling `/api/transaction/{id}/status`: the sign-in still
completes, with a delay.

## NGINX

```nginx theme={null}
# http context
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    http2 on;
    server_name auth.your-domain.com;

    ssl_certificate     /etc/nginx/certs/server.crt;
    ssl_certificate_key /etc/nginx/certs/server.key;

    # Sign-in requests carry cookies; the 8k default answers 400 on a longer header
    large_client_header_buffers 4 32k;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket of the sign-in page
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_read_timeout 300s;
    }
}
```

When Veriqa shares the host name with your application, keep the body of the `location` block and
repeat it for each prefix of the table instead of `/`:

```nginx theme={null}
location ~ ^/(\.well-known|connect|auth|hubs|api/transaction|api/channels|_content)/ {
    # the same proxy_* directives as above
}
```

The repository carries a complete configuration with TLS settings and an HTTP-to-HTTPS redirect:
`samples/docker-compose/nginx.conf` ([samples](/docs/samples)).

## Under a path prefix

With a path prefix every address of Veriqa starts with it —
`https://app.your-domain.com/auth/veriqa/connect/token` — and the root paths of the host name stay
with your application. Set the prefix on the Veriqa host and repeat it in every public address you
configure:

| Key | Value |
| - | - |
| `Veriqa:PathBase` | `/auth/veriqa` |
| `Veriqa:OpenIddict:Server:Issuer` | `https://app.your-domain.com/auth/veriqa` |
| `Veriqa:Channels:Email:PublicBaseUrl`, `WebhookBaseUrl` of a channel in `Webhook` mode | the same address, with the prefix |

`Veriqa:PathBase` starts with `/`, does not end with `/` and may have several segments. It is
written as plain text: no `?`, `#`, `%`, `\`, whitespace, and no `.` or `..` segment. A value of
another form stops the host at startup; an empty one means the root. The prefix applies whichever
way the proxy forwards the path — use one of the two blocks:

```nginx theme={null}
# The path is passed as it is: Veriqa receives /auth/veriqa/connect/token
location /auth/veriqa/ {
    proxy_pass http://127.0.0.1:8080;
    # the same proxy_* directives as above
}

# The prefix is trimmed: Veriqa receives /connect/token
location /auth/veriqa/ {
    proxy_pass http://127.0.0.1:8080/;
    # the same proxy_* directives as above
}
```

`https://app.your-domain.com/auth/veriqa/.well-known/openid-configuration` then returns the `issuer`
and every endpoint with the prefix. A client that reads the metadata by RFC 8414 asks for it at the
root of the host name, with the path of the issuer appended. Map that address:

```nginx theme={null}
location = /.well-known/oauth-authorization-server/auth/veriqa {
    proxy_pass http://127.0.0.1:8080/auth/veriqa/.well-known/oauth-authorization-server;
    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}
```

This mapping is verified for a proxy that passes the path as it is. With a proxy that trims the
prefix the target is the same address without `/auth/veriqa`; that variant has not been verified.

What to keep in mind:

* **A prefix that a path of Veriqa itself starts with** — `/auth`, `/api`, `/connect`, `/hubs`,
  `/_content`, `/health`, or the path of hop links (`/r` by default, `Veriqa:HopMode:HopPath`) —
  works only when the proxy passes the path as it is. Behind a trimming proxy such a prefix answers
  `404`. `/auth/veriqa` is not one of them.
* **Requests that reach an instance directly go without the prefix** — a health probe asks
  `http://127.0.0.1:8080/health/live`. Veriqa cannot tell such a request from one the proxy
  trimmed and serves both, so closing the root of the instance from the outside is the job of the
  proxy.
* **Several instances.** The proxy has to keep a browser on one instance (session affinity,
  `ip_hash` in NGINX): without it the hub of the sign-in page does not connect. The Email channel
  has a limit of its own — the multi-instance caveat in
  [Channel setup, Email](/docs/guides/channels#email). Neither depends on the prefix.
* **Verified under a prefix:** the sign-in by Email in both modes with its inbound webhook, the
  messenger webhook (`Webhook` mode), the [phone number](/docs/guides/phone-number) request, confirming
  an email address by a code or by a message the user sends during
  [claim enrichment](/docs/concepts/claim-enrichment), the tokens and `userinfo`, the form that collects
  a missing profile field, and a confirmation transaction created by your server. The messenger
  part was checked on Telegram only.

`Veriqa:PathBase` is a setting of the shipped host. When Veriqa is embedded in your ASP.NET Core
application, the prefix is the `UsePathBase` of that application, and discovery carries it.

## Other proxies

Traefik, Caddy and IIS need the same three things: every path of the table, the `X-Forwarded-Proto`
and `X-Forwarded-For` headers, and the WebSocket upgrade. Traefik and Caddy pass the upgrade
without configuration; in IIS the WebSocket Protocol feature has to be installed. Under a path
prefix the paths of the table give way to the prefix alone — see
[Under a path prefix](#under-a-path-prefix).

## Check

* `https://auth.your-domain.com/.well-known/openid-configuration` returns `https` URLs on your host
  name. `http` URLs or `invalid_request` mean the proxy is not trusted —
  [self-hosted, step 6](/docs/quickstart/self-hosted#6-put-it-behind-a-reverse-proxy).
* On the sign-in page the browser's network tab shows `/hubs/auth` answered with `101 Switching
  Protocols`.
* `Request Header Or Cookie Too Large` — raise `large_client_header_buffers`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.