Serving PWA Apps
Esta página aún no está disponible en tu idioma.
(Introduced in 2026.10.13-release+4119.)
With the PWALoader amendment, any account can publish a small HTML bootstrap page for a Progressive Web App on ledger, stored in an AppLoader ledger object. A node operator can opt in to serving these documents over HTTP by configuring a pwa port. Serving is a node feature: it is configured locally and is not amendment-gated.
URL Scheme
Section titled “URL Scheme”Each account gets its own host on a DNS zone that is used for nothing else:
| URL | Behaviour |
|---|---|
https://pwa<AccountIndex>.<domain>/<r-address> | Serves the document. |
https://<domain>/<r-address> | Returns 301 to the URL above. The bare zone never serves a document, because every page would then share one origin. |
https://<custom host>/ | Serves the document if custom_domains = 1 (see Custom Domains). |
<AccountIndex>is the account’sAccountIndex(see AccountRoot) written in decimal.AccountIndexis a UInt64, so JSON shows it in hexadecimal: an account with"AccountIndex": "10"is served atpwa16.<domain>. Accounts created beforeAccountIndexexisted have no host.- The path must be that account’s exact r-address. One trailing slash and a query string are ignored; anything else is
404. - Never set cookies at or above the zone, and never serve your own site from it. A wildcard DNS record and a wildcard certificate cover the zone.
Port Configuration
Section titled “Port Configuration”[server]port_rpc_admin_localport_peerport_pwa
[port_pwa]port = 8088ip = 127.0.0.1protocol = pwasecure_gateway = 127.0.0.1domain = pwapp.example# optional:# custom_domains = 1# resolver = 127.0.0.1:53| Key | Required | Meaning |
|---|---|---|
protocol = pwa | Yes | Must be the port’s only protocol. |
domain | Yes | The zone or zones, comma-separated. Must be valid DNS names. They are normalised to lower case with no trailing dot. |
secure_gateway | Yes | The reverse proxy address. It may not cover every address. Every request must come from it and carry X-Forwarded-For; the rightmost entry is taken as the client. The Forwarded header is ignored. |
custom_domains | No | 0 (default) or 1. See Custom Domains. |
resolver | No | An IPv4 address[:port] (default port 53) used for custom-domain TXT lookups. Defaults to the system resolver. xahaud does not validate DNSSEC, so a validating resolver on the same host is recommended. |
admin, user/password, ssl_* | Forbidden | TLS must be terminated at the proxy. |
The server refuses to start if:
pwais combined with any other protocol;admin,user,passwordorssl_*is set on apwaport;domainorsecure_gatewayis missing;secure_gatewaycovers every address;domainorresolveris invalid;custom_domainsis not0or1;- an account appears on both the allowlist and the blocklist.
Reverse Proxy
Section titled “Reverse Proxy”A reverse proxy in front of the port terminates TLS and must forward the Host header unchanged. Example with nginx:
server { listen 443 ssl; server_name pwapp.example ~^pwa[0-9]+\.pwapp\.example$; ssl_certificate /etc/ssl/wildcard.pwapp.example.pem; ssl_certificate_key /etc/ssl/wildcard.pwapp.example.key; location / { proxy_pass http://127.0.0.1:8088; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; }}Lookups run on at most two job-queue threads, below client RPC priority, and beyond a bounded backlog they are refused with 503. Put a cache in front of the port.
Allow and Block Lists
Section titled “Allow and Block Lists”These sections are global, apply to every pwa port, and take one r-address per line:
[pwa_allowlist]rAllowedAccount1...
[pwa_blocklist]rBlockedAccount1...- If the allowlist is non-empty, only accounts on it are served.
- Blocklisted accounts are never served (
403), regardless of the allowlist. - An account on both lists is a startup error.
Custom Domains
Section titled “Custom Domains”With custom_domains = 1, a domain owner can point their own name at your proxy:
app.example.org. CNAME pwa-proxy.your.example._xahau-pwa.app.example.org. TXT "rAbC..."- The TXT record lives on the
_xahau-pwalabel beneath the host. - Exactly one address must be present there. Other TXT strings are ignored, and two addresses count as none.
- Consent: the named account’s
Domainfield must equal the host or a parent of it (case-insensitive). Otherwise the response is503. - Only the root path serves the document.
- Caching: no usable record →
404, cached 15 minutes. Resolver failure →503, cached 60 seconds. A valid record is cached for its TTL, clamped to 60–3600 seconds. - On a cache miss the response is
429withRetry-Afterand a refreshing page, rather than waiting. Lookups run at most four at a time and one per host. - The proxy must accept, and hold certificates for, the hosts being served (for example, Caddy on-demand TLS).
Request Handling
Section titled “Request Handling”- Only
GETandHEADare accepted (other methods →405). Every response closes the connection. - Only validated ledger state is served.
- Clients are metered per IPv4 address or per IPv6 /64. A miss costs the same as a hit, and over-use returns
503until it decays.
Headers on a served document:
Content-Type: text/html; charset=utf-8Content-Security-Policy: sandbox allow-scripts allow-forms allow-popupsX-Frame-Options: DENYCross-Origin-Resource-Policy: same-originCross-Origin-Opener-Policy: same-originCache-Control: no-cacheETag: "<PreviousTxnID of the AppLoader object>"X-NetworkID: <network id>X-LedgerSeq: <validated ledger seq>X-LedgerHash: <validated ledger hash>X-PWASeq: <AppLoaderSequence>If-None-Match with the ETag (or *) returns 304. Some proxies and CDNs strip the ETag header from responses; conditional requests with the PreviousTxnID still work.
Status Codes
Section titled “Status Codes”These are also the outcome names reported by the pwa_info admin method.
| Outcome | HTTP | Meaning |
|---|---|---|
served | 200 | Document returned. |
not_modified | 304 | ETag matched. |
redirected | 301 | Bare-zone request redirected to the per-account host. |
not_found | 404 | No such loader. |
bad_target | 404 | Host and path do not name a document. |
unknown_domain | 404 | No usable _xahau-pwa record. |
bad_host | 400 | Invalid Host header. |
no_forwarded_for | 400 | Missing X-Forwarded-For. |
direct_connection | 403 | Not from a secure_gateway address. |
blocked | 403 | Allowlist or blocklist. |
upgrade_refused | 403 | WebSocket or other upgrade attempted. |
bad_method | 405 | Not GET or HEAD. |
dns_pending | 429 | Custom-domain lookup in flight. Retry. |
domain_mismatch | 503 | The account’s Domain does not endorse the host. |
dns_failed | 503 | Resolver failure. |
throttled | 503 | Resource manager limit. |
busy | 503 | Too many requests in flight. |
unavailable | 503 | Internal lookup failure. |
Verifying Served Documents
Section titled “Verifying Served Documents”Clients (for example, browser extensions) can confirm that a served document is the account’s current one, as of the validated ledger, by calling pwa_validate on a second node.