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

# Self-hosting

> Run the keeper, the indexer and the web app on one box behind a reverse proxy.

<Frame className="wr-art">
  <img src="https://mintcdn.com/windrose/G2X0jK9uSBPQAS-b/images/art/operate.webp?fit=max&auto=format&n=G2X0jK9uSBPQAS-b&q=85&s=be82958075c5a1457b785996f8292533" alt="A paper windmill on a walnut post" width="1600" height="686" data-path="images/art/operate.webp" />
</Frame>

Windrose is three long-running processes plus a reverse proxy. Everything defaults to Robinhood testnet (46630); the same steps work for any chain in `packages/abis/chains.json` once the contracts are deployed there and `packages/abis/deployments/<chainId>.json` exists (see [Deploy contracts](/operate/deploy-contracts) and [Add a chain](/operate/add-a-chain)).

| Process | Package            | Port  | Needs                                                                                    |
| ------- | ------------------ | ----- | ---------------------------------------------------------------------------------------- |
| Keeper  | `packages/keeper`  | none  | the keeper private key (allow-listed on `KeeperFxOracle`), an RPC URL                    |
| Indexer | `packages/indexer` | 42069 | an RPC that allows topic-filtered `eth_getLogs` and has archive state, optional Postgres |
| Web     | `apps/web`         | 3000  | the public URL of each indexer at build time                                             |

Requirements on the box: node 22, pnpm 9 (via corepack), pm2, git. About 1.5 GB of RAM headroom is enough for a testnet; each indexer sits around 600 to 700 MB during backfill, so a box running three chains wants a swapfile (the production box has 3.8 GB of RAM and a 2 GB swapfile).

## Choosing the chain

`packages/abis/chains.json` is the chain registry: one entry per chain with its RPCs and websocket, explorer and Blockscout verifier URL, gas parameters, the dollar launches settle in, EURC, Permit2, Multicall3, Pyth, the official Uniswap v4 addresses, a faucet and the indexer's `eth_getLogs` range. `@launchpad/abis` exports it as `CHAINS` and `getChain(id)`, `scripts/chain-env.sh` prints it for shell scripts, and `contracts/script/Deploy.s.sol` mirrors the addresses in its `_defaults` table. Every process selects an entry by chain id:

| Process                                                                     | Variable                                                                                                          | Default                 | Overrides read next to it                                                                                                                                                                                               |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Web (`apps/web`)                                                            | `NEXT_PUBLIC_CHAIN_ID`, the *default* chain only; the switcher offers every registry chain with a deployment file | 46630                   | `NEXT_PUBLIC_RPC_URL_<chainId>`, `NEXT_PUBLIC_INDEXER_URL_<chainId>`; the unsuffixed `NEXT_PUBLIC_RPC_URL`, `NEXT_PUBLIC_INDEXER_URL`, `NEXT_PUBLIC_USDC` and `NEXT_PUBLIC_USDC_SYMBOL` apply to the default chain only |
| Keeper (`packages/keeper`)                                                  | `CHAIN_ID`                                                                                                        | 46630                   | `RPC_URL`, `DEPLOYMENTS_FILE`                                                                                                                                                                                           |
| Indexer (`packages/indexer`)                                                | `CHAIN_ID`                                                                                                        | 46630                   | `PONDER_RPC_URL_<chainId>`, `PONDER_WS_URL_<chainId>`, `START_BLOCK_<chainId>`, `PORT`, `PGLITE_DIR`, `ETH_GETLOGS_BLOCK_RANGE`, `INDEX_POOL_SWAPS`                                                                     |
| Generator scripts (`pnpm abis:logos`, `pnpm abis:tokenlist`)                | `CHAIN_ID` or `--chain`                                                                                           | 5042002                 | `ARC_RPC_URL` or `RPC_URL`                                                                                                                                                                                              |
| `scripts/deploy-testnet.sh`, `scripts/seed-testnet.sh`, `scripts/verify.sh` | `CHAIN_ID`                                                                                                        | 5042002 (verify: 46630) | `RPC`                                                                                                                                                                                                                   |
| `scripts/deploy-mainnet.sh`                                                 | `CHAIN_ID`                                                                                                        | 4663                    | `RPC`                                                                                                                                                                                                                   |

An id that is not in the registry fails at startup with the list of registered chains. Registered today: Arc testnet 5042002 (`arc-testnet`), Arc 5042 (`arc`, not deployed), Robinhood testnet 46630 (`robinhood-testnet`), Robinhood Chain 4663 (`robinhood`).

### One keeper and one indexer per chain

The web app serves every chain at once, so each chain the switcher offers needs its own keeper (rates) and its own indexer (history). Both run from the same packages with a different `CHAIN_ID`; `deploy/ecosystem.config.cjs` starts all of them:

| Chain                   | Keeper                       | Indexer                                                       | Web variable                                                        |
| ----------------------- | ---------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------- |
| Robinhood Chain 4663    | `windrose-keeper`            | `windrose-indexer`, port 42069, schema `windrose`             | `NEXT_PUBLIC_INDEXER_URL_4663=https://api.windrose.market`          |
| Robinhood testnet 46630 | `windrose-keeper-rh-testnet` | `windrose-indexer-rh-testnet`, port 42071, schema `launchpad` | `NEXT_PUBLIC_INDEXER_URL_46630=https://api.windrose.market/testnet` |
| Arc testnet 5042002     | `windrose-keeper-arc`        | `windrose-indexer-arc`, port 42070, schema `launchpad_arc`    | `NEXT_PUBLIC_INDEXER_URL_5042002=https://api-arc.windrose.market`   |

The indexers share `packages/indexer/.env.local` (the RPC and start-block variables carry the chain id) and, without `DATABASE_URL`, each keeps its own PGlite database in `.ponder/pglite-<chainId>`. The keepers share `packages/keeper/.env` plus one `.env.<chainId>` per chain for the key. See [Indexer](/operate/indexer) and [Keeper](/operate/keeper).

<Note>
  `docs/HOSTING.md` in the repository still describes `windrose-keeper` and `windrose-indexer` as the Robinhood testnet pair on port 42069. `deploy/ecosystem.config.cjs` is the current state: that pair is Robinhood Chain mainnet, and the testnet pair carries the `-rh-testnet` suffix on port 42071.
</Note>

## First run

```bash theme={"system"}
bash scripts/vps-setup.sh                 # installs pnpm/pm2 if missing, pnpm install, builds web, starts pm2
WITH_FOUNDRY=1 bash scripts/vps-setup.sh  # also installs forge/cast for contract operations from the box
```

The script copies the three env files from their examples when they are missing, so check them first:

<AccordionGroup>
  <Accordion title="packages/keeper/.env">
    `KEEPER_PRIVATE_KEY`, `CHAIN_ID`, optionally `RPC_URL` (default: the registry `rpc` of `CHAIN_ID`). The keeper must be allow-listed by the oracle owner (`setKeeper`). Rates are fresh for 2 hours and the keeper re-posts hourly, so this process must stay up or minting and backing stop. Gas parameters come from the registry.
  </Accordion>

  <Accordion title="packages/indexer/.env.local">
    `CHAIN_ID`, `PONDER_RPC_URL_<chainId>` (an endpoint with archive state that tolerates topic-filtered `eth_getLogs`; see the per-chain table in [Indexer](/operate/indexer)), `START_BLOCK_<chainId>` (the deployment block), `DATABASE_SCHEMA`. `ETH_GETLOGS_BLOCK_RANGE` defaults to the registry value. For durability add `DATABASE_URL=postgresql://...`; without it Ponder uses an embedded PGlite database under `packages/indexer/.ponder`.
  </Accordion>

  <Accordion title="apps/web/.env.local">
    `NEXT_PUBLIC_CHAIN_ID` (the default chain), an RPC per chain (`NEXT_PUBLIC_RPC_URL_<chainId>`) and an indexer URL per chain (`NEXT_PUBLIC_INDEXER_URL_<chainId>=https://api.<your-domain>`). The browser calls the indexer URLs, so they must be public; rebuild the web app after changing any of them.
  </Accordion>
</AccordionGroup>

pm2 runs the packages' JS entry files (`node_modules/next/dist/bin/next`, `node_modules/tsx/dist/cli.mjs`, `node_modules/ponder/dist/esm/bin/ponder.js`), not the `node_modules/.bin/*` shims: pnpm writes those as shell scripts, which pm2 would try to run with node.

## Domain and reverse proxy

One hostname for the app plus one per indexer. Caddy issues TLS certificates automatically:

```text theme={"system"}
launch.example.com {
    reverse_proxy 127.0.0.1:3000
}
api.launch.example.com {          # mainnet indexer
    reverse_proxy 127.0.0.1:42069
}
api-arc.launch.example.com {      # Arc testnet indexer
    reverse_proxy 127.0.0.1:42070
}
```

The production box uses nginx and certbot instead: one site file with `server` blocks on port 80 that proxy the app hostname (and `www`) to `127.0.0.1:3000`, `api.` to `127.0.0.1:42069` (with a `location /testnet/` that proxies to `127.0.0.1:42071` for the Robinhood testnet indexer) and `api-arc.` to `127.0.0.1:42070`, each serving `/.well-known/acme-challenge/` from `/var/www/html` for certbot's HTTP challenge and passing the usual `X-Forwarded-*` and `Upgrade` headers. `nginx -t && systemctl reload nginx` after edits. The indexers already answer CORS, so the browser can query `api.` from the app's origin without extra rules.

DNS: an `A` record per hostname (`@`, `www`, `api`, `api-arc`) pointing at the box, optional `AAAA` records, and behind Cloudflare keep them "DNS only" until the certificate exists. `deploy/certbot-when-dns.sh` runs from root's crontab every 5 minutes, waits until every hostname resolves to the box's public IPv4, then runs `certbot --nginx --redirect` for all of them (which also rewrites the port-80 blocks to redirect to HTTPS) and removes its own cron line. Renewal is certbot's systemd timer.

<Info>
  This documentation site is not on the box. It is built by Mintlify from `apps/docs` and served at `docs.windrose.market` through a `CNAME` record, with the TXT verification records the Mintlify dashboard shows. The app links to it through `NEXT_PUBLIC_DOCS_URL`.
</Info>

Then set the public indexer URLs in `apps/web/.env.local`, rebuild (`pnpm --filter @launchpad/web build`) and `pm2 restart windrose-web`. Regenerate the token list with the real origin so wallets can fetch logos: `TOKEN_LOGO_BASE=https://launch.example.com/tokens/ CHAIN_ID=<id> pnpm abis:tokenlist`.

## Day to day

```bash theme={"system"}
pm2 status                                  # every keeper, indexer and the web app should be "online"
pm2 logs windrose-keeper                    # "run complete: posted N" every 10 minutes
curl -s localhost:42069/status              # indexed block should track the chain head
curl -s localhost:42070/status              # Arc testnet indexer (0.5 s blocks)
pm2 restart windrose-indexer windrose-indexer-arc   # after changing packages/indexer/.env.local
```

Updating: `git pull`, `pnpm install --frozen-lockfile`, `pnpm --filter @launchpad/web build`, `pm2 restart all`. If the web build gets killed for lack of memory, `pm2 stop windrose-indexer-arc` first, build, then start it again; if the box swaps hard, stop the Arc pair (`windrose-indexer-arc`, `windrose-keeper-arc`) while Arc is listed as "soon" on the public site.

## Contract operations from the box

Install Foundry (`WITH_FOUNDRY=1 bash scripts/vps-setup.sh`), put a testnet deployer key in `contracts/.env`, then follow [Deploy contracts](/operate/deploy-contracts). The testnet deployer and keeper keys hold only faucet funds; mainnet is signed by an encrypted keystore on the workstation, and the box's mainnet keeper key only ever holds gas. Never commit any `.env` file.
