title: Hosting bind.ws audience: developer
Hosting bind.ws
How 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.
Configure
Edit wrangler.jsonc:
| Setting | Meaning |
|---|---|
DOMAIN |
your domain, such as relays.example |
DEV_RELAY |
the name used when a request has no subdomain |
LIGHTNING_ADDRESS |
where zaps go; leave empty to run without top-ups |
SERVICE_PUBKEY |
the nostr pubkey zap requests must name |
FREE_* |
monthly allowances per relay |
SATS_PER_* |
prices past the allowances |
LEASE_DAYS |
how long a temporary relay from POST / lives; 14 if unset |
ratelimits |
how many leases a minute, per address and overall; the defaults are 5 and 60 |
The 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.
Point routes at your zone: the apex as a custom domain and *.<domain>/ as a zone route. Create the R2 bucket named in r2_buckets.
DNS
The wildcard route needs proxied placeholder records so Cloudflare answers for every subdomain:
| Record | Name | Value | Proxy |
|---|---|---|---|
AAAA |
* |
100:: |
on |
A |
* |
192.0.2.1 |
on |
Universal SSL covers one level of wildcard, which is all the names use.
Service key
Zaps 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.
Deploy
npm install
npm run deploy
Nothing 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 for the tenant meters and host overhead.
Presets
The 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:/, for editors and for owners who keep their relays as files (Scripts and agents).
Operate
wrangler tailstreams logs, including receipt validation.- 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.
- The
usagetable grows one row per relay per month. No cleanup is needed. - To take a relay down as the operator, use the owner's typed-name
deleterelaycall or the console.
Observe
Nothing 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.
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.
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:
"observability": {
"enabled": true,
"logs": { "enabled": true, "destinations": ["my-logs"] },
"traces": { "enabled": true, "head_sampling_rate": 0.1, "destinations": ["my-traces"] }
}
Traces 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.
Analytics Engine. For a metrics store on Cloudflare itself, bind a dataset and every flush also writes one data point:
"analytics_engine_datasets": [{ "binding": "METRICS", "dataset": "bindws_relays" }]
The 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.
Custom domains
Owners can put a relay under a hostname they control, such as relay.example.com, through Cloudflare for SaaS custom hostnames. The feature is off until the one-time setup below is done; the console says so until then.
How it works, from the docs: 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, which Cloudflare completes on its own once the CNAME resolves to you, so the owner has one DNS record to create. Hostname validation happens the same way; the optional TXT record the console also shows lets an owner activate the hostname before switching DNS.
One-time setup, in this order:
-
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, plans).
-
Create the fallback origin record, originless and proxied, in the zone's DNS (worker as origin):
Record Name Value Proxy AAAAfallback100::on 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. -
Create the CNAME target owners will point at, proxied (getting started, step 2):
Record Name Value Proxy CNAMEcustomersfallback.<domain>on CNAME_TARGETinwrangler.jsoncmust match:customers.<domain>. -
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.jsoncand 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. -
KV namespace.
npx wrangler kv namespace create HOSTSand put the id in bothkv_namespacesentries ofwrangler.jsonc(the repo carries the bind.ws one). -
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_IDinwrangler.jsoncis 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 answerunsupported:.
The Worker calls three endpoints with that token: create, get and delete under /. A hostname is ready when the API reports status: active and ssl.status: active (check when 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>.
Locally, wrangler dev has no custom hostname traffic; the KV binding exists so tests can seed a mapping.
Local development
npm run dev
wrangler dev --env dev serves http:/ as the relay named <name>, http:/ as the apex and anything else as DEV_RELAY. The dev environment has no routes, so hostnames pass through.
To 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.