Skip to content

Upgrade to Tauri 3.0 Alpha

This guide walks you through upgrading your Tauri 2.0 app to the Tauri 3.0 alpha.

The headline change in Tauri 3.0 is that the webview runtime is no longer selected by Cargo features of the tauri crate. Your app now depends on a runtime crate directly and selects it when building the app:

  • tauri-runtime-wry uses the system webview (webkit2gtk on Linux, WebView2 on Windows, and WKWebView on macOS, iOS, and Android). This is what every Tauri 2.0 app uses.
  • tauri-runtime-cef uses the Chromium Embedded Framework, new in 3.0, which ships Chromium with your app.

Read the Webview Runtime guide for the whole picture. The rest of this page lists what changes for an existing app.

Tauri 3.0 requires Rust 1.95 or newer. The Rust crates are published as 3.0.0-alpha.x and the npm packages under the next tag:

npm install @tauri-apps/cli@next @tauri-apps/api@next

Then update the Rust dependencies and add the runtime crate:

src-tauri/Cargo.toml
[build-dependencies]
tauri-build = { version = "2", features = [] }
tauri-build = { version = "3.0.0-alpha.0", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "3.0.0-alpha.0", features = [] }
tauri-runtime-wry = "3.0.0-alpha.0"

The official plugins are published as 3.0.0-alpha.x as well, and their npm packages under the next tag. A 2.x plugin depends on tauri 2 and cannot be registered on a 3.0 app, so bump every tauri-plugin-* crate and @tauri-apps/plugin-* package your app depends on. For example, for the dialog plugin:

src-tauri/Cargo.toml
[dependencies]
tauri-plugin-dialog = "2"
tauri-plugin-dialog = "3.0.0-alpha.0"
npm install @tauri-apps/plugin-dialog@next

The plugin APIs are unchanged from 2.x. The 3.0 alpha of each plugin only updates its tauri dependency and adopts the new autogenerated permission layout, so no code changes are needed on your side. Community plugins must be updated by their authors before they build with Tauri 3.0.

  • The wry and cef Cargo features of the tauri crate were removed, along with the x11, dbus, and macos-proxy features it forwarded to wry. Depend on tauri-runtime-wry (or tauri-runtime-cef) instead, and enable such features on that crate.
  • The runtime is selected with the new tauri::Builder::runtime method, which takes the attributes type of a runtime crate (tauri_runtime_wry::Wry or tauri_runtime_cef::Cef). Building the app without selecting a runtime fails with tauri_runtime::Error::RuntimeNotConfigured. Migration
  • tauri::Builder::default() now uses the type-erased tauri::DynRuntime, which is also the default runtime type of AppHandle, App, Window, Webview, WebviewWindow, and the other generic types. Code that names tauri::Wry as the runtime type must be updated. Migration
  • The runtime_init_attrs builder method was merged into runtime.
  • The tauri::Wry and tauri::WryHandle re-exports were removed. The types live in tauri-runtime-wry, where the Wry<T> runtime type was renamed to WryRuntime<T>, and Wry is now the attributes type that selects it.
  • The tauri::tao and tauri::wry re-exports were removed. Use tauri_runtime_wry::{tao, wry}. Migration
  • Runtime-specific APIs moved from the tauri crate to extension traits in the runtime crates. Migration
  • tauri::webview_version() was removed, since the version depends on the runtime in use. Use App::webview_version or AppHandle::webview_version. Migration
  • The tauri_macros::default_runtime attribute macro was removed. The generic types of tauri default to tauri::DynRuntime directly.
  • devtools, macos-private-api, and unstable must now be enabled on the runtime crate (tauri-runtime-wry or tauri-runtime-cef), which also enables them on tauri. Enabling them on tauri alone no longer enables them on the runtime. Migration
  • tauri gained the gtk3 and gtk4 features, which select the Linux GTK bindings. The runtime crates enable the right one for you: tauri-runtime-wry enables gtk3 and tauri-runtime-cef enables gtk4. Migration
  • tauri gained the linux-libappindicator feature to keep using libappindicator for the tray icon on Linux. Migration
  • The test feature no longer implies GTK 3, so it can be combined with gtk4.
  • The tray icon uses the ksni backend (StatusNotifierItem over D-Bus) by default instead of libappindicator, dropping the libayatana-appindicator system dependency. Migration
  • Window::gtk_window, Window::default_vbox, WindowBuilder::transient_for_raw, and the menu integration are gated behind the gtk3 and gtk4 features. They fail with the new tauri::Error::GtkVersionMismatch under a runtime whose GTK version is not the one selected at compile time. Migration

The custom scheme URL format (tauri://localhost or http://tauri.localhost) is now defined by the runtime instead of the platform, and the convertFileSrc JavaScript API takes the format from the runtime. The wry runtime keeps using http://tauri.localhost on Windows and Android and tauri://localhost elsewhere, so nothing changes for the frontend of an existing app.

tauri::test::MockRuntime uses tauri://localhost on every platform. Tests that sent IPC requests from http://tauri.localhost on Windows and Android must use tauri://localhost instead. Migration

tauri-build no longer copies the configured bundle > resources to the Cargo target directory. On desktop, unbundled apps (tauri dev and cargo run) resolve resources at runtime from their source paths instead, so editing a resource file no longer triggers a full rebuild of the app:

  • When all configured resources are plain relative paths (for example "assets/*"), the resource directory resolves to the directory containing tauri.conf.json, and files are read directly from their sources. The running app picks up changes live.
  • When resources are remapped (map notation, ../, or absolute paths), the bundle layout is mirrored next to the executable the first time the resource directory is accessed in each run.

The bundle > resources configuration is now part of the config embedded by generate_context!, where it was previously stripped.

  • The autogenerated allow-$command and deny-$command permissions are no longer materialized as two permission files per command. The plugin and app manifests store a commands list instead, and the permissions are resolved on demand, which reduces the size of the ACL embedded in the app. Nothing changes in your capability files.
  • The app manifest gained implicit allow-* and deny-* permissions that allow or deny all of the app’s own commands (the ones listed with tauri_build::AppManifest::commands) through a single resolved entry, so a capability no longer needs to list every command individually.
  • Plugin and app build scripts write the autogenerated command permissions to OUT_DIR instead of the crate’s permissions/autogenerated folder. Only the human-readable permissions/autogenerated/reference.md documentation is still written to the plugin crate. Migration

The tauri::android_binding! macro moved to tauri_runtime_wry::android_binding!, and #[tauri::mobile_entry_point] expands to it, so Android apps must depend on tauri-runtime-wry. The dependency update above already covers this. tauri::handle_android_plugin_response and tauri::send_channel_data are exposed on Android so that other runtimes can implement their own binding.

  • tauri-build detects the CEF runtime through the app’s tauri-runtime-cef dependency (the DEP_TAURI_RUNTIME_CEF_RUNTIME environment variable it exports to the app’s build script) instead of the removed cef feature of tauri.
  • The Tauri CLI detects the webview runtime from the tauri-runtime-wry and tauri-runtime-cef dependencies in the app manifest. The webkit2gtk dependencies of the Debian and RPM packages and the WebView2 installation step of the Windows installers are only added when the app uses wry. The CEF files, code signing entitlements, and macOS dev flow are only used when the app uses CEF.
  • tauri_bundler::BundleSettings::cef_path and BundleSettings::cef_shared_runtime were replaced by BundleSettings::webview_runtime, a WebviewRuntime enum with Wry, Cef { distribution, helper }, and Other variants. This only affects programmatic users of the bundler.
  • tauri-build supports Cargo’s build-dir layout (the default since Rust 1.100) when staging external binaries and frameworks.
  • Added tauri_build::try_build_context and ContextAttributes for packages that expand tauri::generate_context! once and share the context with the rest of the workspace. It runs only what the context expansion consumes, and skips app artifact staging and executable-specific build configuration, which stay with the package that owns the binary.

These changes to the tauri-runtime crate only matter if you implement your own Runtime:

  • RuntimeSpecificInitAttrs was renamed to RuntimeInitAttrs. The trait is now generic over the user event type and has a type Runtime: Runtime<T, RuntimeInitAttrs = Self> associated type, so the attributes alone identify the runtime. The implementation for () was removed, so every runtime must define its own attributes type and implement From<Self> for tauri_runtime::dynamic::DynRuntimeInitAttrs, so that its attributes can be passed to the type-erased builder.
  • Runtime::WindowOpener and window::WindowBuilderBase now require 'static, so they can be type-erased.
  • Runtime::custom_scheme_url moved to RuntimeHandle::custom_scheme_url(&self, scheme, https).
  • Added the required RuntimeHandle::webview_version method.
  • Added the required WebviewDispatch::with_ios_webview method on iOS. It gives access to the platform webview, plugin manager, and view controller pointers through the new webview::IosWebviewHandle.
  • WebviewDispatch::open_devtools, close_devtools, and is_devtools_open are required regardless of the devtools feature, so that the type-erased runtime can forward them. Keep the implementation behind the feature and make it a no-op without it.
  • The GTK types crossing the runtime boundary are version-agnostic raw pointers, so a runtime can use a GTK version different from the one tauri was built with. WindowDispatch::gtk_window and default_vbox return *mut c_void (transfer full), WindowBuilder::transient_for takes a *mut c_void (transfer full), and RawWindow::gtk_window and default_vbox are *mut c_void (transfer none). tauri-runtime no longer depends on the gtk crate, and the new tauri_runtime::gtk module carries the GTK version a runtime binds to (gtk::declare_version).
  • The Chromium Embedded Framework runtime ships a consistent Chromium version with your app on Windows, macOS, and Linux.
  • The type-erased tauri::DynRuntime lets apps, plugins, and libraries handle AppHandle, Window, and Webview values without naming the runtime type. See Webview Runtime.
  • tauri::webview::PlatformWebview::downcast_ref reaches the runtime’s webview type from Webview::with_webview, whatever the runtime generic in use.
  • The macOS bundler supports Liquid Glass icons. Add an Icon Composer .icon directory (compiled with actool from Xcode 26 or newer) or a prebuilt Assets.car file to bundle > icon.
  • The CLI respects the CARGO_BUILD_TARGET environment variable when resolving the build target, matching Cargo’s precedence over build.target in .cargo/config.toml.
  • When a captured command fails, the CLI and bundler include its stderr in the error message.

Add the runtime crate to your dependencies and pass its attributes to tauri::Builder::runtime:

src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.runtime(tauri_runtime_wry::Wry::default())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

tauri_runtime_wry::Wry is a unit-like struct. wry has no runtime-wide options, so Wry::default() is all it takes. The CEF runtime’s tauri_runtime_cef::Cef is configured through builder methods instead. See the CEF guide.

Building the app without a runtime fails with tauri_runtime::Error::RuntimeNotConfigured.

Enable devtools, macos-private-api, and unstable on the runtime crate. They enable the same feature on tauri, so you do not need to list them twice:

src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2", features = ["devtools", "macos-private-api"] }
tauri = "3.0.0-alpha.0"
tauri-runtime-wry = { version = "3.0.0-alpha.0", features = ["devtools", "macos-private-api"] }

The same applies to the wry-specific x11, dbus, macos-proxy, and tracing features, which are now features of tauri-runtime-wry only. x11 and dbus are enabled by default.

tauri::Builder::default() uses the type-erased tauri::DynRuntime, and so do the default generic parameters of AppHandle<R>, Window<R>, Webview<R>, and the other types. Code that spelled out tauri::Wry must either drop the type, since the default now works with any runtime, or name the concrete runtime type, tauri_runtime_wry::WryRuntime:

fn setup(app: &tauri::AppHandle<tauri::Wry>) {}
fn setup(app: &tauri::AppHandle) {}

If you prefer static dispatch, with no trait objects between tauri and the runtime, name the runtime type on the builder. Its runtime method then only accepts the attributes of that runtime:

tauri::Builder::<tauri_runtime_wry::WryRuntime>::new()
.runtime(tauri_runtime_wry::Wry::default())
.run(tauri::generate_context!())
.expect("error while running tauri application");

In this case the generic types must name the runtime everywhere (tauri::AppHandle<tauri_runtime_wry::WryRuntime>), as they did in 2.0 for any runtime other than tauri::Wry. Note that tauri_runtime_wry::Wry<T> was renamed to WryRuntime<T>. Wry is now the attributes type that selects the runtime.

Generic code that takes R: tauri::Runtime keeps working with both models, which is what plugins and libraries should keep doing.

The APIs that only make sense on one runtime moved from the tauri crate to extension traits in the runtime crates, which now depend on tauri. The traits are implemented both for the concrete runtime type and for tauri::DynRuntime, and they return tauri_runtime::Error::RuntimeTypeMismatch when the app runs on a different runtime:

Tauri 2.0 Tauri 3.0
tauri::tao, tauri::wry tauri_runtime_wry::tao, tauri_runtime_wry::wry
AppHandle::create_tao_window, AppHandle::send_tao_window_event tauri_runtime_wry::AppHandleWryExt
App::wry_plugin tauri_runtime_wry::AppWryExt (also on AppHandle through AppHandleWryExt)
Webview::with_webview with the wry webview type tauri_runtime_wry::WebviewWryExt::with_wry_webview or PlatformWebview::downcast_ref
WebviewWindowBuilder::with_environment, with_related_view, with_webview_configuration tauri_runtime_wry::WebviewWindowBuilderWryExt (and WebviewBuilderWryExt with the unstable feature)

The CEF runtime follows the same pattern with tauri_runtime_cef::{WebviewCefExt, WebviewWindowBuilderCefExt, WebviewBuilderCefExt}. See the CEF guide.

Import the trait and the call sites stay the same:

src-tauri/src/lib.rs
use tauri_runtime_wry::AppHandleWryExt;
fn create_window(app: &tauri::AppHandle) -> tauri::Result<()> {
app.create_tao_window(|| ("my window".into(), tauri_runtime_wry::tao::window::WindowBuilder::new()))?;
Ok(())
}

The wry WebviewAttribute enum was replaced by the WryWebviewAttributes struct, with the environment (Windows), related_view (Linux), and webview_configuration (macOS) fields. The NewWindowResponse::Create documentation now points to the WebviewWindowBuilderWryExt methods to link the new webview to its opener.

Webview::with_webview hands out a PlatformWebview that dereferences to the webview type of the runtime generic in use. With the default tauri::DynRuntime, use PlatformWebview::downcast_ref to reach the runtime’s webview type:

src-tauri/src/lib.rs
use tauri::Manager;
fn zoom(app: &tauri::AppHandle) -> tauri::Result<()> {
app.get_webview_window("main").unwrap().with_webview(|webview| {
let Some(webview) = webview.downcast_ref::<tauri_runtime_wry::Webview>() else {
return;
};
#[cfg(target_os = "linux")]
{
use webkit2gtk::WebViewExt;
webview.inner().set_zoom_level(4.);
}
})
}

tauri_runtime_wry::WebviewWryExt::with_wry_webview does the downcast for you and fails with RuntimeTypeMismatch on another runtime. The CEF runtime offers tauri_runtime_cef::WebviewCefExt::with_cef_webview for its tauri_runtime_cef::Webview handle.

The webview version is now a method of the app instead of a free function:

let version = tauri::webview_version()?;
let version = app.webview_version()?;

The tauri crate no longer links GTK unconditionally on Linux. The gtk3 and gtk4 features select the bindings, and the runtime crates enable the right one: tauri-runtime-wry enables gtk3 and tauri-runtime-cef enables gtk4.

An app does not need to do anything. A plugin or library that depends on tauri alone and uses Window::gtk_window, Window::default_vbox, WindowBuilder::transient_for_raw, or the Linux menu integration must enable one of the features explicitly:

Cargo.toml
[target.'cfg(target_os = "linux")'.dependencies]
tauri = { version = "3.0.0-alpha.0", features = ["gtk3"] }
gtk = "0.18"

Enabling both features selects GTK 4. Cargo’s feature unification enables both whenever the dependency graph contains runtime crates that disagree on the GTK version, and such a binary can only run one of those runtimes, because GTK 3 and GTK 4 cannot be initialized in the same process. Under a runtime whose GTK version is not the selected one, the GTK APIs and the Linux menu integration fail with tauri::Error::GtkVersionMismatch instead of reinterpreting the runtime’s window objects.

The tray icon now uses the ksni backend by default, a pure D-Bus implementation of the StatusNotifierItem specification. libayatana-appindicator is no longer required at build time or at runtime, so you can drop it from your system dependencies, CI images, and Debian and RPM package dependencies.

To keep using libappindicator, enable the linux-libappindicator feature:

src-tauri/Cargo.toml
[dependencies]
tauri = { version = "3.0.0-alpha.0", features = ["tray-icon", "linux-libappindicator"] }

tauri::test::MockRuntime uses the tauri://localhost custom scheme URL on every platform. Tests that hardcoded the Windows or Android origin when sending IPC requests must use it too:

tauri::test::get_ipc_response(
&webview,
tauri::webview::InvokeRequest {
cmd: "ping".into(),
callback: tauri::ipc::CallbackFn(0),
error: tauri::ipc::CallbackFn(1),
url: "http://tauri.localhost".parse().unwrap(),
url: "tauri://localhost".parse().unwrap(),
body: tauri::ipc::InvokeBody::default(),
headers: Default::default(),
invoke_key: tauri::test::INVOKE_KEY.to_string(),
},
);

Plugin build scripts now write the autogenerated allow-$command and deny-$command permissions to OUT_DIR, and remove the permissions/autogenerated/commands folder that older versions wrote into the crate’s source tree. After the first build with Tauri 3.0, delete that folder from version control. Only permissions/autogenerated/reference.md remains in the crate.

If your build script merges permission files from several directories, the new collect_permission_files and define_permissions_from_files functions in tauri_utils::acl::build collect them into a single permission file list.


© 2026 Tauri Contributors. CC-BY / MIT