diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml
--- a/.github/workflows/update.yml
+++ b/.github/workflows/update.yml
@@ -28,9 +28,16 @@ jobs:
       - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
         with:
           components: rustfmt, clippy
+          targets: wasm32-unknown-unknown
       - run: cargo fmt --check
       # Default (no egui) and the egui feature both build and lint cleanly.
       - run: cargo clippy -p craft-update --all-targets --locked -- -D warnings
       - run: cargo clippy -p craft-update --all-targets --locked --features egui -- -D warnings
       - run: cargo test -p craft-update --locked -- --nocapture
       - run: cargo test -p craft-update --locked --features egui -- --nocapture
+      # Without `native`, release parsing, versions and platforms build for the web (ArtCraft
+      # Launcher's wasm32 core uses them).
+      - run: cargo clippy -p craft-update --all-targets --locked --no-default-features -- -D warnings
+      - run: cargo test -p craft-update --locked --no-default-features
+      - if: matrix.os == 'ubuntu-latest'
+        run: cargo clippy -p craft-update --locked --no-default-features --target wasm32-unknown-unknown -- -D warnings
diff --git a/crates/update/Cargo.toml b/crates/update/Cargo.toml
--- a/crates/update/Cargo.toml
+++ b/crates/update/Cargo.toml
@@ -11,8 +11,11 @@ repository.workspace = true
 workspace = true
 
 [features]
-default = []
-egui = ["dep:egui"]
+default = ["native"]
+# Downloads, installs and self-updates. Without it only the pure parts build (release parsing,
+# versions, platforms, the suite), which is what a wasm32 build can use.
+native = ["dep:getrandom", "dep:tempfile", "dep:sha2", "dep:ureq", "dep:flate2", "dep:tar", "dep:plist", "dep:winreg", "dep:dunce", "dep:zip", "dep:winsafe", "dep:verifysign"]
+egui = ["native", "dep:egui"]
 local-mirror = []
 
 [dependencies]
@@ -20,10 +23,10 @@ egui = { workspace = true, optional = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
 log = "0.4"
-getrandom = "0.3"
-tempfile = "3"
-sha2 = "0.10"
-ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "_ring", "gzip", "platform-verifier"] }
+getrandom = { version = "0.3", optional = true }
+tempfile = { version = "3", optional = true }
+sha2 = { version = "0.10", optional = true }
+ureq = { version = "3.4", optional = true, default-features = false, features = ["rustls-no-provider", "_ring", "gzip", "platform-verifier"] }
 chrono = { workspace = true }
 semver = { version = "1", features = ["serde"] }
 
@@ -31,15 +34,15 @@ semver = { version = "1", features = ["serde"] }
 flate2 = "1"
 
 [target.'cfg(unix)'.dependencies]
-flate2 = "1"
-tar = { version = "0.4", default-features = false }
+flate2 = { version = "1", optional = true }
+tar = { version = "0.4", optional = true, default-features = false }
 
 [target.'cfg(target_os = "macos")'.dependencies]
-plist = "1"
+plist = { version = "1", optional = true }
 
 [target.'cfg(target_os = "windows")'.dependencies]
-winreg = "0.55"
-dunce = "1"
-zip = { version = "9", default-features = false, features = ["deflate"] }
-winsafe = { version = "0.0.29", features = ["advapi", "kernel", "shell", "version"] }
-verifysign = "1"
+winreg = { version = "0.55", optional = true }
+dunce = { version = "1", optional = true }
+zip = { version = "9", optional = true, default-features = false, features = ["deflate"] }
+winsafe = { version = "0.0.29", optional = true, features = ["advapi", "kernel", "shell", "version"] }
+verifysign = { version = "1", optional = true }
diff --git a/crates/update/src/lib.rs b/crates/update/src/lib.rs
--- a/crates/update/src/lib.rs
+++ b/crates/update/src/lib.rs
@@ -1,26 +1,44 @@
 //! Release checking, downloading, installing and self-updating for the Craft App Suite and ArtCraft Launcher.
+//!
+//! The default `native` feature brings everything that downloads, installs or touches the
+//! system. Without it (`default-features = false`) only the pure parts build: release parsing,
+//! versions, platforms and asset choice, the suite's apps and install statuses. Those build for
+//! wasm32, which is how ArtCraft Launcher's web build shares them.
 
 #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
 
 pub mod app;
+#[cfg(feature = "native")]
 pub mod desktop_entry;
+#[cfg(feature = "native")]
 pub mod detect;
 pub mod distro;
+#[cfg(feature = "native")]
 mod folders;
+#[cfg(feature = "native")]
 pub mod http;
+#[cfg(feature = "native")]
 pub mod install;
+#[cfg(feature = "native")]
 pub mod manifest;
+#[cfg(feature = "native")]
 pub mod packages;
+#[cfg(feature = "native")]
 pub mod paths;
 pub mod platform;
-#[cfg(windows)]
+#[cfg(all(windows, feature = "native"))]
 pub mod portable;
+#[cfg(feature = "native")]
 pub mod process;
 pub mod protocol;
+#[cfg(feature = "native")]
 pub mod proxy;
+#[cfg(feature = "native")]
 pub mod queue;
 pub mod release;
+#[cfg(feature = "native")]
 pub mod self_update;
+#[cfg(feature = "native")]
 pub mod source;
 pub mod status;
 pub mod suite;
@@ -33,7 +51,9 @@ pub mod dialog;
 pub mod updater;
 
 pub use app::App;
+#[cfg(feature = "native")]
 pub use http::{Http, Stop};
+#[cfg(feature = "native")]
 pub use paths::Paths;
 pub use platform::{Arch, Os, PackageKind, Platform, pick_asset};
 pub use protocol::{InstallLocations, JobPhase, LatestRelease, Source};
@@ -47,6 +67,7 @@ pub use version::{Version, is_newer, parse as parse_version};
 pub use dialog::{StartupPrompt, StartupPromptResponse};
 
 /// Checks for an update against GitHub releases for the given app.
+#[cfg(feature = "native")]
 pub fn check_for_update(app: &App, http: &Http, current_version: &str) -> Result<Option<Release>, String> {
     let url = format!("https://api.github.com/repos/{}/releases?per_page=30", app.repo);
     match http.get_api(&url, None)? {
@@ -67,9 +88,10 @@ pub fn check_for_update(app: &App, http: &Http, current_version: &str) -> Result
 }
 
 /// Unix seconds now.
+#[cfg(feature = "native")]
 pub fn now() -> i64 {
     std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
 }
 
-#[cfg(test)]
+#[cfg(all(test, feature = "native"))]
 pub(crate) const TEST_AGENT: &str = "ArtCraft-Launcher-Test/0.0";
diff --git a/crates/update/src/release.rs b/crates/update/src/release.rs
--- a/crates/update/src/release.rs
+++ b/crates/update/src/release.rs
@@ -270,7 +270,7 @@ pub fn valid_file_name(name: &str) -> bool {
 
 /// `s` cut to at most `max` bytes (on a char boundary) with an ellipsis, for text from a
 /// release or the feed shown in the UI.
-pub(crate) fn truncate(s: &str, max: usize) -> String {
+pub fn truncate(s: &str, max: usize) -> String {
     if s.len() <= max {
         return s.to_string();
     }
