Repository: tauri-apps/tauri (language: rust)

Issue:
[bug]  [v2] IndexedDB path changed after upgrading to tauri 2
### Describe the bug

After upgrading from tauri 1 to tauri 2, the IndexedDB directory changed from `https_tauri.localhost_0.indexeddb.leveldb` to `http_tauri.localhost_0.indexeddb.leveldb`. Since my app stores all data in IndexedDB, this will cause "data losing" from user's point of view.

This happens on Windows, the full paths are:

- `C:\Users\[username]\AppData\Local\[appid]\EBWebView\Default\IndexedDB\https_tauri.localhost_0.indexeddb.leveldb`
- `C:\Users\[username]\AppData\Local\[appid]\EBWebView\Default\IndexedDB\http_tauri.localhost_0.indexeddb.leveldb`

### Reproduction

To verify that tauri 2 uses said IndexedDB directory `http_tauri.localhost_0.indexeddb.leveldb`, following these steps:

1. create a new app: `pnpm create tauri-app`
2. do anything that causes IndexedDB directory being created, for example, using `idb`:
  a. `pnpm i idb`
  b. open IndexedDB: `openDB("test").then((db) => console.log("db opened: ", db));`
3. build: `pnpm tauri build --debug`
4. run the app and check the directory

I've set up a repo at https://github.com/hut36/tauri2-indexeddb for you to test.



### Expected behavior

The directory `https_tauri.localhost_0.indexeddb.leveldb` is expected, instead of `http_tauri.localhost_0.indexeddb.leveldb`. See above for the full path.

### Full `tauri info` output

```text
[✔] Environment
    - OS: Windows 10.0.22631 x86_64 (X64)
    ✔ WebView2: 125.0.2535.92
    ✔ MSVC: Visual Studio 生成工具 2022
    ✔ rustc: 1.81.0 (eeb90cda1 2024-09-04)
    ✔ cargo: 1.81.0 (2dbb1af80 2024-08-20)
    ✔ rustup: 1.27.1 (54dd3d00f 2024-04-24)
    ✔ Rust toolchain: stable-x86_64-pc-windows-msvc (default)
    - node: 20.12.0
    - pnpm: 9.10.0
    - yarn: 1.22.22
    - npm: 9.6.3

[-] Packages
    - tauri 🦀: 2.0.1
    - tauri-build 🦀: 2.0.1
    - wry 🦀: 0.44.1
    - tao 🦀: 0.30.3
    - @tauri-apps/api : 2.0.1
    - @tauri-apps/cli : 2.0.1

[-] Plugins
    - tauri-plugin-shell 🦀: 2.0.1
    - @tauri-apps/plugin-shell : 2.0.0

[-] App
    - build-type: bundle
    - CSP: unset
    - frontendDist: ../build
    - devUrl: http://localhost:1420/
    - framework: Svelte
    - bundler: Vite
```


### Stack trace

_No response_

### Additional context

Related discussion: https://github.com/tauri-apps/tauri/discussions/11231. @FabianLars says this is related to `dangerousUseHttpScheme`.


Discussion / hints:

Fix patch:
diff --git a/.changes/use_https_windows-and-android-config.md b/.changes/use_https_windows-and-android-config.md
new file mode 100644
index 000000000000..3e5598a89e42
--- /dev/null
+++ b/.changes/use_https_windows-and-android-config.md
@@ -0,0 +1,6 @@
+---
+"tauri": "minor:feat"
+"tauri-utils": "minor:feat"
+---
+
+Add `app > windows > useHttpsScheme` config option to choose whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android
diff --git a/.changes/use_https_windows-and-android.md b/.changes/use_https_windows-and-android.md
new file mode 100644
index 000000000000..0e1bbb1a6c6d
--- /dev/null
+++ b/.changes/use_https_windows-and-android.md
@@ -0,0 +1,7 @@
+---
+"tauri": "minor:feat"
+"tauri-runtime": "minor:feat"
+"tauri-runtime-wry": "minor:feat"
+---
+
+Add `WebviewWindowBuilder/WebviewBuilder::use_https_scheme` to choose whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android
diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json
index 90acf8973183..820fb35a55f0 100644
--- a/crates/tauri-cli/config.schema.json
+++ b/crates/tauri-cli/config.schema.json
@@ -486,6 +486,11 @@
           "description": "Whether browser extensions can be installed for the webview process\n\n ## Platform-specific:\n\n - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)\n - **MacOS / Linux / iOS / Android** - Unsupported.",
           "default": false,
           "type": "boolean"
+        },
+        "useHttpsScheme": {
+          "description": "Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.\n\n ## Note\n\n Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.\n\n ## Warning\n\n Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.",
+          "default": false,
+          "type": "boolean"
         }
       },
       "additionalProperties": false
diff --git a/crates/tauri-cli/src/migrate/migrations/v1/config.rs b/crates/tauri-cli/src/migrate/migrations/v1/config.rs
index ab956e76aa19..2b211470a096 100644
--- a/crates/tauri-cli/src/migrate/migrations/v1/config.rs
+++ b/crates/tauri-cli/src/migrate/migrations/v1/config.rs
@@ -87,6 +87,28 @@ fn migrate_config(config: &mut Value) -> Result<MigratedConfig> {
         migrated.permissions = permissions;
       }
 
+      // dangerousUseHttpScheme/useHttpsScheme
+      let dangerouse_use_http = tauri_config
+        .get("security")
+        .and_then(|w| w.as_object())
+        .and_then(|w| {
+          w.get("dangerousUseHttpScheme")
+            .or_else(|| w.get("dangerous-use-http-scheme"))
+        })
+        .and_then(|v| v.as_bool())
+        .unwrap_or_default();
+
+      if let Some(windows) = tauri_config
+        .get_mut("windows")
+        .and_then(|w| w.as_array_mut())
+      {
+        for window in windows {
+          if let Some(window) = window.as_object_mut() {
+            window.insert("useHttpsScheme".to_string(), (!dangerouse_use_http).into());
+          }
+        }
+      }
+
       // security
       if let Some(security) = tauri_config
         .get_mut("security")
@@ -802,7 +824,8 @@ mod test {
         "pattern": { "use": "brownfield" },
         "security": {
           "csp": "default-src 'self' tauri:"
-        }
+        },
+        "windows": [{}]
       }
     });
 
@@ -907,6 +930,8 @@ mod test {
       migrated["app"]["withGlobalTauri"],
       original["build"]["withGlobalTauri"]
     );
+
+    assert_eq!(migrated["app"]["windows"][0]["useHttpsScheme"], true);
   }
 
   #[test]
@@ -941,6 +966,28 @@ mod test {
     );
   }
 
+  #[test]
+  fn migrate_dangerous_use_http_scheme() {
+    let original = serde_json::json!({
+      "tauri": {
+        "windows": [{}],
+        "security": {
+          "dangerousUseHttpScheme": true,
+        }
+      }
+    });
+
+    let migrated = migrate(&original);
+    assert_eq!(
+      !migrated["app"]["windows"][0]["useHttpsScheme"]
+        .as_bool()
+        .unwrap(),
+      original["tauri"]["security"]["dangerousUseHttpScheme"]
+        .as_bool()
+        .unwrap()
+    );
+  }
+
   #[test]
   fn can_migrate_default_config() {
     let original = serde_json::to_value(tauri_utils_v1::config::Config::default()).unwrap();
diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs
index 0e2f23386f46..838def7790ec 100644
--- a/crates/tauri-runtime-wry/src/lib.rs
+++ b/crates/tauri-runtime-wry/src/lib.rs
@@ -22,8 +22,8 @@ use tauri_runtime::{
   monitor::Monitor,
   webview::{DetachedWebview, DownloadEvent, PendingWebview, WebviewIpcHandler},
   window::{
-    CursorIcon, DetachedWindow, DragDropEvent, PendingWindow, RawWindow, WebviewEvent,
-    WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, WindowSizeConstraints,
+    CursorIcon, DetachedWindow, DetachedWindowWebview, DragDropEvent, PendingWindow, RawWindow,
+    WebviewEvent, WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, WindowSizeConstraints,
   },
   DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState,
   ProgressBarStatus, Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType,
@@ -276,7 +276,16 @@ impl<T: UserEvent> Context<T> {
     let label = pending.label.clone();
     let context = self.clone();
     let window_id = self.next_window_id();
-    let webview_id = pending.webview.as_ref().map(|_| context.next_webview_id());
+    let (webview_id, use_https_scheme) = pending
+      .webview
+      .as_ref()
+      .map(|w| {
+        (
+          Some(context.next_webview_id()),
+          w.webview_attributes.use_https_scheme,
+        )
+      })
+      .unwrap_or((None, false));
 
     send_user_message(
       self,
@@ -300,13 +309,19 @@ impl<T: UserEvent> Context<T> {
       context: self.clone(),
     };
 
-    let detached_webview = webview_id.map(|id| DetachedWebview {
-      label: label.clone(),
-      dispatcher: WryWebviewDispatcher {
-        window_id: Arc::new(Mutex::new(window_id)),
-        webview_id: id,
-        context: self.clone(),
-      },
+    let detached_webview = webview_id.map(|id| {
+      let webview = DetachedWebview {
+        label: label.clone(),
+        dispatcher: WryWebviewDispatcher {
+          window_id: Arc::new(Mutex::new(window_id)),
+          webview_id: id,
+          context: self.clone(),
+        },
+      };
+      DetachedWindowWebview {
+        webview,
+        use_https_scheme,
+      }
     });
 
     Ok(DetachedWindow {
@@ -746,6 +761,8 @@ impl WindowBuilder for WindowBuilderWrapper {
       builder = builder.title_bar_style(TitleBarStyle::Visible);
     }
 
+    builder = builder.title("Tauri App");
+
     builder
   }
 
@@ -2497,10 +2514,16 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
   ) -> Result<DetachedWindow<T, Self>> {
     let label = pending.label.clone();
     let window_id = self.context.next_window_id();
-    let webview_id = pending
+    let (webview_id, use_https_scheme) = pending
       .webview
       .as_ref()
-      .map(|_| self.context.next_webview_id());
+      .map(|w| {
+        (
+          Some(self.context.next_webview_id()),
+          w.webview_attributes.use_https_scheme,
+        )
+      })
+      .unwrap_or((None, false));
 
     let window = create_window(
       window_id,
@@ -2524,13 +2547,19 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
       .borrow_mut()
       .insert(window_id, window);
 
-    let detached_webview = webview_id.map(|id| DetachedWebview {
-      label: label.clone(),
-      dispatcher: WryWebviewDispatcher {
-        window_id: Arc::new(Mutex::new(window_id)),
-        webview_id: id,
-        context: self.context.clone(),
-      },
+    let detached_webview = webview_id.map(|id| {
+      let webview = DetachedWebview {
+        label: label.clone(),
+        dispatcher: WryWebviewDispatcher {
+          window_id: Arc::new(Mutex::new(window_id)),
+          webview_id: id,
+          context: self.context.clone(),
+        },
+      };
+      DetachedWindowWebview {
+        webview,
+        use_https_scheme,
+      }
     });
 
     Ok(DetachedWindow {
@@ -4026,6 +4055,11 @@ fn create_webview<T: UserEvent>(
     .with_clipboard(webview_attributes.clipboard)
     .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled);
 
+  #[cfg(any(target_os = "windows", target_os = "android"))]
+  {
+    webview_builder = webview_builder.with_https_scheme(webview_attributes.use_https_scheme);
+  }
+
   if webview_attributes.drag_drop_handler_enabled {
     let proxy = context.proxy.clone();
     let window_id_ = window_id.clone();
@@ -4168,11 +4202,6 @@ fn create_webview<T: UserEvent>(
     });
   }
 
-  #[cfg(windows)]
-  {
-    webview_builder = webview_builder.with_https_scheme(false);
-  }
-
   #[cfg(windows)]
   {
     webview_builder = webview_builder
@@ -4282,7 +4311,7 @@ fn create_webview<T: UserEvent>(
       builder
     }
   }
-  .map_err(|e| Error::CreateWebview(Box::new(dbg!(e))))?;
+  .map_err(|e| Error::CreateWebview(Box::new(e)))?;
 
   if kind == WebviewKind::WindowContent {
     #[cfg(any(
diff --git a/crates/tauri-runtime/src/webview.rs b/crates/tauri-runtime/src/webview.rs
index 651aaec5eac3..149983808895 100644
--- a/crates/tauri-runtime/src/webview.rs
+++ b/crates/tauri-runtime/src/webview.rs
@@ -210,6 +210,7 @@ pub struct WebviewAttributes {
   pub proxy_url: Option<Url>,
   pub zoom_hotkeys_enabled: bool,
   pub browser_extensions_enabled: bool,
+  pub use_https_scheme: bool,
 }
 
 impl From<&WindowConfig> for WebviewAttributes {
@@ -218,6 +219,7 @@ impl From<&WindowConfig> for WebviewAttributes {
       .incognito(config.incognito)
       .focused(config.focus)
       .zoom_hotkeys_enabled(config.zoom_hotkeys_enabled)
+      .use_https_scheme(config.use_https_scheme)
       .browser_extensions_enabled(config.browser_extensions_enabled);
     #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
     {
@@ -264,6 +266,7 @@ impl WebviewAttributes {
       proxy_url: None,
       zoom_hotkeys_enabled: false,
       browser_extensions_enabled: false,
+      use_https_scheme: false,
     }
   }
 
@@ -388,6 +391,21 @@ impl WebviewAttributes {
     self.browser_extensions_enabled = enabled;
     self
   }
+
+  /// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
+  ///
+  /// ## Note
+  ///
+  /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
+  ///
+  /// ## Warning
+  ///
+  /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.
+  #[must_use]
+  pub fn use_https_scheme(mut self, enabled: bool) -> Self {
+    self.use_https_scheme = enabled;
+    self
+  }
 }
 
 /// IPC handler.
diff --git a/crates/tauri-runtime/src/window.rs b/crates/tauri-runtime/src/window.rs
index 54676ef65bf9..cea526f5f120 100644
--- a/crates/tauri-runtime/src/window.rs
+++ b/crates/tauri-runtime/src/window.rs
@@ -512,7 +512,23 @@ pub struct DetachedWindow<T: UserEvent, R: Runtime<T>> {
   pub dispatcher: R::WindowDispatcher,
 
   /// The webview dispatcher in case this window has an attached webview.
-  pub webview: Option<DetachedWebview<T, R>>,
+  pub webview: Option<DetachedWindowWebview<T, R>>,
+}
+
+/// A detached webview associated with a window.
+#[derive(Debug)]
+pub struct DetachedWindowWebview<T: UserEvent, R: Runtime<T>> {
+  pub webview: DetachedWebview<T, R>,
+  pub use_https_scheme: bool,
+}
+
+impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindowWebview<T, R> {
+  fn clone(&self) -> Self {
+    Self {
+      webview: self.webview.clone(),
+      use_https_scheme: self.use_https_scheme,
+    }
+  }
 }
 
 impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindow<T, R> {
diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json
index 90acf8973183..820fb35a55f0 100644
--- a/crates/tauri-schema-generator/schemas/config.schema.json
+++ b/crates/tauri-schema-generator/schemas/config.schema.json
@@ -486,6 +486,11 @@
           "description": "Whether browser extensions can be installed for the webview process\n\n ## Platform-specific:\n\n - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)\n - **MacOS / Linux / iOS / Android** - Unsupported.",
           "default": false,
           "type": "boolean"
+        },
+        "useHttpsScheme": {
+          "description": "Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.\n\n ## Note\n\n Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.\n\n ## Warning\n\n Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.",
+          "default": false,
+          "type": "boolean"
         }
       },
       "additionalProperties": false
diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs
index 041deda585b1..4affc4c87cd2 100644
--- a/crates/tauri-utils/src/config.rs
+++ b/crates/tauri-utils/src/config.rs
@@ -1519,6 +1519,18 @@ pub struct WindowConfig {
   /// - **MacOS / Linux / iOS / Android** - Unsupported.
   #[serde(default)]
   pub browser_extensions_enabled: bool,
+
+  /// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
+  ///
+  /// ## Note
+  ///
+  /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
+  ///
+  /// ## Warning
+  ///
+  /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.
+  #[serde(default, alias = "use-https-scheme")]
+  pub use_https_scheme: bool,
 }
 
 impl Default for WindowConfig {
@@ -1567,6 +1579,7 @@ impl Default for WindowConfig {
       proxy_url: None,
       zoom_hotkeys_enabled: false,
       browser_extensions_enabled: false,
+      use_https_scheme: false,
     }
   }
 }
@@ -2538,6 +2551,7 @@ mod build {
       let parent = opt_str_lit(self.parent.as_ref());
       let zoom_hotkeys_enabled = self.zoom_hotkeys_enabled;
       let browser_extensions_enabled = self.browser_extensions_enabled;
+      let use_https_scheme = self.use_https_scheme;
 
       literal_struct!(
         tokens,
@@ -2584,7 +2598,8 @@ mod build {
         incognito,
         parent,
         zoom_hotkeys_enabled,
-        browser_extensions_enabled
+        browser_extensions_enabled,
+        use_https_scheme
       );
     }
   }
diff --git a/crates/tauri/scripts/core.js b/crates/tauri/scripts/core.js
index 6f4523c8d67a..b68b6039195b 100644
--- a/crates/tauri/scripts/core.js
+++ b/crates/tauri/scripts/core.js
@@ -8,12 +8,13 @@
   }
 
   const osName = __TEMPLATE_os_name__
+  const protocolScheme = __TEMPLATE_protocol_scheme__
 
   Object.defineProperty(window.__TAURI_INTERNALS__, 'convertFileSrc', {
     value: function (filePath, protocol = 'asset') {
       const path = encodeURIComponent(filePath)
       return osName === 'windows' || osName === 'android'
-        ? `http://${protocol}.localhost/${path}`
+        ? `${protocolScheme}://${protocol}.localhost/${path}`
         : `${protocol}://localhost/${path}`
     }
   })
diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs
index b513ed7f8964..509ba340c8f4 100644
--- a/crates/tauri/src/app.rs
+++ b/crates/tauri/src/app.rs
@@ -268,6 +268,10 @@ pub struct AssetResolver<R: Runtime> {
 impl<R: Runtime> AssetResolver<R> {
   /// Gets the app asset associated with the given path.
   ///
+  /// By default it tries to infer your application's URL scheme in production by checking if all webviews
+  /// were configured with [`crate::webview::WebviewBuilder::use_https_scheme`] or `tauri.conf.json > app > windows > useHttpsScheme`.
+  /// If you are resolving an asset for a webview with a more dynamic configuration, see [`AssetResolver::get_for_scheme`].
+  ///
   /// Resolves to the embedded asset that is part of the app
   /// in dev when [`devUrl`](https://v2.tauri.app/reference/config/#devurl) points to a folder in your filesystem
   /// or in production when [`frontendDist`](https://v2.tauri.app/reference/config/#frontenddist)
@@ -276,6 +280,19 @@ impl<R: Runtime> AssetResolver<R> {
   /// Fallbacks to reading the asset from the [distDir] folder so the behavior is consistent in development.
   /// Note that the dist directory must exist so you might need to build your frontend assets first.
   pub fn get(&self, path: String) -> Option<Asset> {
+    let use_https_scheme = self
+      .manager
+      .webviews()
+      .values()
+      .all(|webview| webview.use_https_scheme());
+    self.get_for_scheme(path, use_https_scheme)
+  }
+
+  ///  Same as [AssetResolver::get] but resolves the custom protocol scheme based on a parameter.
+  ///
+  /// - `use_https_scheme`: If `true` when using [`Pattern::Isolation`](tauri::Pattern::Isolation),
+  ///   the csp header will contain `https://tauri.localhost` instead of `http://tauri.localhost`
+  pub fn get_for_scheme(&self, path: String, use_https_scheme: bool) -> Option<Asset> {
     #[cfg(dev)]
     {
       // on dev if the devPath is a path to a directory we have the embedded assets
@@ -306,7 +323,7 @@ impl<R: Runtime> AssetResolver<R> {
       }
     }
 
-    self.manager.get_asset(path).ok()
+    self.manager.get_asset(path, use_https_scheme).ok()
   }
 
   /// Iterate on all assets.
diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs
index d272b241bfb9..81f0eaae284d 100644
--- a/crates/tauri/src/manager/mod.rs
+++ b/crates/tauri/src/manager/mod.rs
@@ -340,9 +340,10 @@ impl<R: Runtime> AppManager<R> {
     self.config.build.dev_url.as_ref()
   }
 
-  pub(crate) fn protocol_url(&self) -> Cow<'_, Url> {
+  pub(crate) fn protocol_url(&self, https: bool) -> Cow<'_, Url> {
     if cfg!(windows) || cfg!(target_os = "android") {
-      Cow::Owned(Url::parse("http://tauri.localhost").unwrap())
+      let scheme = if https { "https" } else { "http" };
+      Cow::Owned(Url::parse(&format!("{scheme}://tauri.localhost")).unwrap())
     } else {
       Cow::Owned(Url::parse("tauri://localhost").unwrap())
     }
@@ -351,10 +352,10 @@ impl<R: Runtime> AppManager<R> {
   /// Get the base URL to use for webview requests.
   ///
   /// In dev mode, this will be based on the `devUrl` configuration value.
-  pub(crate) fn get_url(&self) -> Cow<'_, Url> {
+  pub(crate) fn get_url(&self, https: bool) -> Cow<'_, Url> {
     match self.base_path() {
       Some(url) => Cow::Borrowed(url),
-      _ => self.protocol_url(),
+      _ => self.protocol_url(https),
     }
   }
 
@@ -372,7 +373,11 @@ impl<R: Runtime> AppManager<R> {
     }
   }
 
-  pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
+  pub fn get_asset(
+    &self,
+    mut path: String,
+    use_https_schema: bool,
+  ) -> Result<Asset, Box<dyn std::error::Error>> {
     let assets = &self.assets;
     if path.ends_with('/') {
       path.pop();
@@ -435,7 +440,7 @@ impl<R: Runtime> AppManager<R> {
               let default_src = csp_map
                 .entry("default-src".into())
                 .or_insert_with(Default::default);
-              default_src.push(crate::pattern::format_real_schema(schema));
+              default_src.push(crate::pattern::format_real_schema(schema, use_https_schema));
             }
 
             csp_header.replace(Csp::DirectiveMap(csp_map).to_string());
@@ -771,17 +776,25 @@ mod test {
     #[cfg(custom_protocol)]
     {
       assert_eq!(
-        manager.get_url().to_string(),
+        manager.get_url(false).to_string(),
         if cfg!(windows) || cfg!(target_os = "android") {
           "http://tauri.localhost/"
         } else {
           "tauri://localhost"
         }
       );
+      assert_eq!(
+        manager.get_url(true).to_string(),
+        if cfg!(windows) || cfg!(target_os = "android") {
+          "https://tauri.localhost/"
+        } else {
+          "tauri://localhost"
+        }
+      );
     }
 
     #[cfg(dev)]
-    assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
+    assert_eq!(manager.get_url(false).to_string(), "http://localhost:4000/");
   }
 
   struct EventSetup {
diff --git a/crates/tauri/src/manager/webview.rs b/crates/tauri/src/manager/webview.rs
index 5eb4ba8de2a3..8773600dca2e 100644
--- a/crates/tauri/src/manager/webview.rs
+++ b/crates/tauri/src/manager/webview.rs
@@ -137,10 +137,14 @@ impl<R: Runtime> WebviewManager<R> {
 
     let mut webview_attributes = pending.webview_attributes;
 
+    let use_https_scheme = webview_attributes.use_https_scheme;
+
     let ipc_init = IpcJavascript {
       isolation_origin: &match &*app_manager.pattern {
         #[cfg(feature = "isolation")]
-        crate::Pattern::Isolation { schema, .. } => crate::pattern::format_real_schema(schema),
+        crate::Pattern::Isolation { schema, .. } => {
+          crate::pattern::format_real_schema(schema, use_https_scheme)
+        }
         _ => "".to_string(),
       },
     }
@@ -180,6 +184,7 @@ impl<R: Runtime> WebviewManager<R> {
         &ipc_init.into_string(),
         &pattern_init.into_string(),
         is_init_global,
+        use_https_scheme,
       )?);
 
     for plugin_init_script in plugin_init_scripts {
@@ -190,7 +195,7 @@ impl<R: Runtime> WebviewManager<R> {
     if let crate::Pattern::Isolation { schema, .. } = &*app_manager.pattern {
       webview_attributes = webview_attributes.initialization_script(
         &IsolationJavascript {
-          isolation_src: &crate::pattern::format_real_schema(schema),
+          isolation_src: &crate::pattern::format_real_schema(schema, use_https_scheme),
           style: tauri_utils::pattern::isolation::IFRAME_STYLE,
         }
         .render_default(&Default::default())?
@@ -232,7 +237,8 @@ impl<R: Runtime> WebviewManager<R> {
       && window_url.scheme() != "http"
       && window_url.scheme() != "https"
     {
-      format!("http://{}.localhost", window_url.scheme())
+      let https = if use_https_scheme { "https" } else { "http" };
+      format!("{https}://{}.localhost", window_url.scheme())
     } else if let Some(host) = window_url.host() {
       format!(
         "{}://{}{}",
@@ -320,6 +326,7 @@ impl<R: Runtime> WebviewManager<R> {
         assets.clone(),
         *crypto_keys.aes_gcm().raw(),
         window_origin,
+        use_https_scheme,
       );
       pending.register_uri_scheme_protocol(schema, move |webview_id, request, responder| {
         protocol(webview_id, request, UriSchemeResponder(responder))
@@ -335,6 +342,7 @@ impl<R: Runtime> WebviewManager<R> {
     ipc_script: &str,
     pattern_script: &str,
     with_global_tauri: bool,
+    use_https_scheme: bool,
   ) -> crate::Result<String> {
     #[derive(Template)]
     #[default_template("../../scripts/init.js")]
@@ -357,6 +365,7 @@ impl<R: Runtime> WebviewManager<R> {
     #[default_template("../../scripts/core.js")]
     struct CoreJavascript<'a> {
       os_name: &'a str,
+      protocol_scheme: &'a str,
       invoke_key: &'a str,
     }
 
@@ -378,6 +387,7 @@ impl<R: Runtime> WebviewManager<R> {
       bundle_script,
       core_script: &CoreJavascript {
         os_name: std::env::consts::OS,
+        protocol_scheme: if use_https_scheme { "https" } else { "http" },
         invoke_key: self.invoke_key(),
       }
       .render_default(&Default::default())?
@@ -411,7 +421,7 @@ impl<R: Runtime> WebviewManager<R> {
         let url = if PROXY_DEV_SERVER {
           Cow::Owned(Url::parse("tauri://localhost").unwrap())
         } else {
-          app_manager.get_url()
+          app_manager.get_url(pending.webview_attributes.use_https_scheme)
         };
         // ignore "index.html" just to simplify the url
         if path.to_str() != Some("index.html") {
@@ -425,7 +435,7 @@ impl<R: Runtime> WebviewManager<R> {
         }
       }
       WebviewUrl::External(url) => {
-        let config_url = app_manager.get_url();
+        let config_url = app_manager.get_url(pending.webview_attributes.use_https_scheme);
         let is_local = config_url.make_relative(url).is_some();
         let mut url = url.clone();
         if is_local && PROXY_DEV_SERVER {
@@ -572,8 +582,9 @@ impl<R: Runtime> WebviewManager<R> {
     &self,
     window: Window<R>,
     webview: DetachedWebview<EventLoopMessage, R>,
+    use_https_scheme: bool,
   ) -> Webview<R> {
-    let webview = Webview::new(window, webview);
+    let webview = Webview::new(window, webview, use_https_scheme);
 
     let webview_event_listeners = self.event_listeners.clone();
     let webview_ = webview.clone();
diff --git a/crates/tauri/src/pattern.rs b/crates/tauri/src/pattern.rs
index 221fce9fb1c2..d0cd132c21b9 100644
--- a/crates/tauri/src/pattern.rs
+++ b/crates/tauri/src/pattern.rs
@@ -85,9 +85,10 @@ pub(crate) struct PatternJavascript {
 }
 
 #[allow(dead_code)]
-pub(crate) fn format_real_schema(schema: &str) -> String {
+pub(crate) fn format_real_schema(schema: &str, https: bool) -> String {
   if cfg!(windows) || cfg!(target_os = "android") {
-    format!("http://{schema}.{ISOLATION_IFRAME_SRC_DOMAIN}")
+    let scheme = if https { "https" } else { "http" };
+    format!("{scheme}://{schema}.{ISOLATION_IFRAME_SRC_DOMAIN}")
   } else {
     format!("{schema}://{ISOLATION_IFRAME_SRC_DOMAIN}")
   }
diff --git a/crates/tauri/src/protocol/isolation.rs b/crates/tauri/src/protocol/isolation.rs
index 6d546d288695..f85f8e6cd3a7 100644
--- a/crates/tauri/src/protocol/isolation.rs
+++ b/crates/tauri/src/protocol/isolation.rs
@@ -21,9 +21,11 @@ pub fn get<R: Runtime>(
   assets: Arc<EmbeddedAssets>,
   aes_gcm_key: [u8; 32],
   window_origin: String,
+  use_https_scheme: bool,
 ) -> UriSchemeProtocolHandler {
   let frame_src = if cfg!(any(windows, target_os = "android")) {
-    format!("http://{schema}.localhost")
+    let https = if use_https_scheme { "https" } else { "http" };
+    format!("{https}://{schema}.localhost")
   } else {
     format!("{schema}:")
   };
diff --git a/crates/tauri/src/protocol/tauri.rs b/crates/tauri/src/protocol/tauri.rs
index 2d33c4a00f7f..ac33b5be2de7 100644
--- a/crates/tauri/src/protocol/tauri.rs
+++ b/crates/tauri/src/protocol/tauri.rs
@@ -30,7 +30,10 @@ pub fn get<R: Runtime>(
 ) -> UriSchemeProtocolHandler {
   #[cfg(all(dev, mobile))]
   let url = {
-    let mut url = manager.get_url().as_str().to_string();
+    let mut url = manager
+      .get_url(window_origin.starts_with("https"))
+      .as_str()
+      .to_string();
     if url.ends_with('/') {
       url.pop();
     }
@@ -152,7 +155,8 @@ fn get_response<R: Runtime>(
 
   #[cfg(not(all(dev, mobile)))]
   let mut response = {
-    let asset = manager.get_asset(path)?;
+    let use_https_scheme = request.uri().scheme() == Some(&http::uri::Scheme::HTTPS);
+    let asset = manager.get_asset(path, use_https_scheme)?;
     builder = builder.header(CONTENT_TYPE, &asset.mime_type);
     if let Some(csp) = &asset.csp_header {
       builder = builder.header("Content-Security-Policy", csp);
diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs
index 06fa0eda0909..199d5a8f90d8 100644
--- a/crates/tauri/src/webview/mod.rs
+++ b/crates/tauri/src/webview/mod.rs
@@ -605,11 +605,17 @@ tauri::Builder::default()
 
     pending.webview_attributes.bounds = Some(tauri_runtime::Rect { size, position });
 
+    let use_https_scheme = pending.webview_attributes.use_https_scheme;
+
     let webview = match &mut window.runtime() {
       RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_webview(pending),
       _ => unimplemented!(),
     }
-    .map(|webview| app_manager.webview.attach_webview(window.clone(), webview))?;
+    .map(|webview| {
+      app_manager
+        .webview
+        .attach_webview(window.clone(), webview, use_https_scheme)
+    })?;
 
     Ok(webview)
   }
@@ -794,6 +800,21 @@ fn main() {
     self.webview_attributes.browser_extensions_enabled = enabled;
     self
   }
+
+  /// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
+  ///
+  /// ## Note
+  ///
+  /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
+  ///
+  /// ## Warning
+  ///
+  /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.
+  #[must_use]
+  pub fn use_https_scheme(mut self, enabled: bool) -> Self {
+    self.webview_attributes.use_https_scheme = enabled;
+    self
+  }
 }
 
 /// Webview.
@@ -806,6 +827,7 @@ pub struct Webview<R: Runtime> {
   pub(crate) manager: Arc<AppManager<R>>,
   pub(crate) app_handle: AppHandle<R>,
   pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
+  use_https_scheme: bool,
 }
 
 impl<R: Runtime> std::fmt::Debug for Webview<R> {
@@ -813,6 +835,7 @@ impl<R: Runtime> std::fmt::Debug for Webview<R> {
     f.debug_struct("Window")
       .field("window", &self.window.lock().unwrap())
       .field("webview", &self.webview)
+      .field("use_https_scheme", &self.use_https_scheme)
       .finish()
   }
 }
@@ -825,6 +848,7 @@ impl<R: Runtime> Clone for Webview<R> {
       manager: self.manager.clone(),
       app_handle: self.app_handle.clone(),
       resources_table: self.resources_table.clone(),
+      use_https_scheme: self.use_https_scheme,
     }
   }
 }
@@ -847,13 +871,18 @@ impl<R: Runtime> PartialEq for Webview<R> {
 /// Base webview functions.
 impl<R: Runtime> Webview<R> {
   /// Create a new webview that is attached to the window.
-  pub(crate) fn new(window: Window<R>, webview: DetachedWebview<EventLoopMessage, R>) -> Self {
+  pub(crate) fn new(
+    window: Window<R>,
+    webview: DetachedWebview<EventLoopMessage, R>,
+    use_https_scheme: bool,
+  ) -> Self {
     Self {
       manager: window.manager.clone(),
       app_handle: window.app_handle.clone(),
       window: Arc::new(Mutex::new(window)),
       webview,
       resources_table: Default::default(),
+      use_https_scheme,
     }
   }
 
@@ -880,6 +909,11 @@ impl<R: Runtime> Webview<R> {
     &self.webview.label
   }
 
+  /// Whether the webview was configured to use the HTTPS scheme or not.
+  pub(crate) fn use_https_scheme(&self) -> bool {
+    self.use_https_scheme
+  }
+
   /// Registers a window event listener.
   pub fn on_webview_event<F: Fn(&WebviewEvent) + Send + 'static>(&self, f: F) {
     self
@@ -1180,9 +1214,11 @@ fn main() {
   }
 
   fn is_local_url(&self, current_url: &Url) -> bool {
+    let uses_https = current_url.scheme() == "https";
+
     // if from `tauri://` custom protocol
     ({
-      let protocol_url = self.manager().protocol_url();
+      let protocol_url = self.manager().protocol_url(uses_https);
       current_url.scheme() == protocol_url.scheme()
       && current_url.domain() == protocol_url.domain()
     }) ||
@@ -1190,7 +1226,7 @@ fn main() {
     // or if relative to `devUrl` or `frontendDist`
       self
           .manager()
-          .get_url()
+          .get_url(uses_https)
           .make_relative(current_url)
           .is_some()
 
@@ -1206,7 +1242,7 @@ fn main() {
         // so we check using the first part of the domain
         #[cfg(any(windows, target_os = "android"))]
         let local = {
-          let protocol_url = self.manager().protocol_url();
+          let protocol_url = self.manager().protocol_url(uses_https);
           let maybe_protocol = current_url
             .domain()
             .and_then(|d| d .split_once('.'))
diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs
index 853d5406452d..f66b119abe19 100644
--- a/crates/tauri/src/webview/webview_window.rs
+++ b/crates/tauri/src/webview/webview_window.rs
@@ -898,6 +898,21 @@ impl<'a, R: Runtime, M: Manager<R>> WebviewWindowBuilder<'a, R, M> {
     self.webview_builder = self.webview_builder.browser_extensions_enabled(enabled);
     self
   }
+
+  /// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
+  ///
+  /// ## Note
+  ///
+  /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
+  ///
+  /// ## Warning
+  ///
+  /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.
+  #[must_use]
+  pub fn use_https_scheme(mut self, enabled: bool) -> Self {
+    self.webview_builder = self.webview_builder.use_https_scheme(enabled);
+    self
+  }
 }
 
 /// A type that wraps a [`Window`] together with a [`Webview`].
diff --git a/crates/tauri/src/window/mod.rs b/crates/tauri/src/window/mod.rs
index 9b8e02b8ef7a..f1292b37ab94 100644
--- a/crates/tauri/src/window/mod.rs
+++ b/crates/tauri/src/window/mod.rs
@@ -381,7 +381,11 @@ tauri::Builder::default()
       );
 
       if let Some(webview) = detached_window.webview {
-        app_manager.webview.attach_webview(window.clone(), webview);
+        app_manager.webview.attach_webview(
+          window.clone(),
+          webview.webview,
+          webview.use_https_scheme,
+        );
       }
 
       window
diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs
index 512f43c0aac0..7f7007eef558 100644
--- a/examples/api/src-tauri/src/lib.rs
+++ b/examples/api/src-tauri/src/lib.rs
@@ -66,7 +66,8 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
           .build()?,
       ));
 
-      let mut window_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
+      let mut window_builder =
+        WebviewWindowBuilder::new(app, "main", WebviewUrl::default()).use_https_scheme(true);
 
       #[cfg(all(desktop, not(test)))]
       {
diff --git a/packages/api/src/webview.ts b/packages/api/src/webview.ts
index e04252a95018..a495e135c2c2 100644
--- a/packages/api/src/webview.ts
+++ b/packages/api/src/webview.ts
@@ -734,6 +734,21 @@ interface WebviewOptions {
    * - **Android / iOS**: Unsupported.
    */
   zoomHotkeysEnabled?: boolean
+
+  /**
+   * Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
+   *
+   * #### Note
+   *
+   * Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
+   *
+   * #### Warning
+   *
+   * Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access them.
+   *
+   * @since 2.1.0
+   */
+  useHttpsScheme?: boolean
 }
 
 export { Webview, getCurrentWebview, getAllWebviews }


Test patch:
diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs
index 80c32e26d8ed..7f51b1902077 100644
--- a/crates/tauri/src/test/mock_runtime.rs
+++ b/crates/tauri/src/test/mock_runtime.rs
@@ -9,8 +9,10 @@ use tauri_runtime::{
   dpi::{PhysicalPosition, PhysicalSize, Position, Size},
   monitor::Monitor,
   webview::{DetachedWebview, PendingWebview},
-  window::{CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowEvent, WindowId},
-  window::{WindowBuilder, WindowBuilderBase},
+  window::{
+    CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowBuilder,
+    WindowBuilderBase, WindowEvent, WindowId,
+  },
   DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState,
   Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent,
   WebviewDispatch, WindowDispatch, WindowEventId,
@@ -158,14 +160,17 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
       },
     );
 
-    let webview = webview_id.map(|id| DetachedWebview {
-      label: pending.label.clone(),
-      dispatcher: MockWebviewDispatcher {
-        id,
-        context: self.context.clone(),
-        url: Arc::new(Mutex::new(pending.webview.unwrap().url)),
-        last_evaluated_script: Default::default(),
+    let webview = webview_id.map(|id| DetachedWindowWebview {
+      webview: DetachedWebview {
+        label: pending.label.clone(),
+        dispatcher: MockWebviewDispatcher {
+          id,
+          context: self.context.clone(),
+          url: Arc::new(Mutex::new(pending.webview.unwrap().url)),
+          last_evaluated_script: Default::default(),
+        },
       },
+      use_https_scheme: false,
     });
 
     Ok(DetachedWindow {
@@ -773,14 +778,17 @@ impl<T: UserEvent> WindowDispatch<T> for MockWindowDispatcher {
       },
     );
 
-    let webview = webview_id.map(|id| DetachedWebview {
-      label: pending.label.clone(),
-      dispatcher: MockWebviewDispatcher {
-        id,
-        context: self.context.clone(),
-        url: Arc::new(Mutex::new(pending.webview.unwrap().url)),
-        last_evaluated_script: Default::default(),
+    let webview = webview_id.map(|id| DetachedWindowWebview {
+      webview: DetachedWebview {
+        label: pending.label.clone(),
+        dispatcher: MockWebviewDispatcher {
+          id,
+          context: self.context.clone(),
+          url: Arc::new(Mutex::new(pending.webview.unwrap().url)),
+          last_evaluated_script: Default::default(),
+        },
       },
+      use_https_scheme: false,
     });
 
     Ok(DetachedWindow {
@@ -1065,14 +1073,17 @@ impl<T: UserEvent> Runtime<T> for MockRuntime {
       },
     );
 
-    let webview = webview_id.map(|id| DetachedWebview {
-      label: pending.label.clone(),
-      dispatcher: MockWebviewDispatcher {
-        id,
-        context: self.context.clone(),
-        url: Arc::new(Mutex::new(pending.webview.unwrap().url)),
-        last_evaluated_script: Default::default(),
+    let webview = webview_id.map(|id| DetachedWindowWebview {
+      webview: DetachedWebview {
+        label: pending.label.clone(),
+        dispatcher: MockWebviewDispatcher {
+          id,
+          context: self.context.clone(),
+          url: Arc::new(Mutex::new(pending.webview.unwrap().url)),
+          last_evaluated_script: Default::default(),
+        },
       },
+      use_https_scheme: false,
     });
 
     Ok(DetachedWindow {