Pular para o conteúdo

Serving PWA Apps

Este conteúdo não está disponível em sua língua ainda.

(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.

Each account gets its own host on a DNS zone that is used for nothing else:

URLBehaviour
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’s AccountIndex (see AccountRoot) written in decimal. AccountIndex is a UInt64, so JSON shows it in hexadecimal: an account with "AccountIndex": "10" is served at pwa16.<domain>. Accounts created before AccountIndex existed 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.
[server]
port_rpc_admin_local
port_peer
port_pwa
[port_pwa]
port = 8088
ip = 127.0.0.1
protocol = pwa
secure_gateway = 127.0.0.1
domain = pwapp.example
# optional:
# custom_domains = 1
# resolver = 127.0.0.1:53
KeyRequiredMeaning
protocol = pwaYesMust be the port’s only protocol.
domainYesThe zone or zones, comma-separated. Must be valid DNS names. They are normalised to lower case with no trailing dot.
secure_gatewayYesThe 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_domainsNo0 (default) or 1. See Custom Domains.
resolverNoAn 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_*ForbiddenTLS must be terminated at the proxy.

The server refuses to start if:

  • pwa is combined with any other protocol;
  • admin, user, password or ssl_* is set on a pwa port;
  • domain or secure_gateway is missing;
  • secure_gateway covers every address;
  • domain or resolver is invalid;
  • custom_domains is not 0 or 1;
  • an account appears on both the allowlist and the blocklist.

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.

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.

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-pwa label 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 Domain field must equal the host or a parent of it (case-insensitive). Otherwise the response is 503.
  • 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 429 with Retry-After and 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).
  • Only GET and HEAD are 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 503 until it decays.

Headers on a served document:

Content-Type: text/html; charset=utf-8
Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups
X-Frame-Options: DENY
Cross-Origin-Resource-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Cache-Control: no-cache
ETag: "<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.

These are also the outcome names reported by the pwa_info admin method.

OutcomeHTTPMeaning
served200Document returned.
not_modified304ETag matched.
redirected301Bare-zone request redirected to the per-account host.
not_found404No such loader.
bad_target404Host and path do not name a document.
unknown_domain404No usable _xahau-pwa record.
bad_host400Invalid Host header.
no_forwarded_for400Missing X-Forwarded-For.
direct_connection403Not from a secure_gateway address.
blocked403Allowlist or blocklist.
upgrade_refused403WebSocket or other upgrade attempted.
bad_method405Not GET or HEAD.
dns_pending429Custom-domain lookup in flight. Retry.
domain_mismatch503The account’s Domain does not endorse the host.
dns_failed503Resolver failure.
throttled503Resource manager limit.
busy503Too many requests in flight.
unavailable503Internal lookup failure.

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.