Networking in Apps
What fetch, XHR and WebSocket can actually reach
Brewser apps can talk to the network with the standard tools (fetch, XMLHttpRequest, and
WebSocket), and they reach further than a browser tab does, because the runtime isn't bound by
the same-origin rules a browser enforces.
What you can reach
- The public internet, over both
http://andhttps://. DNS resolution works, using the console's configured DNS. - Your local network. LAN addresses (
http://192.168.x.x, a.localdevice, a Raspberry Pi dashboard) are reachable, which makes Brewser useful as a hardware or IoT dashboard. - HLS and other network media streams, handled by the media pipeline, from any host (see Media is exempt below). See Audio & Media.
XMLHttpRequest is a polyfill over the same fetch machinery, so it reaches the same places (a
synchronous XHR GET reads local files instead). WebSocket (ws:// and wss://) works too:
if you want players or screens talking to each other, Brewser hosts a room service you can
connect to directly. See Multiplayer and the
WebSockets Quick Start.
Gotchas
HTTPS to a raw IP address fails. Certificate validation matches on hostname, and a bare IP has no matching certificate, so
https://192.168.1.50won't connect. For LAN devices, usehttp://192.168.1.50(plain HTTP to an IP works fine).https://to a real hostname works normally.
- No CORS. Brewser has no browser "origin," so cross-origin requests work. You can call
an API that doesn't send
Access-Control-Allow-Originheaders, something a browser tab can't do. Useful for talking to devices and services that were never CORS-configured. - Mixed content is allowed. An
https://page can callhttp://endpoints; nothing is blocked or upgraded.
These make Brewser a friendlier network client than a browser, but they're also why you should be deliberate: declare exactly what you talk to.
Publishing: declare your hosts
A published app that uses the network needs the network capability and every external host it
fetches from listed in its manifest's allowed_origins. The
security scanner compares the hosts your code contacts against
that list and flags anything undeclared, so keep it accurate, and use literal URLs (assembled or
obfuscated URLs get flagged). Apps that make no network calls declare nothing, so there's nothing
to check.
Media is exempt
allowed_origins governs programmatic requests — fetch, XMLHttpRequest and WebSocket.
It does not apply to media and image element loads:
| Loaded by | Needs allowed_origins? |
|---|---|
fetch, XMLHttpRequest, WebSocket | Yes |
<video>, <audio>, <source> (including HLS) | No |
<img>, CSS background-image | No |
<script>, <link>, <iframe> | Yes |
These loads still need the network (or local_network) capability — an app that declares no
network capability loads no remote media — and local_network is still LAN-only for them. The
exemption skips the host list, nothing else.
This exists because a media app's byte sources are the one thing it genuinely cannot know ahead
of time. An HLS variant resolves to a different CDN edge by region and by hour; an IPTV index
points at broadcaster hosts in 200+ countries; a self-hosted server's address is typed by the
user. The old rule pushed those apps into declaring an empty allowed_origins, which
switched the check off for their API calls too. Now they can pin the part that is knowable:
{
"permissions": ["network"],
"allowed_origins": ["https://gql.twitch.tv", "https://usher.ttvnw.net"]
}Streams and artwork then play from wherever those APIs point, while anything the app fetches stays held to the list.
Note the boundary is the API that issues the request, not whether the bytes are "a stream".
If you fetch an HLS master playlist with fetch() to pick a variant yourself — as a Twitch
client does — that fetch is a fetch, and its host must be declared. Only the URL you hand to the
<video> element is exempt.
When the user picks the server
If the address comes from the person using the app rather than from you, set
user_origins in the manifest. Brewser asks once, on the console,
before the app reaches a host outside allowed_origins, and remembers the answer:
"permissions": ["local_network", "storage"],
"allowed_origins": [],
"user_origins": trueThe prompt is drawn by the shell, not by your page, and offers Allow / Just once / Deny. Media
and image loads never raise it, *.brewser.io never raises it, and a redirect to an unapproved
host is refused rather than re-asked.
The Home-Assistant / LAN-dashboard pattern
A common Brewser use case is a couch dashboard for something on your network: Home Assistant, a Pi-hole, an ESP32 exposing a web endpoint, a NAS. The recipe:
- Point
fetchat the device overhttp://<lan-ip>(nothttps://to an IP; see above). - No CORS headers required on the device.
- Poll or open a
WebSocketfor live updates (a device's own socket, or a Brewser room if several screens share state).
// Poll a Home Assistant entity over the LAN.
const res = await fetch('http://192.168.1.20:8123/api/states/sensor.temperature', {
headers: { Authorization: 'Bearer ' + token },
});
const { state } = await res.json();Examples
com.natureglass.speedtestruns a Cloudflare speed test over external HTTPS.com.natureglass.streamcastpulls a live HLS video stream and third-party APIs.com.natureglass.speedwatchtalks to public map/geocoding APIs (this one targets mobile/web rather than the Switch, but shows the pattern).
Browse them at play.brewser.io.
Related
- Multiplayer: real-time rooms, presence and shared state over
wss://ws.brewser.io. - Manifest Reference:
allowed_origins. - Security Review: how declared hosts are checked.
- Web Platform Support: the standards Brewser implements.
- Network Behaviour: what the shell itself connects to.

Brewser Docs