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

# Production deployment

> How istravex.com, api.istravex.com and rpc.istravex.com are deployed by GitHub Actions.

Production is split by what each part needs. The site is static, so it lives on Cloudflare's edge. The API and mock RPC are a small Node service, so they run on a VPS. Both are deployed by one GitHub Actions workflow after CI passes on `main`: merge, and a few minutes later everything is running that commit.

## What runs where

```mermaid theme={null}
flowchart LR
  browser([Browser]) -->|istravex.com| cf[Cloudflare<br/>Workers static assets]
  client([Indexer / curl]) -->|api.istravex.com<br/>rpc.istravex.com| nginx
  subgraph VPS [VPS, shared with other sites]
    nginx[host nginx<br/>TLS via certbot] -->|127.0.0.1:3480| api[istravex-api<br/>Node 24]
  end
  reader([Reader]) -->|docs.istravex.com| mintlify[Mintlify]
```

| Host | Served by | Deployed by |
| - | - | - |
| `istravex.com`, `www.istravex.com` | Cloudflare Workers static assets (`wrangler.jsonc`) | `wrangler deploy` in the Deploy workflow |
| `api.istravex.com` | Host nginx → `istravex-api` container on the VPS | Deploy workflow, over SSH |
| `rpc.istravex.com` | Same container; nginx rewrites the path to `/rpc/…` | Deploy workflow, over SSH |
| `docs.istravex.com` | Mintlify, from `docs/` | Mintlify's GitHub app, on push |

The DNS zone is on Cloudflare. The site's records are created by Cloudflare when the Worker's custom domains are attached; `api` and `rpc` are created by the workflow (`deploy/dns.mjs`) as **DNS-only** records, so nginx on the VPS terminates TLS itself and sees real client addresses for rate limiting.

## The site on Cloudflare

`wrangler.jsonc` deploys `dist/` as static assets with no Worker script:

* `not_found_handling: single-page-application`, so every path serves the app;
* custom domains `istravex.com` and `www.istravex.com`, with `workers.dev` and preview URLs off;
* `public/_headers` (copied into `dist/`) sets HSTS, `nosniff`, a referrer policy, `X-Frame-Options: DENY`, and a one-year immutable cache on Vite's fingerprinted `/assets/*`. Everything else revalidates on every request.

CI runs `wrangler deploy --dry-run` on every push, so a broken config fails before merge.

## The VPS is shared

The VPS also runs other, unrelated sites behind its own nginx, so the deploy is careful to stay inside its own footprint:

| Touches | Never touches |
| - | - |
| `/opt/istravex` | Other directories under `/opt` |
| One container in the Compose project `istravex`, on `127.0.0.1:3480` | Other containers, networks or volumes; image cleanup is filtered to this repository's label |
| One nginx site, `istravex-api.conf` | Other sites' configs; new server blocks reuse the existing `:80`/`:443` sockets, so no socket options change |
| One certbot certificate, `istravex-api` | Other certificates or certbot settings |

Every nginx change is checked with `nginx -t` before a graceful `systemctl reload nginx`; if the check fails, the previous `istravex-api.conf` is restored and nothing is reloaded.

## The pipeline

`.github/workflows/deploy.yml` runs when CI completes successfully on `main`, or manually from the Actions tab. Its three jobs run in parallel where they can.

**1. Site → Cloudflare**

`npm run build`, validate with `wrangler deploy --dry-run`, then `wrangler deploy --message <commit>`. The first deploy attaches the custom domains, replacing any placeholder records at those names. A smoke test fetches `https://istravex.com/`.

**2. API image → GHCR**

Built from the `Dockerfile` and pushed to `ghcr.io/hackcat29/istravex-api`, tagged with the commit SHA and `latest`.

**3. API → VPS**

Upserts the `api` and `rpc` DNS records, then over SSH (strict host-key checking against a pinned `known_hosts`) copies `deploy/` to `/opt/istravex` and runs `deploy/remote.sh`. That script logs in to GHCR with the workflow's short-lived token, pulls the image for this commit, and runs `docker compose up -d --wait`, which only returns once the API's health check passes. It then installs the nginx site. On the first deploy it serves the ACME challenge over HTTP, gets a certificate from certbot (webroot, renewed by the host's certbot timer with a reload hook), and switches to HTTPS. A smoke test then requires `https://api.istravex.com/v1/health` to report the commit just deployed, and the RPC to report healthy.

Deploys are serialised (`concurrency: deploy-production`), so two merges in quick succession deploy in order. Each part is skipped, not failed, until it's configured.

## One-time setup

**1. Cloudflare API token**

In the Cloudflare dashboard, **My Profile → API Tokens → Create Token**, start from the **Edit Cloudflare Workers** template, add **Zone → DNS → Edit**, and limit it to your account and the `istravex.com` zone. Store it in GitHub:

```bash theme={null}
gh secret set CLOUDFLARE_API_TOKEN --repo hackcat29/istravex
gh variable set CLOUDFLARE_ACCOUNT_ID --repo hackcat29/istravex --body <account id>
```

`npx wrangler whoami` prints the account ID.

**2. VPS access**

Generate a key just for deploys, authorise it on the server, and pin the host keys:

```bash theme={null}
ssh-keygen -t ed25519 -N '' -C istravex-deploy -f istravex-deploy
ssh-copy-id -i istravex-deploy.pub root@<VPS IPv4>
ssh-keyscan -t ed25519,ecdsa,rsa <VPS IPv4> > known_hosts
```

Compare the scanned fingerprints (`ssh-keygen -lf known_hosts`) with the server's own (`ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`, from the provider's console) before trusting them.

**3. Repository secrets and variables**

| Kind | Name | Value |
| - | - | - |
| Secret | `VPS_HOST` | VPS IPv4 |
| Secret | `VPS_USER` | `root`, or a sudo-capable user |
| Secret | `VPS_SSH_KEY` | The private key `istravex-deploy` |
| Secret | `VPS_KNOWN_HOSTS` | Contents of `known_hosts` |
| Variable | `VPS_IPV6` | VPS IPv6 (optional; adds AAAA records) |
| Variable | `DEPLOY_ENABLED` | `true` |

Then delete the private key from your machine.

**4. Server prerequisites**

Docker with the Compose plugin, nginx, certbot (with an ACME account registered), and ports 80 and 443 open. The deploy doesn't install or reconfigure any of these, since other sites depend on them.

**5. Docs domain**

See [Docs](https://docs.istravex.com/operations/docs.md#publishing). In Cloudflare, keep the `docs` CNAME **DNS-only**: Mintlify issues its own certificate.

## Operating it

**Site.** `npx wrangler deployments list` shows recent deploys; `npx wrangler rollback` returns to the previous one instantly.

**API.**

```bash theme={null}
ssh root@<VPS IPv4>
cd /opt/istravex
docker compose ps                 # status and health
docker compose logs -f api        # JSON request logs
nginx -t && tail -f /var/log/nginx/error.log   # proxy
certbot certificates --cert-name istravex-api  # TLS
```

Roll back by re-running the Deploy workflow on an earlier commit, or on the server:

```bash theme={null}
echo "TAG=<earlier sha>" > .env && docker compose up -d --wait
```

**Resources.** The API holds corpus metadata in memory and decompresses at most four ledgers at a time; it runs comfortably in under 200 MB, and the container is capped at 512 MB so it can't crowd out the server's other services. Container logs rotate at 5 × 10 MB.


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