# Desktop app

> The Investi desktop shell for macOS and Windows: what it loads, investi:// deep links, notifications, security, and current release status.

Source: https://investi.live/docs/integrations/desktop-app

The Investi desktop app is a Tauri 2 shell around the web app. It opens your Investi deployment in one native window and adds `investi://` deep links and native notifications. It has no research UI of its own. Signed releases are published for macOS and Windows, and [/downloads](/downloads) always links to the latest one.

## Status

| Item | State |
| --- | --- |
| macOS universal build (Apple silicon and Intel) | Published since 0.1.0 (November 2, 2026). Signed with a Developer ID, notarized by Apple and stapled. Requires macOS 11 or later. |
| Windows installer (x64) | Published since 0.1.0. Not code-signed yet, so Windows SmartScreen warns before it runs, and the downloads page says so. Requires Windows 10 or later. |
| Automatic updates | Release builds include the updater, which checks once at startup. Each release ships signed update files and a `latest.json` manifest. |
| Chrome Web Store listing | None. Install the extension in developer mode (see [Chrome extension](/docs/integrations/chrome-extension)). |

Releases are published to the public `VirSanghavi/investi-releases` repository with a `SHA256SUMS.txt` file, and only after a CI job has verified notarization and the update signatures. You can also keep using Investi in any modern browser; the web app is the same.

## Downloads page

[/downloads](/downloads) decides what to show at request time, from two inputs:

1. **The release status file** (`apps/desktop/release-status.json`), bundled into the web app at build time. For each platform it records the minimum OS, the architecture, whether builds are signed (and notarized, for macOS), an optional explicit artifact, and a file name pattern.
2. **The latest published GitHub release** of the releases repository named in that file, fetched from the GitHub API with a 4 second timeout and cached for 10 minutes.

A platform shows a download button only when one of these holds:

- The latest GitHub release is published (not a draft) and has an asset whose name matches the platform's pattern (`Investi_<version>_universal.dmg` for macOS, `Investi_<version>_x64-setup.exe` for Windows). Only `https://github.com/` download links are accepted. Releases are published only after a CI job verifies notarization.
- Or the status file names an explicit `https` artifact. For macOS that counts only when the file also marks the build signed and notarized.

Anything else, including a private repository, a 404, a timeout or malformed data, shows **Coming soon** with "No macOS build has been published yet" (or Windows). The status file sets `artifact` to `null` for both platforms, so the buttons come from the published GitHub release.

When a build is available, its card shows the file name, size and the start of its SHA-256 checksum, taken from the release's `SHA256SUMS.txt` or GitHub's own digest. A Windows build without code signing carries a notice that SmartScreen may warn, and to choose **More info**, then **Run anyway**.

The page also detects your OS and puts that card first. On a phone or tablet it says the desktop app needs a Mac or Windows computer and that the web app works in your phone's browser. Local development builds are never offered.

## Which URL it loads

The app resolves your Investi origin when it starts. First match wins:

1. `INVESTI_DESKTOP_URL` in the environment when the app starts
2. `INVESTI_DESKTOP_URL` set when the app was built
3. `http://localhost:3100`

Only `http` and `https` URLs with a host are accepted, and only the origin is kept; any path is dropped.

Before loading, the app opens a TCP connection to the host with a 1.5 second timeout. If it connects, the window opens on `/home`. If it does not, the app shows a bundled offline page with the URL, the error and a **Retry** button. An HTTP error from a reachable server still loads, and the web app renders it.

The window remembers its size and position. A second launch, or a deep link while the app is running, focuses the existing window instead of opening another.

| Menu item | Shortcut | Action |
| --- | --- | --- |
| Go, Reload | Cmd+R (Ctrl+R on Windows) | Recheck reachability and reload the current page |
| Go, Home | Cmd+Shift+H (Ctrl+Shift+H) | Open `/home` |
| Go, Open in Browser | Cmd+Shift+O (Ctrl+Shift+O) | Open the current page in your default browser |

**Back** and **Next** buttons sit in the app's top bar, left of the search box, on every screen (in the browser too). They are the page history: Back returns to the previous screen, Next goes forward again, and each is dimmed when there is nowhere to go.

Swipe to go back or forward, as in Chrome: a two-finger swipe left or right on a Mac trackpad (from 0.1.3), or a swipe from the screen edge on a Windows touch screen. It moves through the pages you visited in the app, never outside your Investi.

## Deep links

The app registers the `investi` URL scheme. A deep link becomes a path on your configured origin, so a link can never point the window at another site.

| Link | Opens |
| --- | --- |
| `investi://` or `investi://home` | `/home` |
| `investi://research/SPXC` | `/research/SPXC` |
| `investi://portfolio` | `/portfolio` |
| `investi://discover` | `/discover` |
| `investi://history` | `/history` |
| `investi://analytics` | `/analytics` |
| `investi://settings` | `/settings` |
| `investi://calendar` | `/calendar` |
| `investi://markets` | `/markets` |
| `investi://notebook` | `/notebook` |

The scheme and route names are case-insensitive, and a trailing slash is allowed. Research tickers are upper-cased and must be 1 to 10 characters: they start with a letter, then letters or digits, with single `.` or `-` separators (for example `BRK.B` or `BF-B`).

Everything else is rejected and logged:

- Unknown routes and extra path segments
- Empty segments, `.` and `..`, or any segment starting with `.`
- Any character other than ASCII letters, digits, `/`, `.` and `-`. This rejects `%` escapes, `?` queries, `#` fragments, `@`, `:`, backslashes, whitespace and non-ASCII text.
- Links longer than 128 characters

> **Note:** On macOS, the operating system hands the app a link that has already been parsed, which resolves dot segments first. `investi://research/../settings` therefore arrives as `investi://research/settings` and opens `/research/SETTINGS`. That is still an allowed route on your configured origin.

If your deployment is unreachable when a link arrives, the offline page shows, and **Retry** goes to the linked page. To try a link with the app running on macOS:

```bash
open 'investi://research/SPXC'
```

## Notifications for web pages

Pages on your configured origin get `window.__TAURI__`, so the web app can detect the desktop shell and send native notifications:

```ts
const tauri = (window as any).__TAURI__;
if (tauri?.notification) {
  const { isPermissionGranted, requestPermission, sendNotification } = tauri.notification;
  let granted = await isPermissionGranted();
  if (!granted) granted = (await requestPermission()) === "granted";
  if (granted) sendNotification({ title: "SPXC alert", body: "Crossed your 50-day moving average" });
} else if ("Notification" in window) {
  // regular browser path
}
```

Pages can also listen for deep links with `tauri.event.listen("deep-link://new-url", ...)` and read the link that launched the app with `tauri.deepLink.getCurrent()`. The shell already navigates the window for deep links, so the web app needs no code for them to work.

> **Note:** No page in the Investi web app calls the notification API yet, so the desktop app does not send notifications today. The API above is available for when it does.

## Security model

- **Remote pages get three abilities.** Your Investi origin can send notifications, listen for events, and read the launch deep link. It has no file system, shell, window control or other plugin access. This applies only to the main window and only to your exact configured origin.
- **The offline page is sandboxed.** The bundled fallback page can call only the shell's two commands (show connection info and retry), and the remote site cannot call them. It runs under a strict Content Security Policy: no inline script, and scripts, styles and fonts only from the app bundle.
- **Navigation stays on your origin.** The window loads only your configured origin and the bundled offline page. Other `http`, `https` and `mailto` links open in your default browser. Other schemes are blocked. `target=_blank` and `window.open` never create a second window: same-origin links open in the main window, others in your browser.
- **No broker credentials.** Schwab sign-in runs in the web app and opens in your system browser, because it is on another origin. The app stores only its window size and position, plus the webview's normal cookie store for your Investi session.
- **Read-only, like the web app.** The desktop app cannot place, change or cancel an order.

## Related

- [Chrome extension](/docs/integrations/chrome-extension)
- [Security and privacy](/docs/support/security-privacy)
- [Connect Schwab](/docs/getting-started/connect-schwab)
