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.
PreviewPreview
Section titled “Preview”CDN assets must be published under the running instance’s version prefix. Otherwise, asset requests fall back to local files — see Fallback is unconditional.
Why it exists
Section titled “Why it exists”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.
Turning it on
Section titled “Turning it on”There are three modes, and the default is the first:
| Mode | What it does |
|---|---|
local-always | Never 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-remote | Ask 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-always | Ask 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 atry) becomes an asyncimport()inside atry/catch. - Every
modulepreload/preloadhint naming a hashed bundle file gets repointed at the CDN, so the preload actually warms the thing that gets loaded. - Each stylesheet
<link>gets itshrefrepointed, pluscrossorigin="anonymous"and anonerrorhandler.
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.
Fallback is unconditional
Section titled “Fallback is unconditional”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.
Versioning and cache-busting
Section titled “Versioning and cache-busting”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.
Escape hatches
Section titled “Escape hatches”ADI_WEBAPP_DIST(the disk override for localdist/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_URLoverrides the base URL, for testing against a bucket that isn’tcdn.withadi.dev.
Stylesheet integrity
Section titled “Stylesheet integrity”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:
- Creates the R2 bucket.
- Sets a CORS policy allowing
GET/HEADfrom any origin. This has to be permissive: an ADI panel’s origin isapp.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. - 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:
./scripts/publish.sh # trunk build --release, then publish# publish target/webapp-dist-publish/ without rebuilding./scripts/publish.sh --no-buildIt 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.
See also
Section titled “See also”- Hive — the panel this setting affects (
app.adi) is itself a hive service, and a fleet node reached overn.adiis exactly the slow-uplink case this setting is meant to help — and whatcdn-when-remotesingles out.