Deep Link Plugin
Set your Tauri app as the default handler for custom URL schemes and universal links on all platforms
Introduction
A deep link is a URL that opens your app directly instead of a web page. When a user taps myapp://settings or clicks https://yourdomain.com/open, the operating system launches your Tauri application and hands it that URL. The Deep Link plugin gives your app the ability to register those URL patterns and react when they arrive. The Deep Link Plugin introduction covers custom schemes versus verified HTTPS links.
This matters for any workflow that crosses between the browser and your native app: OAuth callbacks, email verification, payment redirects, or simply letting users open specific screens from a link in an email. Without deep link support, your app would stay closed while the browser handled everything.
The plugin abstracts away the platform-specific machinery — Android intent filters, iOS URL schemes and universal links, Windows and Linux registry entries — and exposes a consistent API on both the Rust and JavaScript sides.
Platform coverage:
The plugin works on Windows, macOS, Linux, Android, and iOS. Each platform handles deep links differently, but the configuration and the event-based listener API keep your application code largely the same.
How the plugin works
On mobile platforms, the plugin uses your tauri.conf.json configuration to automatically generate the required manifest entries. For Android, it writes <intent-filter> blocks into the AndroidManifest.xml at build time. For iOS, it updates Info.plist with CFBundleURLTypes and, if you use verified universal links, adds the com.apple.developer.associated-domains entitlement. You do not need to touch those platform files by hand.
On desktop, the plugin registers URL schemes with the operating system. On Windows and Linux, the OS opens a completely new process of your app and passes the URL as a command line argument. macOS behaves more like iOS — it delivers the URL to the already-running instance.
Because of that desktop behaviour, you almost always want to combine this plugin with the single-instance plugin. The single-instance plugin prevents multiple copies of your app from running; it catches the new process, extracts the deep link, and forwards it as an event that the running instance can hear.
Before you begin
You need Rust 1.77.2 or later. The plugin is part of the Tauri v2 ecosystem and works with both React + Vite and other frontend frameworks — all examples here assume React with Vite and TypeScript.
Installation
Step 1: Add the Rust crate
In your src-tauri directory, add the plugin to Cargo.toml:
[dependencies]
tauri-plugin-deep-link = "2.0.0"
Alternatively, if you prefer Git sources:
tauri-plugin-deep-link = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
Step 2: Install the JavaScript bindings
Install the npm package so the frontend can call the plugin’s functions:
npm install @tauri-apps/plugin-deep-link
Step 3: Register the plugin in lib.rs
Open src-tauri/src/lib.rs and add the plugin initialisation. At this point the plugin will be active, but it won’t handle any URLs until you define which schemes it should listen for.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_deep_link::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Step 4: Add permissions
In your capabilities file (e.g. src-tauri/capabilities/default.json), include the permissions the plugin needs. The deep-link:default permission set covers all common operations.
{
"permissions": [
"deep-link:default"
]
}
Missing permissions break silently:
If you skip this step, calls like getCurrent() or onOpenUrl() will fail without an obvious error message. The promise simply never resolves, or the handler never fires. Always add the permissions as part of the setup.
Configuration
All URL patterns your app should handle are declared inside tauri.conf.json under plugins.deep-link. The structure differs slightly between mobile and desktop because the platforms work differently.
Mobile configuration
The mobile key accepts an array of objects. Each object defines one URL pattern — either a verified App Link / Universal Link (https with a specific host) or a custom scheme like myapp://.
Custom scheme (no server required)
{
"plugins": {
"deep-link": {
"mobile": [
{
"scheme": ["myapp"],
"appLink": false
}
]
}
}
}
This registers myapp:// on both Android and iOS. No .well-known files or HTTPS hosting needed.
App Link / Universal Link (verified HTTPS)
{
"plugins": {
"deep-link": {
"mobile": [
{
"scheme": ["https"],
"host": "your.website.com",
"pathPrefix": ["/open"],
"appLink": true
}
]
}
}
}
This tells the OS that https://your.website.com/open/* should open your app. You will need to host a verification file on your web server (see the platform-specific sections below).
Desktop configuration
Desktop only supports custom schemes, not HTTPS-based universal links. Provide a list of scheme names under desktop.schemes.
{
"plugins": {
"deep-link": {
"desktop": {
"schemes": ["something", "my-tauri-app"]
}
}
}
}
This configuration will make something:// and my-tauri-app:// open your Tauri app. On Windows and Linux, the plugin writes the necessary registry and desktop file entries during the first run.
Desktop behaviour — a new process by default:
On Windows and Linux, the operating system starts a fresh process of your app for every deep link. That means the onOpenUrl event will not fire in an already-open window unless you also add the single‑instance plugin. We cover that in the Handling Deep Links section.
Platform‑specific setup
Android
The plugin’s build script (build.rs) reads your mobile configuration and injects <intent-filter> entries into AndroidManifest.xml automatically. You do not need to edit the manifest yourself.
For custom schemes (appLink: false), that is all you need. The intent filter will match myapp:// URLs immediately.
For App Links (appLink: true), Android requires domain verification. You must host a file at https://<your-domain>/.well-known/assetlinks.json that proves your app owns the domain. The JSON shape looks like this:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "$APP_BUNDLE_ID",
"sha256_cert_fingerprints": [
"$CERT_FINGERPRINT"
]
}
}
]
$APP_BUNDLE_ID is your tauri.conf.json identifier with hyphens replaced by underscores. $CERT_FINGERPRINT is the SHA‑256 fingerprint of the signing certificate you use to build the APK or AAB. More details are available in Android’s documentation on verifying app links.
iOS
As with Android, the plugin writes the required entries into Info.plist and (when appLink: true) the associated domains entitlement. No manual plist editing needed.
For custom schemes, the plugin creates a CFBundleURLTypes entry that handles your scheme. The app will open for any myapp:// URL.
For Universal Links (appLink: true), iOS expects a server‑hosted apple-app-site-association file at https://<your-domain>/.well-known/apple-app-site-association. A minimal example:
{
"applinks": {
"details": [
{
"appIDs": ["$DEVELOPMENT_TEAM_ID.$APP_BUNDLE_ID"],
"components": [
{
"/": "/open/*",
"comment": "Matches any URL whose path starts with /open/"
}
]
}
]
}
}
$DEVELOPMENT_TEAM_ID is your Apple development team identifier (set in tauri.conf.json under bundle.iOS.developmentTeam or via the TAURI_APPLE_DEVELOPMENT_TEAM environment variable). $APP_BUNDLE_ID is your tauri.conf.json > identifier.
You can verify the association with a curl command:
curl -v https://app-site-association.cdn-apple.com/a/v1/<your-host>
For quick simulator testing, run:
xcrun simctl openurl booted "myapp://some-path"
Desktop (Windows, Linux, macOS)
Desktop configuration is simpler: the plugin will register the schemes you listed in tauri.conf.json. On macOS, deep links are delivered as a system event to the running application. On Windows and Linux, the OS launches a new process with the URL as a command‑line argument.
Because of that new‑process behaviour, you almost always want the single‑instance plugin. That plugin ensures only one copy of your app runs; when a second instance starts, it forwards the URL to the first instance and exits. This lets you listen for deep links exactly like on macOS.
Add the single-instance dependency with the deep-link feature:
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
Then in lib.rs, initialise single-instance before deep-link:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let mut builder = tauri::Builder::default();
#[cfg(desktop)]
{
builder = builder.plugin(
tauri_plugin_single_instance::init(|_app, argv, _cwd| {
println!("a new app instance was opened with {argv:?}");
})
);
}
builder = builder.plugin(tauri_plugin_deep_link::init());
builder
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
With this in place, deep link events will fire inside the first instance, and you can use onOpenUrl on all desktop platforms.
Handling Deep Links
Once the plugin is installed and configured, the real work begins: deciding what to do when a deep link arrives. The plugin gives you a JavaScript API and a Rust API. Most applications will use the JavaScript side because navigation and state live in the frontend, but the Rust side is useful for system‑level actions or logging. Handling Deep Links is the focused API reference.
JavaScript API overview
The @tauri-apps/plugin-deep-link package exports four main functions:
| Function | What it does |
|---|---|
getCurrent | Returns an array of URLs that launched the app. Call this on startup to check the initial intent. |
onOpenUrl | Registers a callback that fires whenever a new deep link arrives while the app is already open. |
isRegistered | Checks whether the app is the default handler for a protocol (desktop only, Windows). |
register | Registers the app as the default handler for a protocol at runtime (desktop only, Windows/Linux). |
Rust API overview
The Rust side exposes a DeepLinkExt trait that you can call from anywhere you have an AppHandle or a type that implements Manager. The key methods mirror the JavaScript functions:
deep_link().get_current()— returnsOption<Vec<Url>>.deep_link().on_open_url(|event| { ... })— registers a closure.deep_link().register("my-app")— runtime registration (Windows/Linux only).
Listening for deep links
Here is a complete React component that listens for deep links both at startup and while the app is running, then parses the URL to navigate or perform an action.
import { useEffect } from "react";
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
function App() {
useEffect(() => {
// Check if the app was opened via a deep link
getCurrent().then((urls) => {
if (urls && urls.length > 0) {
handleUrl(urls[0]);
}
}).catch((err) =>
console.error("Failed to get initial deep link:", err)
);
// Listen for deep links while the app is running
const unlistenPromise = onOpenUrl((urls) => {
if (urls.length > 0) {
handleUrl(urls[0]);
}
});
return () => {
unlistenPromise.then((unlisten) => unlisten());
};
}, []);
const handleUrl = (urlString: string) => {
try {
const url = new URL(urlString);
console.log("Deep link received:", url.href);
// Example: route based on the path
if (url.pathname === "/settings") {
// navigate to settings screen
console.log("Navigate to settings");
} else if (url.pathname.startsWith("/product/")) {
const productId = url.pathname.split("/")[2];
console.log("Open product:", productId);
}
// Extract query parameters
const code = url.searchParams.get("code");
if (code) {
console.log("OAuth code:", code);
}
} catch (error) {
console.error("Invalid deep link URL:", error);
}
};
return (
<div>
<h1>Your App</h1>
<p>Open a deep link to test.</p>
</div>
);
}
export default App;
When you start the app, getCurrent() checks whether the current process was launched by a deep link. After that, onOpenUrl registers a persistent listener that fires every time the OS sends a new URL. The cleanup returned by onOpenUrl stops listening when the component unmounts.
Parsing the raw string with new URL() gives you structured access to the path, query parameters, and host. That makes it straightforward to dispatch the deep link to the correct part of your application.
onOpenUrl requires single-instance on Windows and Linux:
The onOpenUrl event works on macOS, iOS, and Android out of the box. On Windows and Linux, it only fires if you have the single‑instance plugin set up with the deep-link feature. If you skip single‑instance, each deep link spawns a new app process and only getCurrent() can tell you about the URL on launch.
Using the Rust API
If you need to handle deep links before the frontend is ready, or you want to keep all deep link logic in Rust, you can attach a listener during setup:
use tauri_plugin_deep_link::DeepLinkExt;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let mut builder = tauri::Builder::default();
#[cfg(desktop)]
{
builder = builder.plugin(
tauri_plugin_single_instance::init(|_app, argv, _cwd| {
println!("another instance opened with {argv:?}");
})
);
}
builder
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
// Check if the app started via a deep link
if let Some(urls) = app.deep_link().get_current()? {
println!("App started with deep link: {:?}", urls);
}
// Listen for subsequent deep links
app.deep_link().on_open_url(|event| {
println!("Deep link received: {:?}", event.urls());
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The get_current() call inside setup captures the initial URL when the app was started via a deep link. The on_open_url closure then fires for every new deep link while the app stays alive.
Runtime registration (Windows and Linux only)
If you need to associate a scheme with your app after it is already running, use register() from JavaScript or the equivalent Rust method. This is useful when the user turns on a feature that requires a new URL scheme without restarting the app.
import { register } from "@tauri-apps/plugin-deep-link";
await register("my-new-scheme");
On Windows, this writes the appropriate registry keys. On Linux, it updates the application’s .desktop file. Neither macOS, iOS, nor Android support runtime registration — on those platforms, you must declare all schemes in tauri.conf.json beforehand.
Manual argument checking with runtime registration:
If you register schemes dynamically and the OS launches a new instance of your app, the URL will arrive as a command line argument. The single‑instance plugin’s init closure already receives the raw arguments (argv). If you are not using single‑instance, you must parse std::env::args() yourself in get_current().
Verifying your setup
After building and running your app, trigger a deep link from the terminal or browser:
- macOS / iOS Simulator:
open "myapp://test" - Android emulator:
adb shell am start -W -a android.intent.action.VIEW -d "myapp://test" - Windows:
Run
start myapp://testfrom Command Prompt, or click a link in a webpage. - Linux:
xdg-open myapp://test
Everything is working:
If your app opens (without spawning duplicate windows) and the console logs the myapp://test URL, the deep link pipeline is correctly configured.
Common Pitfalls
- Forgetting the single‑instance plugin on desktop. Without it,
onOpenUrlwill never fire on Windows or Linux. The URL still arrives — but only as a command‑line argument to a new process. Always pairdeep-linkwithsingle-instanceif you want a unified listener experience. - Skipping the permission declaration. If
deep-link:defaultisn’t listed in your capabilities, the JavaScript functions silently do nothing. AgetCurrent()call will never resolve, andonOpenUrlhandlers will not execute. - Not serving the verification file for App Links / Universal Links. Android and iOS require the
assetlinks.json/apple-app-site-associationfile to be reachable over HTTPS. Without it, yourhttps://‑based deep links open in the browser instead of your app. Verify withcurlbefore shipping. - Using the wrong plugin initialisation order. Always initialise
single-instancebeforedeep-link. The single‑instance plugin must be the first to catch the new process so it can forward the URL. Reversing the order causes the deep link event to be lost. - Trying to register schemes at runtime on mobile.
register()andunregister()are only supported on Windows and Linux. Mobile platforms require all schemes to be declared statically intauri.conf.json. - Expecting
getCurrent()to return a URL after the app has already started.getCurrent()only reads the command‑line arguments that were present when the app launched. If a deep link arrives later, onlyonOpenUrlwill notify you. Use both in combination:getCurrent()for the initial URL,onOpenUrlfor everything that follows.
Summary
The Deep Link plugin turns your Tauri app into a first‑class citizen of the URL‑driven world. After a one‑time configuration in tauri.conf.json, the plugin handles the platform‑specific registration machinery and gives you a single, event‑based interface to react to incoming links.
The key takeaway is that desktop platforms behave differently from mobile. On macOS, iOS, and Android, the OS delivers deep links to the already‑running app. On Windows and Linux, each deep link spawns a new process — a problem the single‑instance plugin solves neatly. Build your listener around getCurrent() for the launch URL and onOpenUrl for subsequent arrivals, test on all target platforms early, and double‑check your permissions and verification files.
Once deep links are working, you can wire them into OAuth flows, product‑page navigation, or any cross‑context action that should land inside your app.
Introduction to the Deep Link Plugin
Learn what deep links are, the real-world problems they solve, and how Tauri's deep link plugin enables your app to receive and react to custom URLs across all supported platforms.
Handling Deep Links
Learn how to register custom URL schemes, listen for deep link events, and process incoming URLs in your Tauri v2 application with the Deep Link Plugin