Brewser Docs
Switch Runtime

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:// and https://. DNS resolution works, using the console's configured DNS.
  • Your local network. LAN addresses (http://192.168.x.x, a .local device, 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.50 won't connect. For LAN devices, use http://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-Origin headers, 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 call http:// 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 byNeeds allowed_origins?
fetch, XMLHttpRequest, WebSocketYes
<video>, <audio>, <source> (including HLS)No
<img>, CSS background-imageNo
<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": true

The 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 fetch at the device over http://<lan-ip> (not https:// to an IP; see above).
  • No CORS headers required on the device.
  • Poll or open a WebSocket for 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.speedtest runs a Cloudflare speed test over external HTTPS.
  • com.natureglass.streamcast pulls a live HLS video stream and third-party APIs.
  • com.natureglass.speedwatch talks to public map/geocoding APIs (this one targets mobile/web rather than the Switch, but shows the pattern).

Browse them at play.brewser.io.

On this page