clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

dependabot/github_actions/actions-9b61906d8b
bindws / docs / 11-hosting-bindws.md
11-hosting-bindws.md 11.1K rendered
1---2title: Hosting bind.ws3audience: developer4---56# Hosting bind.ws78How to run your own copy on your own domain. You need a Cloudflare account with Workers Paid, a zone for the domain and `wrangler` logged in. To run it on your own machines instead, with no Cloudflare in the path, see [Hosting without Cloudflare](16-hosting-without-cloudflare.md).910## Configure1112Edit `wrangler.jsonc`:1314| Setting | Meaning |15|---|---|16| `DOMAIN` | your domain, such as `relays.example` |17| `DEV_RELAY` | the name used when a request has no subdomain |18| `LIGHTNING_ADDRESS` | where zaps go; leave empty to run without top-ups |19| `SERVICE_PUBKEY` | the nostr pubkey zap requests must name |20| `FREE_*` | monthly allowances per relay |21| `SATS_PER_*` | prices past the allowances |22| `LEASE_DAYS` | how long a temporary relay from `POST /lease` lives; 14 if unset |23| `ratelimits` | how many leases a minute, per address and overall; the defaults are 5 and 60 |2425The prices carry a comment with the Cloudflare rates they were set from. `npm run margin` compares them with those rates at the current bitcoin price, and a weekly workflow does the same; see [Costs and margins](15-costs-and-margins.md).2627Point `routes` at your zone: the apex as a custom domain and `*.<domain>/*` as a zone route. Create the R2 bucket named in `r2_buckets`.2829## DNS3031The wildcard route needs proxied placeholder records so Cloudflare answers for every subdomain:3233| Record | Name | Value | Proxy |34|---|---|---|---|35| `AAAA` | `*` | `100::` | on |36| `A` | `*` | `192.0.2.1` | on |3738Universal SSL covers one level of wildcard, which is all the names use.3940## Service key4142Zaps are addressed to a nostr pubkey. Generate a keypair, put the pubkey in `SERVICE_PUBKEY` and keep the secret out of the repo. Import the secret into the wallet behind `LIGHTNING_ADDRESS` so the wallet signs receipts as that key. The wallet's LNURL document must report `allowsNostr: true` and a `nostrPubkey`.4344## Deploy4546```47npm install48npm run deploy49```5051Nothing else is required: no origin, no certificates, no servers. Hibernation avoids idle duration charges; retained storage, alarm work and provider operations still follow Cloudflare's rates. See [Costs and margins](15-costs-and-margins.md) for the tenant meters and host overhead.5253## Presets5455The presets the console offers are the files in `relay-templates/`, one relay configuration per template with a `template` block for its title and blurb. Add, edit or remove files there, run `npm run build:templates`, and deploy. `npm run typecheck` checks every file against the schema and the relay's own parser, and fails while the generated module is stale. The schema is served at `https://<domain>/relay-config.schema.json`, for editors and for owners who keep their relays as files ([Scripts and agents](13-scripts-and-agents.md#the-configuration-file)).5657## Operate5859- `wrangler tail` streams logs, including receipt validation.60- Deploys roll to objects within seconds. Live websockets survive when the object hibernates, and so does a mid-flight negentropy sync: the next message rebuilds the session from the store.61- The `usage` table grows one row per relay per month. No cleanup is needed.62- To take a relay down as the operator, use the owner's typed-name `deleterelay` call or the console.6364## Observe6566Nothing runs between requests, so there is no process to scrape. What the platform offers instead is logs and traces, exported to any OpenTelemetry endpoint, and a metrics store queried with SQL.6768**The meter line.** Every relay writes one JSON line to the console each time it flushes its usage counters: on a socket closing, on the daily alarm, and whenever the counters pass a threshold in between. The line is `{ "msg": "meter", "relay": <name>, "bytesIn", "bytesOut", "rowsRead", "rowsWritten", "activeMs", "accepted", "refused", "reqs", "connections" }`. All but the last are deltas since the previous line; a destination sums them per relay. `accepted` and `refused` count events answered with an OK, `reqs` counts REQ and COUNT messages, `activeMs` is the awake time fuel meters. `wrangler tail` shows the lines live, and the Workers Logs page keeps them for the plan's retention. A second kind of line, `{ "msg": "audit", "relay", "at", "actor", "action", "target", "detail" }`, is written whenever a management method or a moderation event changes something; it is the same row the relay keeps in its moderation log.6970**Logs and traces elsewhere.** In the dashboard, Workers Observability, add a destination of type Logs and one of type Traces with the provider's OTLP endpoint and its auth header. Then name them in `wrangler.jsonc`:7172```73"observability": {74 "enabled": true,75 "logs": { "enabled": true, "destinations": ["my-logs"] },76 "traces": { "enabled": true, "head_sampling_rate": 0.1, "destinations": ["my-traces"] }77}78```7980Traces cover the Worker's own fetches and binding calls, the Durable Object invocations included; there are no custom spans yet. `persist: false` on either sends it out without keeping a copy in the dashboard. Export needs Workers Paid; Cloudflare bills it per event from October 2026, ten million a month included. Metrics cannot be exported this way yet, which is why the meter line exists: a Grafana or Honeycomb query over it gives per-relay rates.8182**Analytics Engine.** For a metrics store on Cloudflare itself, bind a dataset and every flush also writes one data point:8384```85"analytics_engine_datasets": [{ "binding": "METRICS", "dataset": "bindws_relays" }]86```8788The point's index is the relay's name, `blob1` is `meter`, and `double1` to `double9` are the meter line's numbers in the same order. Query it with the SQL API or the Grafana plugin, for example `SELECT index1 AS relay, sum(double6) AS accepted FROM bindws_relays WHERE timestamp > now() - interval '1' day GROUP BY relay`. Ten million points a month are included on Workers Paid; without the binding nothing is written.8990## Custom domains9192Owners can put a relay under a hostname they control, such as `relay.example.com`, through [Cloudflare for SaaS custom hostnames](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/start/getting-started/). The feature is off until the one-time setup below is done; the console says so until then.9394How it works, from the [docs](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings/worker-as-origin/): the customer's hostname CNAMEs to a name in your zone, so their traffic enters your zone, and a Worker route that matches any host sends it to the Worker. The Worker sees the customer's hostname in `Host`, looks it up in the `HOSTS` KV namespace to find the relay name, and forwards to that object exactly as it does for `<name>.<domain>`. Inside the object nothing changes; the URLs it prints already follow the request host. Certificates use [HTTP validation](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/http/), which Cloudflare completes on its own once the CNAME resolves to you, so the owner has one DNS record to create. [Hostname validation](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/) happens the same way; the optional TXT record the console also shows lets an owner activate the hostname before switching DNS.9596One-time setup, in this order:97981. **Enable Cloudflare for SaaS** on the zone: dashboard, your zone, SSL/TLS, Custom Hostnames, Enable. Non-enterprise zones enter payment details; the first 100 hostnames are free ([enable](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/start/enable/), [plans](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/plans/)).992. **Create the fallback origin record**, originless and proxied, in the zone's DNS ([worker as origin](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/start/advanced-settings/worker-as-origin/)):100101 | Record | Name | Value | Proxy |102 |---|---|---|---|103 | `AAAA` | `fallback` | `100::` | on |104105 Then on the Custom Hostnames page set **Fallback Origin** to `fallback.<domain>` and wait for it to read Active. The wildcard placeholder records from the DNS section above are not enough here: the fallback origin must be a record of its own.1063. **Create the CNAME target** owners will point at, proxied ([getting started, step 2](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/start/getting-started/#2-optional-create-cname-target)):107108 | Record | Name | Value | Proxy |109 |---|---|---|---|110 | `CNAME` | `customers` | `fallback.<domain>` | on |111112 `CNAME_TARGET` in `wrangler.jsonc` must match: `customers.<domain>`.1134. **Routes.** Only now can the zone take a catch-all route, which is what makes custom hostname traffic reach the Worker; before Cloudflare for SaaS is enabled the API refuses it with code 10022. Uncomment the `*/*` route in `wrangler.jsonc` and deploy, then confirm in Workers Routes that the zone lists `*/*` for `bindws-relay`. If wrangler still refuses the pattern, add it by hand on that page.1145. **KV namespace.** `npx wrangler kv namespace create HOSTS` and put the id in both `kv_namespaces` entries of `wrangler.jsonc` (the repo carries the bind.ws one).1156. **API token.** My Profile, API Tokens, Create Token, Custom token: permission **Zone, SSL and Certificates, Edit**, zone resources limited to your zone. Store it as a secret: `npx wrangler secret put CF_API_TOKEN`. `ZONE_ID` in `wrangler.jsonc` is the zone the hostnames are created in (the same id the routes use). Without the secret the console reports custom domains as not enabled and the methods answer `unsupported:`.116117The Worker calls three endpoints with that token: [create](https://developers.cloudflare.com/api/resources/custom_hostnames/methods/create/), [get](https://developers.cloudflare.com/api/resources/custom_hostnames/methods/get/) and [delete](https://developers.cloudflare.com/api/resources/custom_hostnames/methods/delete/) under `/zones/<zone>/custom_hostnames`. A hostname is ready when the API reports `status: active` and `ssl.status: active` ([check when ready](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/start/getting-started/#check-when-a-custom-hostname-is-ready)). Deleting a relay removes its hostnames from Cloudflare and KV; transferring keeps them. Do not let an owner add the zone's own apex as a custom hostname; the relay refuses anything under `<domain>`.118119Locally, `wrangler dev` has no custom hostname traffic; the KV binding exists so tests can seed a mapping.120121## Local development122123```124npm run dev125```126127`wrangler dev --env dev` serves `http://<name>.localhost:8787` as the relay named `<name>`, `http://<domain>.localhost:8787` as the apex and anything else as `DEV_RELAY`. The dev environment has no routes, so hostnames pass through.128129To use the console without a browser extension, run `npm run dev:signer` and paste the snippet from that file into the devtools console before clicking sign in. `npm run dev:seed <name>` publishes sample events. `npm run dev` folds the console's files into their generated module first; the templates need `npm run build:templates` after a change.