# Deploying support-chat

One container serves the API, the agent dashboard and the customer widget:

```
https://your-app.com/          -> redirects to /desk/
https://your-app.com/desk/     -> agent dashboard
https://your-app.com/chat/     -> customer widget
https://your-app.com/chat/embed.js
https://your-app.com/api/…     -> REST
https://your-app.com/socket.io -> WebSocket
https://your-app.com/health    -> health check
```

One service instead of three: cheaper, and the dashboard is same-origin with
its own API so there is no CORS to configure for it.

---

## Fastest path: Render, one signup

`render.yaml` provisions the app, PostgreSQL **and** Redis together, so you do
not need Neon or Upstash at all:

1. Push this repo to GitHub
2. Render → **New → Blueprint** → pick the repo
3. It prompts for `SEED_ADMIN_EMAIL` and `SEED_ADMIN_PASSWORD` — choose a strong password
4. Deploy

On first boot the app creates its own schema (`AUTO_MIGRATE=true`) and your
admin login. No shell, no migration command, and **no default password ever
exists**. Afterwards set `PUBLIC_URL` to the URL Render gives you, and
`CORS_ORIGINS` to any site embedding the widget.

If your Render account rejects `type: keyvalue`, change it to `type: redis` —
the service was renamed and older accounts still use the old name.

---

## Alternative: bring your own databases

## 1. Databases (free tiers, ~5 minutes)

| | Provider | Copy this |
|---|---|---|
| Postgres | [neon.tech](https://neon.tech) | the **direct** connection string, not the pooled one |
| Redis | [upstash.com](https://upstash.com) | the `rediss://` URL |

TLS is automatic: `sslmode=require` in the Postgres URL and the `rediss://`
scheme both switch it on without extra flags.

## 2. Generate secrets

Run twice — never reuse the development values:

```bash
openssl rand -hex 32
```

## 3. Environment variables

Set these on the platform. Everything else has a working default.

| Variable | Value |
|---|---|
| `DATABASE_URL` | Neon direct string |
| `REDIS_URL` | Upstash `rediss://` URL |
| `JWT_SECRET` | 32-byte random hex |
| `FILE_SIGNING_SECRET` | a *different* 32-byte random hex |
| `PUBLIC_URL` | `https://your-app.com` — signed file links are built from this |
| `CORS_ORIGINS` | sites that embed the widget, comma-separated |
| `NODE_ENV` | `production` |
| `TRUST_PROXY` | `true` (all four platforms terminate TLS in front of you) |

`PORT` is injected by the platform; leave it unset.

`CORS_ORIGINS` only governs **other sites embedding the widget** — the dashboard
is same-origin. If nothing embeds it yet, set it to your own URL.

## 4. Deploy

**Render** — commit `render.yaml`, then *New → Blueprint*. The `starter` plan
stays awake; `free` sleeps after ~15 minutes, which makes the first customer of
the day wait for a cold start.

**Railway** — *New Project → Deploy from GitHub*. It picks up `railway.json`
and builds the Dockerfile.

**Fly.io** — edit `app` and `primary_region` in `fly.toml`, then:

```bash
fly launch --no-deploy
fly secrets set DATABASE_URL=… REDIS_URL=… JWT_SECRET=… FILE_SIGNING_SECRET=… PUBLIC_URL=…
fly deploy
```

**Koyeb** — see `deploy/koyeb.md`.

## 5. Create the schema

Once, against the production database. From your machine:

The app can do this itself. Set these and it creates the schema and your admin
account on first boot:

```
AUTO_MIGRATE=true
SEED_ADMIN_EMAIL=you@yourdomain.com
SEED_ADMIN_PASSWORD=<a strong password you choose>
```

Re-running is safe, and the password is never written to the logs.

> Do **not** run `npm run db:seed` against production. It creates demo accounts
> whose passwords are published in this repo, so it refuses to run when
> `NODE_ENV=production`.

## 6. Embed the widget

```html
<script
  src="https://your-app.com/chat/embed.js"
  data-widget-url="https://your-app.com/chat"
  data-position="right"
  data-color="#2049d6"
  defer></script>
```

Add each embedding site's origin to `CORS_ORIGINS`.

---

## Two things that will bite you

**Uploaded files vanish on redeploy.** Container filesystems are ephemeral. Set
`STORAGE_DRIVER=s3` with an S3/R2 bucket (see README §9), or attach a persistent
volume. Local disk is fine only for a demo.

**Screen sharing needs TURN.** Only STUN is configured, so roughly 10–20% of
users — those behind symmetric NAT — cannot connect. Add a TURN server to
`ICE_SERVERS` in `web/src/hooks/useScreenShare.ts` and
`widget/src/lib/useScreenShare.ts`. Hosted TURN is cheaper than running coturn.

## Scaling later

The Socket.IO Redis adapter is already wired, so running more than one instance
works — an emit on one reaches sockets on another. Enable sticky sessions on the
load balancer when you do.
