# Go to production

URL: https://better-helpdesk.com/docs/operations/production/

The checklist between a helpdesk that works on your machine and one your customers can rely on, and what to do on every upgrade after that.



Work through this page once before the first deploy, with the app from [Install in an existing app](/docs/installation) in front of you. Every section is one thing to check or set.

## Migrate before you deploy [#migrate-before-you-deploy]

Run the migration command in your release step, against the production database, before the new version of your app starts:

```sh
npx better-helpdesk-migrate
```

It reads the connection string from `HELPDESK_DATABASE_URL`, or `APP_DATABASE_URL` when that is unset, opens a single connection, applies the migrations that have not run yet, and exits. It fits next to your own migrations in the same step.

The order matters: migrate first, then deploy. The new version's queries expect the schema its migrations create, and a query that uses a new table or column fails until its migration has run.

## Connect over TLS [#connect-over-tls]

The migration command and your app open their connections separately, and each has its own setting. The settings below are for Postgres. On SQL Server the command encrypts by default, as [SQL Server](/docs/database/connect#sql-server) describes. On MySQL, turn TLS on in the pool your app creates, and run the migrations through that pool, as in [Run the migrations from your own code](/docs/database/connect#run-the-migrations-from-your-own-code).

For the command, `DATABASE_SSL=true` turns on TLS and checks the server's certificate. `DATABASE_SSL=no-verify` encrypts without checking it, for a database with a self-signed certificate. A `sslmode` in the connection string replaces what `DATABASE_SSL` sets.

For your app, add `sslmode=verify-full` to `HELPDESK_DATABASE_URL`:

```sh
HELPDESK_DATABASE_URL=postgres://user:password@db.example.com:5432/app?sslmode=verify-full
```

`pg` 8 treats `sslmode=require` the same way and prints a warning that its meaning changes in `pg` 9, so name `verify-full` and the behavior stays put. If your provider signs its certificates with a CA of its own, pass the CA to the pool and leave `sslmode` out of the URL, since a `sslmode` in the URL replaces the pool's `ssl` option:

```ts title="lib/helpdesk.ts"
new pg.Pool({
  connectionString: process.env.HELPDESK_DATABASE_URL,
  ssl: { ca: process.env.HELPDESK_DATABASE_CA },
});
```

## Keep the pool alive [#keep-the-pool-alive]

On Postgres, use the pool from [Build the helpdesk](/docs/installation#build-the-helpdesk): one pool kept on `globalThis`, with `error` listeners on the pool and on each connection. Without the listeners, the first restart or failover of your database ends a connection with an error nobody handles, and Node.js stops your app.

## Set the production URLs [#set-the-production-urls]

`adminUrl` must be the URL your team opens in production, such as `https://app.example.com/helpdesk/`. The handler accepts changes only from its origin, so a value left at `localhost` refuses every reply with a 403. Set it per environment, and give each preview deployment its own if your team uses the agent UI there.

If the widget runs on another origin, such as a marketing site, list that origin on the inbox in `allowedOrigins`, exactly as the browser sends it, with no trailing slash. [Add the widget](/docs/widget/add#on-another-origin) has the details.

## Tell the helpdesk the client's address [#tell-the-helpdesk-the-clients-address]

Visitors who are not signed in share a budget of writes per hour per client address ([`anonymousRateLimit`](/docs/reference/configuration#rate-limits), 20 by default). The address comes from a header, and which header is right depends on what sits in front of your app.

By default the helpdesk takes the last address in `X-Forwarded-For`, which is the one your own proxy appended, then `X-Real-IP`. That is right behind exactly one proxy that appends to `X-Forwarded-For`. In other setups it goes wrong in one of three ways:

* Behind two proxies, such as a CDN in front of a load balancer, the last address is the CDN's. Every visitor who comes through the same CDN server shares one budget, and real people start getting 429s.
* With no proxy, `next start` fills in `X-Forwarded-For` with the connecting address only when the request has none. A client that sends its own can put a different address in it on every request and never reach the limit.
* With neither header, which happens on a server that sets neither, every visitor counts as `unknown` and they all share one budget.

When your platform names the client in a header of its own, read that header with `clientIp`:

<Tabs items="['Cloudflare', 'Vercel']">
  <Tab value="Cloudflare">
    Cloudflare sends `CF-Connecting-IP`, the address of the client that connected to Cloudflare:

    ```ts title="lib/helpdesk.ts"
    export const helpdesk = buildHelpdesk({
      // ...
      clientIp: request => request.headers.get('cf-connecting-ip'),
    });
    ```

    The header is only as trustworthy as the route to your server. If your origin also accepts requests that do not come through Cloudflare, anyone can send it with any value, so let only Cloudflare reach the origin.
  </Tab>

  <Tab value="Vercel">
    On Vercel, `x-real-ip` is the public address of the client. Vercel overwrites `X-Forwarded-For` to prevent spoofing, and `x-real-ip` carries the same address:

    ```ts title="lib/helpdesk.ts"
    export const helpdesk = buildHelpdesk({
      // ...
      clientIp: request => request.headers.get('x-real-ip'),
    });
    ```
  </Tab>
</Tabs>

When `clientIp` returns `null`, the request counts as `unknown`. [Security model](/docs/operations/security#rate-limits) lists every limit.

## Set the secrets [#set-the-secrets]

Each secret switches on one feature, so set only the ones you use, and keep them in your platform's secret store, never in the repository. Without `inboundWebhookSecret` or `jobsSecret`, their route refuses everyone; without `identityTokenSecret`, the helpdesk ignores identity tokens.

```ts title="lib/helpdesk.ts"
export const helpdesk = buildHelpdesk({
  // ...
  identityTokenSecret: process.env.HELPDESK_IDENTITY_SECRET,
  inboundWebhookSecret: process.env.HELPDESK_INBOUND_SECRET,
  jobsSecret: process.env.HELPDESK_JOBS_SECRET,
});
```

Generate each one with `openssl rand -base64 32`. Here is every variable a production install reads:

| Variable                   | Read by                                     | What it is                                                                                                                                                                                |
| -------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HELPDESK_DATABASE_URL`    | The migration command and `lib/helpdesk.ts` | The connection string.                                                                                                                                                                    |
| `APP_DATABASE_URL`         | The migration command                       | Used when `HELPDESK_DATABASE_URL` is unset.                                                                                                                                               |
| `DATABASE_SSL`             | The migration and import commands           | Postgres only. `true` turns on TLS and checks the certificate; `no-verify` skips the check.                                                                                               |
| `HELPDESK_ADMIN_URL`       | `adminUrl`                                  | The URL your team opens the agent UI at.                                                                                                                                                  |
| `HELPDESK_IDENTITY_SECRET` | `identityTokenSecret`                       | Signs identity tokens for a widget on another origin. `buildHelpdesk` throws on a secret shorter than 32 bytes. See [Sign customers in from another origin](/docs/identity/other-origin). |
| `HELPDESK_INBOUND_SECRET`  | `inboundWebhookSecret`                      | The bearer token your mail relay sends to the inbound route. See [Receive email](/docs/email/receive).                                                                                    |
| `HELPDESK_JOBS_SECRET`     | `jobsSecret`                                | The bearer token your scheduler sends to the jobs route. See [Run jobs and retention](/docs/operations/jobs).                                                                             |

The migration command reads the first three names itself. The others are the names these guides use; your `lib/helpdesk.ts` passes them in, so you may call them what you like.

Rating links in the resolution email need no secret from you. The helpdesk generates their key on first use and keeps it in the `helpdesk.setting` table, so it travels with your database.

## Schedule the jobs [#schedule-the-jobs]

Emails, reminders, snoozes and retention all wait for `runJobs`. Without a schedule, customers never hear about a reply. Run it every minute or every few minutes, as [Run jobs and retention](/docs/operations/jobs) describes.

On Vercel's Hobby plan a cron job runs at most once a day, and a more frequent schedule fails the deploy. A daily run is too rare for email, so on Hobby use the jobs route with a scheduler of your own, or move to a plan with per-minute cron jobs.

## Keep support reachable when your product is down [#keep-support-reachable-when-your-product-is-down]

The helpdesk runs inside the app it is mounted in and shares its database, its sign-in and its fate. When that app is your product, an outage of the product is also an outage of support, at the moment customers need it most. Two things keep a channel open:

* **Mount the helpdesk in an app of its own** when your product's uptime is the thing customers write in about: a small Next.js app on separate hosting with its own database, as the marketing site at better-helpdesk.com does for itself. Your product then embeds the widget cross-origin with an [identity token](/docs/identity/other-origin), and your website carries the same widget on a `public` inbox for anyone who cannot sign in. Agents sign into that app through whatever `identify` you give it; if your usual identity provider can be part of the same outage, give the support app a sign-in that is not.
* **Give the mail relay somewhere to go.** The example [Cloudflare Email Worker](https://github.com/better-helpdesk/better-helpdesk/blob/main/relays/README.md) forwards each message to `FALLBACK_ADDRESS` when the inbound route does not answer, so mail waits in a mailbox instead of bouncing; the Postmark and Mailgun relays retry on their own for a while. When the app is back, the mail that waited still threads in by its message ids.

Point your status page at `GET {basePath}/health/`, which answers `{ ok: true }` once the database has answered one query and 503 otherwise, so the support channel is a component customers can see the state of. Name the channel that stays up in your support documentation before anyone needs it.

## Allow the helpdesk in your Content Security Policy [#allow-the-helpdesk-in-your-content-security-policy]

If your app sends a `Content-Security-Policy`, the helpdesk needs these sources in it:

* `style-src 'unsafe-inline'`: the widget puts a `<style>` element in its shadow root, and the agent UI renders one in the page. Neither takes a nonce.
* `connect-src` with the origin of your storage bucket's upload URL, if you [store attachments](/docs/inbox/attachments): the widget and the agent UI post files straight to it.
* `img-src blob:` and the origin of your storage bucket's download URL: the widget and the agent UI preview an attached image from a local `blob:` URL before it is sent, and both the widget and the agent UI show image attachments from the URL `presignDownload` returns. Add the origin of your agents' avatars too, if `identify` returns an `image`.

With a [captcha](/docs/widget/bot-protection#in-your-content-security-policy), the provider's origin goes in `script-src` and `frame-src` too.

On a page of another origin that loads the widget, that page's policy needs the handler's origin in `connect-src`, and, for the script tag, the origin of `widget.js` in `script-src`.

## Upgrading [#upgrading]

A minor version adds; a major version may change the API and the schema. For every upgrade:

1. Read the notes for each version between yours and the new one on the [GitHub releases page](https://github.com/better-helpdesk/better-helpdesk/releases). They list every pull request a release contains.
2. Update the package with `npm install better-helpdesk@<version>`.
3. Run `npx better-helpdesk-migrate` against production before the new version of your app is deployed, as in [Migrate before you deploy](#migrate-before-you-deploy).
4. If you serve `widget.js` from a copy in `public/`, make sure your build copies the new one.
