Skip to content
Website

Shared assets

Every ADI instance serves its own control panel — the wasm bundle, its JS glue, two stylesheets, fonts — straight off its own disk. Shared assets is one setting, off by default, that instead points a visiting browser at a CDN for those files: https://cdn.withadi.dev/monoapp/<version>/…. index.html itself never moves; the instance you’re talking to always serves that. Only what it points at changes.

cdn.withadi.dev is ADI’s own bucket, and the default CDN URL for every instance: release.yml publishes to it automatically on every tagged release, so choosing a CDN mode is normally all there is to do. For a fork, an air-gapped build, or to keep the panel’s asset requests off ADI’s infrastructure entirely, see Publishing your own bucket.

It shipped in Introduced in v1.13.

CDN assets must be published under the running instance’s version prefix. Otherwise, asset requests fall back to local files — see Fallback is unconditional.

The uplink. The bundle is big, and the connection is sometimes slow — worst when the browser isn’t on the same machine as ADI at all: a paired fleet node, a phone, a laptop reaching another machine over a poor connection. Every byte of that wasm bundle crosses the slow link today. A CDN edge is close to the browser instead of close to the machine running ADI.

The shared cache. Every instance on the same version resolves to the exact same URL. Run two or three ADI instances — a laptop and a couple of fleet nodes, say — and a browser that already loaded the bundle for instance A has it cached under that URL, so instances B and C reuse the cached bundle: same version, same URL, same cache entry. Only the very first load, anywhere, costs anything.

That second point is why the URL carries the version and nothing instance-specific — no hostname, no node ID, no per-install token — and why even the local app.adi panel uses the CDN under cdn-always, rather than being exempted as “already local.” Put anything per-instance into the URL, or let one instance route around the CDN, and the shared cache stops working while everything still looks fine — there’d be no error, just no benefit.

There are three modes, and the default is the first:

ModeWhat it does
local-alwaysNever ask the CDN. Every request gets this instance’s own copy — an instance that has never touched this setting behaves exactly as if shared assets didn’t exist.
cdn-when-remoteAsk the CDN only for a request that doesn’t look like it’s on this machine — a paired fleet node reached over n.adi, say. Loopback and this instance’s own local name get the local copy, since the CDN’s whole benefit is the uplink to a browser somewhere else.
cdn-alwaysAsk the CDN for every request, local or not. The original all-or-nothing switch.

cdn-when-remote decides from the request’s own Host, which is a guess and not a security boundary: a wrong guess costs one failed CDN round trip before the unconditional local fallback fires, never a wrong answer served to the wrong place.

Panel: Settings → Shared assets (/settings/shared-assets). The mode picker, what the selected mode buys and costs, plus the CDN base URL and the version prefix this instance would ask for — check that monoapp/<version>/ has actually been published before choosing a CDN mode, since nothing stops you pointing at a version that isn’t there (you just get the fallback, silently).

API: GET /api/settings/shared-assets returns { mode, base_url }; POST the same path with { "mode": "cdn-always" } to change it. The setting persists in ~/.adi/mono/shared-assets/settings.toml across restarts, and a store written before the three modes existed still reads correctly — enabled = true migrates to cdn-always, the only mode that boolean could have meant. Setting it has no effect on anything except how the next index.html this instance serves is rewritten — no build step, no restart required.

How the rewrite worksExpert area

Section titled “How the rewrite works”

adi-app serves a Trunk-built index.html. When the mode asks for the CDN on this request, it rewrites that shell in memory, in three places, before serving it:

  • The boot script’s module import (a static import … from '/foo.js', which can’t be wrapped in a try) becomes an async import() inside a try/catch.
  • Every modulepreload/preload hint naming a hashed bundle file gets repointed at the CDN, so the preload actually warms the thing that gets loaded.
  • Each stylesheet <link> gets its href repointed, plus crossorigin="anonymous" and an onerror handler.

Three things never move: index.html itself, the service worker registration (/sw.js — a cross-origin service worker registration is refused by the browser outright), and the manifest/icon links.

The rewritten boot script’s catch reissues the same import() against the instance’s own local paths — the exact ones it would have used under local-always — on any failure. Each stylesheet’s onerror does the same for its href. Offline, the bucket down, or a version that was simply never published: all three take the same path, back to the local bundle. Choosing a CDN mode cannot leave someone looking at a page that fails to load; at worst it costs one failed request per asset before the fallback fires.

The prefix is the same version scripts/version.sh resolves and adi-app compiles in at build time (the git tag, or the workspace version if there isn’t one), so an instance always asks for exactly the build it’s running. Under that prefix, Trunk’s own content-hashed filenames do the rest: a publish never overwrites a key some earlier build already uploaded, so an older instance can never be served a newer bundle it wasn’t built to run.

  • ADI_WEBAPP_DIST (the disk override for local dist/ development) wins outright — the rewrite only ever touches the embedded copy, so pointing at your own build gets you exactly your own build, CDN setting or not.
  • ADI_SHARED_ASSETS_BASE_URL overrides the base URL, for testing against a bucket that isn’t cdn.withadi.dev.

The stylesheet links keep their SRI integrity hashes from the original build, and gain crossorigin="anonymous". That combination is what makes the browser actually run the integrity check on a cross-origin fetch — without it, the hash is present but silently unused. The browser refuses to apply a stylesheet whose bytes don’t match, from a CDN or anywhere else.

Font URLs had to become relative Introduced in v1.13

Section titled “Font URLs had to become relative ”

crates/adi-ui/fonts/fonts.css used to reference fonts with root-absolute url("/fonts/…"). That’s fine when the stylesheet is same-origin, but once it’s fetched cross-origin from the CDN, a root-absolute URL resolves against the CDN’s own domain root — not the version prefix the stylesheet itself lives under. A later publish could then change fonts out from under an already published, supposedly immutable version. The fix made the URLs relative (url("fonts/…")), which resolves against the stylesheet’s own URL instead — the version prefix on the CDN, the dist root locally. publish.sh still mirrors fonts/ unversioned at the bucket root, but only as a compatibility shim for builds published before this fix, whose stylesheet bytes (and SRI hash) are already fixed and can’t be changed after the fact.

The trade-offEnterprise interest

Section titled “The trade-off”

ADI is self-hosted and local-first. This setting is the one place a panel load reaches a third party. Under either CDN mode, Cloudflare sees one request per asset per browser: an IP address, a version string, a timestamp. Nothing else — no panel data, no API traffic, no auth tokens. /api/ never goes near the CDN; only the static bundle does.

That’s still a request that, under local-always, never leaves the machine. It’s exactly why that is the default and the CDN is something you choose rather than default behavior: an organization can decide that trade-off for itself, rather than discover it in a network tab after the fact. If your threat model rules out any outbound request from the panel at all, leave it on local-always — nothing else in ADI depends on a CDN mode.

Publishing your own bucketExpert area

Section titled “Publishing your own bucket”

For cdn.withadi.dev itself, publishing isn’t something anyone runs by hand: release.yml’s webapp job runs publish.sh --no-build on every push of a v* tag, gated so a workflow_dispatch dry run builds and uploads nothing. This section is for standing up a different bucket — your own Cloudflare account, for a fork, an air-gapped build, or anyone who’d rather not have their panel’s asset requests reach ADI’s infrastructure.

apps/shared-assets/ has everything needed. Both scripts source scripts/config.sh for the bucket name and domain — that’s the one place to edit, not the scripts themselves. Set ADI_SHARED_ASSETS_BASE_URL to point a running instance at the result.

scripts/setup-cf.sh is idempotent — safe to re-run as a drift check. It:

  1. Creates the R2 bucket.
  2. Sets a CORS policy allowing GET/HEAD from any origin. This has to be permissive: an ADI panel’s origin is app.adi, a fleet node’s <service>.<node>.n.adi, or a bare loopback port during setup, and none of those identities may appear anywhere in the CDN URL — that’s the whole point of why it exists.
  3. Attaches a custom domain and lets Cloudflare write its own CNAME — only possible if the target zone lives on the same Cloudflare account as the token.

It needs a CLOUDFLARE_API_TOKEN (adi-mono secrets set CLOUDFLARE_API_TOKEN) scoped to Account → Workers R2 Storage → Edit, plus Zone → Zone → Read and Zone → DNS → Edit on the domain you’re attaching.

scripts/publish.sh, run from apps/shared-assets/, builds and uploads a release:

Terminal window
./scripts/publish.sh # trunk build --release, then publish
# publish target/webapp-dist-publish/ without rebuilding
./scripts/publish.sh --no-build

It uploads everything under that build except index.html to monoapp/<version>/, skipping any file already reachable at its published URL — safe to re-run, since Trunk’s content-hashed filenames mean identical content always lands on the same key. index.html is deliberately never published: it’s the one file every instance still serves itself.

  • Hive — the panel this setting affects (app.adi) is itself a hive service, and a fleet node reached over n.adi is exactly the slow-uplink case this setting is meant to help — and what cdn-when-remote singles out.