Add the support widget
Mount the widget with the EIP-1193 provider your app already uses. By default a user can ask right away without signing anything. When your team needs proof, it asks the user to sign a SIWE message for your site. The widget does not reconnect their wallet and never treats an address passed by the page as proof.
To have an AI coding tool add it, copy the prompt below.
Configure your site
Create a workspace, open Widget and add your site's HTTPS origin. Only the manager can change the list. A workspace accepts up to ten origins. An origin contains the scheme and hostname, plus a port when needed, with no path.
The manager API is GET and POST /rooms/:workspaceId/widget. Writes include origins, minimumLevel or both, plus expectedPrincipalId and confirmed: true, and require recent authentication. Each field saves on its own. Keep the approved list limited to sites your protocol controls.
Choose who can ask
Who can start a request under Widget sets the lowest level the widget accepts:
| Level | The user | Staff see |
|---|---|---|
| Anyone (default) | Asks without a wallet | No wallet, or an unverified wallet your app reported |
| Connected wallet | Asks with a wallet connected in your app | Unverified wallet and its address |
| Verified wallet | Signs a SIWE message before asking | Verified wallet |
The API value is minimumLevel: anyone, connected or signed. A higher level also applies to open conversations. Users below it sign in to continue, and their thread comes with them.
You need the workspace UUID, its approved HTTPS origin and the wallet provider selected by the user. Check app status for support.widgetVerified and support.conversationsVerified, then check the workspace's coverage. A release flag does not grant a workspace coverage or staff access.
Mount the widget
Use your workspace ID and the provider returned by your app's wallet connector. Run this in the browser after the user has selected a wallet. Import the module when help is first opened; keep that instance in the application shell:
let support;
let loading;
let disposed = false;
const help = document.querySelector("#help");
const openHelp = async () => {
try {
loading ??= import("https://templ.vip/support-widget.js").catch(
(error) => {
loading = undefined;
throw error;
},
);
const { mountSupportWidget } = await loading;
if (disposed) return;
support ??= mountSupportWidget({
workspace: "YOUR_WORKSPACE_UUID",
provider: connectedWalletProvider,
apiUrl: "https://community.templ.vip",
label: "Support",
appearance: {
theme: "auto",
accent: "#7c5cff",
logo: "/logo-square.png",
},
prompts: ["My deposit hasn't arrived", "How do I withdraw?"],
showLauncher: false,
});
support.open();
} catch {
// The widget could not load. Tell the user and let them try again.
showSupportLoadError();
}
};
help.addEventListener("click", openHelp);
// Call on logout, provider/account change or application-shell unmount.
function disposeSupport() {
disposed = true;
help.removeEventListener("click", openHelp);
support?.destroy();
}
provider must implement the EIP-1193 request method. The object returned by mountSupportWidget has open(), close(), destroy() and setContext(context) methods.
appearance matches the widget to your app. theme is "dark" (the default), "light" or "auto", which follows the visitor's system setting. accent is a six-digit hex color for the launcher, buttons and the user's own messages; the text color on it is chosen for contrast. Any other value falls back to the defaults. Widget in your workspace previews both options and updates the snippet as you change them. logo is a square image for the widget's header: an HTTPS URL or a path on your own site. Without it, or if it fails to load, the header shows a chat icon.
prompts lists up to four common questions, each at most 80 characters. They appear as buttons before a user's first message, and a click sends the question as that message. After that the user's thread takes their place. When your workspace asks users to sign in first, they appear after sign-in.
Replies can contain links. The widget turns http and https addresses into links that open in a new tab, and it always shows the full address, even when the reply gives the link a label. Other addresses stay plain text.
showLauncher: false hides the floating button so your existing Help button opens the widget. Omit that option to show the floating button. Closing the widget returns focus to the button that opened it, if that button still exists.
For a single-page app, mount one instance in the application shell and reuse it across route changes. Its in-memory session and draft then survive navigation within that shell. A full reload still requires a new wallet proof. Destroy the instance when the shell unmounts, its provider changes or the app account changes. Mount a new instance with the current provider before the next sign-in. Forward the provider's account and chain events; do not keep a signed-in widget attached to an old account.
Your content security policy must allow https://templ.vip for script-src and https://community.templ.vip for connect-src. If you set logo, img-src must allow its origin. A policy that uses a nonce and strict-dynamic needs an authorized import script. The mounted widget uses an inline stylesheet inside its Shadow DOM. Pass your page's valid style nonce as the styleNonce option to authorize that stylesheet. If your policy cannot allow it, use the exported createSupportClient with your own interface and stylesheet.
A user can review optional context before sending it:
support.setContext({
network: "Ethereum",
transactionHash: transactionHash,
description: "The withdrawal is still pending.",
});
Call setContext after the instance exists. Pass only the issue details the user intends to share. Do not attach portfolio data, private staff notes or URL query strings containing secrets. To connect your own backend AI worker, follow Connect a support agent. Once the manager registers it as a bot, the widget labels its replies AI agent.
To open support from a Help button beside a stuck deposit, set its transaction in the click handler. It reaches your team only if the user ticks Share the context below with the team:
depositHelp.addEventListener("click", async () => {
await openHelp();
support?.setContext({ network: "Base", transactionHash: deposit.hash });
});
Add it with an AI coding tool
Open an AI coding tool in your app's repository and paste this prompt. It tells the tool to read this guide, reuse your wallet connection and ask you for anything it can't find, such as the workspace ID. Review its changes like any other pull request.
Add Templ support to this app using its existing wallet connection.
Read https://templ.vip/agents.md and the widget guide it links, and https://templ.vip/api/status. Inspect this codebase's wallet connector, routing, app session and content security policy before changing code.
Use the EIP-1193 provider selected by the app's wallet connector. Do not add a second wallet modal or substitute window.ethereum for the selected provider. Templ performs a separate SIWE proof when the user chooses to sign in to support. Never request a private key, send a transaction or treat the app's login token as a widget session.
Load https://templ.vip/support-widget.js in the browser and call mountSupportWidget with the workspace UUID, provider and apiUrl: "https://community.templ.vip". Match the app's design with appearance: { theme: "dark" | "light" | "auto", accent: "#rrggbb" }. Use showLauncher: false and open() from the app's Help button. Mount one instance in the app shell, reuse it across routes and call destroy() on shell teardown, provider replacement, app sign-out or a change away from the connected account. Allow an initial wallet connection to finish without destroying that same instance. Preserve keyboard focus and avoid covering the app's controls.
Before enabling live requests, confirm the workspace is listed at https://templ.vip/inbox (the owner creates one at https://templ.vip/create if there is none) and its manager has approved the exact HTTPS site origin in the Widget tab. Ask me for any missing workspace ID or setup decision. Do not invent IDs, grant yourself staff access or bypass an availability gate.
Follow the guide for CSP and optional draft storage. Send network, transaction hash or issue description only when the user chooses to share it. Keep secrets, tokens and private app data out of context.
Test a real member request and a staff reply in the workspace inbox, wallet rejection, account changes, route navigation, denied origins and a failed send followed by an explicit retry. Report which checks passed and any owner setup still needed.
Sign-in boundary
The widget asks for a fresh SIWE signature. It cannot reuse your app's SIWE message, session cookie or an address without proof. The server checks the approved origin, workspace, one-time challenge, message and signature.
Without a signature, the widget starts a guest session with the same origin, workspace and member-only limits. For a new visitor, it starts when they type a message or point at, tap or tab to one of the prompts buttons. Opening the widget alone stores nothing. The widget then keeps a random credential in the host website's local storage for up to 90 days so the same browser can return to its thread. If your app has a wallet connected, the widget sends that address as a claim. Staff see it as unverified, and it grants nothing. Each client can open three guest threads per workspace per day.
When staff ask a guest to verify, the widget shows a sign-in button in the thread. Signing in from that guest session moves the thread, with its history, to the wallet, and the guest credential stops working. If that wallet already has a thread in the workspace, both stay and your team sees them linked.
The initial widget supports externally owned EVM accounts on supported chains. Contract-wallet signatures fail closed in this widget flow; do not advertise smart-wallet support for it.
The widget stores its scoped bearer token in memory for up to one hour. The token is bound to the exact origin and workspace. It uses no third-party cookies and gains no staff permissions. After a reload, a guest resumes from its stored credential and a signed-in user signs again. Changing wallets ends a signed-in session. This differs from the first-party app's cookie session.
Before and after an authenticated request, the client reads the selected account and chain, even when the provider has no event API. Each passive wallet read times out after five seconds; a changed or unreadable wallet clears the session.
By default, unsent drafts and retry details are saved in the host website's local storage, separately for each origin, workspace and canonical account. Closing, destroying or signing out of the widget keeps the saved draft. Signing in to the same account restores it; a draft older than seven days is discarded on that restore attempt. A confirmed send or Discard removes an unsent message, and clearing the text box removes the draft. Stored fields include draft text, transaction hash, the unsent message with its retry ID and target thread, save time and a snapshot of any context approved for that message. Tokens, SIWE proofs and sent history are never saved there. The guest credential has its own entry, and a second entry keeps the ID of that guest's thread. Reading the thread still needs the guest's session.
A sent message shows in the thread right away. If delivery fails or is uncertain, it is marked Not sent. with Retry and Discard. Retry sends the same message, retry ID and approved context again. If a reply arrives but its approved context does not, the message is marked Details not shared., and Retry sends only the context. A rate limit of a minute or less is waited out once, with a countdown. The text box and context fields stay editable, but a new message waits until the unsent one is delivered or discarded. After reload and sign-in, the unsent message comes back with its approved context and is sent again only when the user chooses Retry. If a later read shows it arrived with nothing left to share, the widget marks it sent. Discarding removes it from this browser; it does not remove an already delivered message.
Set persistDrafts: false for memory-only drafts. If browser storage is unavailable, the draft also stays in memory and the widget says so. Host-site scripts can read local storage, including the guest credential, so include both in your privacy notice.
Check the integration
Send a request from the approved site and reply from the team inbox. Confirm that the same account can reopen its thread, another account cannot read it and internal notes remain hidden. Ask once without a wallet, choose Ask to verify wallet in the inbox and sign in from the widget; the thread should keep its history. Then test a rejected signature, an expired session, a wallet change and a site origin that was not approved.
If your team can't take a new request because the trial is over, this month's answered users are used up or paid months have run out, the widget tells the user "This team can't take new requests right now." Email notifications remain closed until delivery checks pass, so staff must check the inbox.
Templ's own support button
Templ's site and app use this same published widget for their own Help buttons, with the floating launcher hidden. It uses the wallet provider selected in the app, and signing in still asks for a separate SIWE proof. When the browser has no wallet, signing in offers to open the page in a wallet app instead.
Once Templ's own workspace passes its hosted checks, an AI agent answers first and Templ's team handles the rest. The agent's replies are labeled AI agent. Until then, or if that workspace's coverage runs out, the button opens an email to Templ's hello@ address instead. If the chat fails to load, the button offers Retry and Email Templ.