Skip to main content

Tauri

A Tauri app has a webview, but not a website: the chat runs as a Rust plugin that owns the conversation, and two custom elements that draw it in React, Vue, Svelte or no framework at all.

1. Install

Two packages, one version. tauri-plugin-helpwing handles HTTP, the visitor token, the offline queue and polling; @helpwing/tauri is the bindings and the elements. Requires Tauri 2. Source at helpwing/helpwing-tauri-sdk.

bash
# in src-tauri/
cargo add tauri-plugin-helpwing

# in your frontend
npm install @helpwing/tauri

2. Register the plugin

rust
// src-tauri/src/lib.rs
tauri::Builder::default()
    .plugin(tauri_plugin_helpwing::Builder::new("https://api.helpwing.app", "pk_your_project_key").build())
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

The key is the same public pk_… the website snippet carries, and shipping it inside an app bundle is expected. The API URL is whichever host serves /widget.js. Then allow the plugin's commands in the capability your window uses:

json
// src-tauri/capabilities/default.json
{
  "identifier": "default",
  "windows": ["main"],
  "permissions": ["core:default", "helpwing:default"]
}

Why the requests go out from Rust

The widget API checks every request's origin against the project's allowed origins list, and a Tauri webview reports tauri://localhost, which no project lists. The plugin sends every request from Rust with no origin at all, the way a mobile SDK does — so there is nothing to add to the allow list.

3. Put the launcher on the page

ts
// main.ts
import { defineHelpwingElements, HelpwingChat } from '@helpwing/tauri'

defineHelpwingElements()
void HelpwingChat.shared().start()
html
<!-- The floating button, and the panel it opens. -->
<helpwing-launcher></helpwing-launcher>

start() loads the project's settings and whatever conversation is already stored, and sends the webview's language and time zone unless you pass { locale, timezone }. The elements call it for you if you don't.

4. Or draw it yourself

An app with a Help item in its own menu puts <helpwing-chat> on a page of its own. Mount it only while it is on screen: being connected is what tells us the visitor is reading.

html
<!-- A page of your own: fills its container, marks the visitor as reading while connected. -->
<helpwing-chat locale="fr"></helpwing-chat>

Or skip the elements entirely. The client carries state, send, retry, identify, refresh, markRead, reset and claimPresence; from Rust, app.helpwing() returns the same conversation.

ts
import { HelpwingChat } from '@helpwing/tauri'

const chat = HelpwingChat.shared()

// Called immediately, then on every change. Returns the unsubscribe.
const stop = chat.subscribe(state => {
  badge.textContent = state.unreadCount ? String(state.unreadCount) : ''
})

5. Say who the visitor is

ts
// At startup, and whenever sign-in changes.
await HelpwingChat.shared().identify(
  user
    ? {
        id: user.id,
        email: user.email,
        name: user.name,
        // Computed by YOUR server. See below.
        userHash: user.supportHash,
      }
    : null,
)

It applies to a conversation that started before anyone signed in too — the conversation moves onto their customer record. A conversation belongs to whoever opened it: identify somebody else and the stored one is dropped, and they start their own.

The identity secret never goes in the app

userHash is an HMAC-SHA256 of the user id, keyed with your project's identity secret, and your server computes it. A desktop bundle is an archive with your code in it, Rust included — a secret compiled into the binary is one strings away. Fetch the hash from your own API alongside the rest of the signed-in user. Identifying users has the full recipe.

ts
// Signing out on a personal machine: the next conversation is nobody's yet.
await HelpwingChat.shared().identify(null)

// Signing out on a shared machine: forget this conversation entirely.
await HelpwingChat.shared().reset()

Links and pictures

Agents reply in markdown, and the elements draw it as DOM nodes — never innerHTML, so nothing anybody typed can become markup. Links open in the system browser through the plugin, and only http, https, mailto and tel. On iOS and Android Tauri has no default opener, so pass one:

ts
import { openUrl } from '@tauri-apps/plugin-opener'
import { HelpwingChat } from '@helpwing/tauri'

// Only http, https, mailto and tel links ever reach it.
HelpwingChat.shared().setLinkOpener(openUrl)

A picture pasted into an email is drawn where it was written, from a signed URL on your API host. If your CSP restricts img-src, allow that host.

What it does about a desktop app

  • It asks every five seconds while a chat is on screen, every thirty while none is, and not at all while the window is hidden. Both intervals are set on the Builder.
  • A message sent twice is stored once. Every send carries an id, so a lost connection is retried without duplicates. Opening a conversation is the one exception — a start whose answer was lost is shown as a failed send, and retrying it is the visitor's decision.
  • Unsent messages survive the app being quit. They are written to the app data directory and go out on the next launch, in the order they were written.
  • A reply is not emailed to somebody who is reading it. While a chat element is connected the plugin says so; close it and the same reply is emailed as well, as long as the visitor gave an address.

What it does not do yet

  • Notifications. Nothing arrives while the app is closed. Ask for an email address — require_email on the widget settings screen — and a reply written while the visitor is away reaches them there.
  • Attachments from the visitor. No client can send one yet. Files an agent attaches are listed by name.
  • One conversation across two machines. The token belongs to one installation.

Checklist

  • Grant helpwing:default. Without it every command is refused, and the chat never loads.
  • Mount the chat only while it is visible. A hidden but connected element claims the visitor is reading, and replies stop being emailed.
  • The hash comes from your server. Everything in the bundle is public, the Rust half included.
  • Ask for an email address. It is the only way to reach somebody who has closed the app.
NextUnity →A UPM package with a chat built in UI Toolkit, for games and apps on every Unity platform.