ahize

One unified API for 18 live-chat & customer-support widgets. Zero deps, tree-shakeable, SSR-safe, CSP-aware, strict TypeScript.

10
0
10
4
TypeScript


ahize

ahize

One unified API for 18 live-chat & customer-support widgets.
Zero dependencies. Tree-shakeable. SSR-safe. Strict TypeScript.

npm version npm downloads bundle size license sponsors

Why

Every live-chat vendor ships its own snippet, its own globals, its own
quirks. Wrappers exist for individual ones (react-use-intercom,
react-zendesk, tawk-messenger-react, …) — but each pulls in a
framework, locks you to one provider, and re-introduces the bugs the
underlying snippet already had: SSR crashes, calls dropped before boot,
HMAC fields silently missing, no shutdown→boot reversibility.

ahize is one zero-dependency layer over all of them. Same surface,
swap providers by changing the import path.
Pre-boot calls are
buffered. Identity verification is typed. SSR is a no-op. CSP is
documented per provider.

Today you’re on Intercom:

import * as chat from "ahize/intercom"

await chat.load({ appId: "abc" })
chat.identify({ id: "u1", email: "ada@example.com" })
chat.track("plan_upgraded", { tier: "pro" })
chat.show()

Tomorrow you switch to Crisp — only the import path and the load()
options change:

import * as chat from "ahize/crisp"

await chat.load({ websiteId: "uuid" })
chat.identify({ id: "u1", email: "ada@example.com" })
chat.track("plan_upgraded", { tier: "pro" })
chat.show()

Install

npm install ahize
# pnpm add ahize / yarn add ahize / bun add ahize

Hello, world

import { load, identify, track, show } from "ahize/intercom"

await load({ appId: "abc123" })
await identify({
  id: "user_1",
  email: "ada@example.com",
  name: "Ada Lovelace",
})
await track("plan_upgraded", { tier: "pro" })
await show()

That’s it. The Intercom CDN is injected, boot is fired, and your
identify/track/show calls were buffered and drained in order.

The unified surface

Every provider exports the exact same functions:

load(options); // inject CDN & boot — Promise resolves when widget API attaches
identify(identity); // set user; verification: hmac | jwt | callback
track(event, metadata?); // emit a custom event; generic <T extends EventMetadata>
pageView({ path, locale }); // notify on SPA route change
show();
hide(); // toggle widget visibility
shutdown(); // end session — keeps config so you can re-identify
destroy(); // hard reset — removes script, globals, listeners
ready(); // Promise<void> resolved once the real API is live
isReady();
state(); // synchronous lifecycle ('idle'|'loading'|'ready'|'shutdown')
getIdentity();
onIdentityChange(cb); // typed identity transitions

If you call any method before load() resolves, the call is queued and
flushed in order once the provider is ready. No more “Intercom is not
defined” warnings.

On top of the unified surface, most providers expose a typed
on(event, handler) for their documented lifecycle events (widget
open/close, message sent/received, conversation started, unread count,
…) and a handful of vendor-native methods — see the Providers
table for the per-provider extras.

Identity verification

Most providers want a server-issued HMAC or JWT to prevent users from
spoofing each other’s profiles. ahize makes it a typed required field:

// Intercom — HMAC of user_id with your app secret
await identify({
  id: "user_1",
  email: "ada@example.com",
  verification: { kind: "hmac", hash: process.env.INTERCOM_USER_HASH! },
})

// HubSpot — JWT
await identify({
  id: "user_1",
  email: "ada@example.com",
  verification: { kind: "jwt", token: serverIssuedJwt },
})

// Zendesk Messenger — callback for token refresh on 401
await identify({
  id: "user_1",
  verification: {
    kind: "callback",
    getToken: async () => fetchFreshJwtFromServer(),
  },
})

Pass the wrong kind for a provider and you get a typed rejection — no
silent drop. See ahize/capabilities for who supports what.

SSR

ahize is safe to import from any server runtime. Every method
short-circuits when window/document are unavailable, so this works
in Next.js App Router, Nuxt 4, Remix, SvelteKit, Astro, and Cloudflare
Workers without guards.

// app/layout.tsx — no "use client" needed for the import itself
import { load } from "ahize/intercom"

await load({ appId: "..." }) // resolves to undefined on the server, no-op

If you want to be belt-and-braces sure no DOM code enters your SSR
bundle, import the matching no-op stub:

import { load, identify, track } from "ahize/server"

Framework adapters

Each adapter is framework-agnostic — bring your own React/Vue/Angular,
no peer dependencies.

Next.js (App Router)

"use client"
import * as React from "react"
import * as intercom from "ahize/intercom"
import { createAhizeComponent } from "ahize/next"
import { usePathname, useSearchParams } from "next/navigation"

const Ahize = createAhizeComponent(React, { usePathname, useSearchParams })

export default function ChatBoot() {
  return <Ahize provider={intercom} options={{ appId: "abc123" }} autoPageView />
}

Mount once in your root layout. pageView() auto-fires on every route
change — fixes HubSpot’s targeting rules, keeps Intercom’s session
tracking accurate.

Nuxt 4 (and Nuxt 3)

// app/plugins/ahize.client.ts   (Nuxt 4 default srcDir)
// plugins/ahize.client.ts       (Nuxt 3, or Nuxt 4 with custom srcDir)
import { defineNuxtPlugin } from "#app"
import * as intercom from "ahize/intercom"
import { createNuxtAhizePlugin } from "ahize/nuxt"

export default defineNuxtPlugin(
  createNuxtAhizePlugin({
    provider: intercom,
    options: { appId: "abc123" },
    autoPageView: true,
  }),
)

The plugin & defineNuxtPlugin API is identical between Nuxt 3 and 4 —
only the default source directory changed (app/ in Nuxt 4). Use
$ahize from useNuxtApp() to access the provider in components.

React (any meta-framework)

import * as React from "react"
import * as crisp from "ahize/crisp"
import { createUseAhize } from "ahize/react"

const useAhize = createUseAhize(React)

function App() {
  const { isReady, identify, show } = useAhize({
    provider: crisp,
    options: { websiteId: "..." },
  })

  return (
    <button disabled={!isReady} onClick={() => show()}>
      Open chat
    </button>
  )
}

Vue 3, Svelte, SvelteKit, Remix, Astro, Angular

All shipped — see ahize/vue, ahize/svelte, ahize/sveltekit,
ahize/remix, ahize/astro, ahize/angular. Same factory pattern:
pass the framework primitives in.

load() never fires on import. Pair it with your CMP:

import * as intercom from "ahize/intercom"

OneTrust.OnConsentChanged(() => {
  if (OnetrustActiveGroups.includes("C0004")) {
    intercom.load({ appId: "abc123" })
  } else {
    intercom.destroy() // removes script, globals, cookies
  }
})

Defer strategies for LCP-sensitive pages:

load({ appId: "...", defer: "idle" }) // requestIdleCallback
load({ appId: "...", defer: "interaction" }) // first pointerdown/scroll/keydown
load({ appId: "...", defer: "manual" }) // never auto-injects
load({ appId: "...", consent: hasConsent }) // gate behind a flag

EU region selection is built in:

intercom.load({ appId: "...", region: "eu" }) // → api-iam.eu.intercom.io
hubspot.load({ portalId: "...", region: "eu1" }) // → js-eu1.hs-scripts.com

CSP

Strict CSP breaks every chat widget by default. ahize/csp ships the
exact directive list per provider, including the WSS endpoints
competitor wrappers always forget:

import { cspDirectives, mergeCsp, toHeaderString } from "ahize/csp"

const policy = mergeCsp(
  cspDirectives("intercom"),
  cspDirectives("crisp"),
  cspDirectives("chatwoot", { chatwootBaseUrl: "https://chat.acme.com" }),
)

response.setHeader("Content-Security-Policy", toHeaderString(policy))

Pass a nonce through load() and the same nonce through your CSP:

load({ appId: "...", nonce: cspNonce })

Want to catch violations in dev?

import { watchCspViolations } from "ahize/csp"

watchCspViolations((event) => {
  console.warn("CSP blocked", event.blockedURI, "for", event.violatedDirective)
})

Facade mode

For pages where chat is below the fold and LCP matters, mount a tiny
launcher. The real provider boots on hover or click:

import * as intercom from "ahize/intercom"
import { mountFacade } from "ahize/facade"

mountFacade({
  provider: "intercom",
  boot: () => intercom.load({ appId: "abc123" }),
})

Under 2 KB. No CSS file, no framework. The launcher removes itself once
the real widget is ready.

Switching providers

Capability matrix is queryable so you don’t hard-code branches:

import { capabilities, supports } from "ahize/capabilities"

if (supports("zendesk", "callback")) {
  await identify({ id: "u1", verification: { kind: "callback", getToken } })
} else if (supports("zendesk", "jwt")) {
  await identify({ id: "u1", verification: { kind: "jwt", token } })
}

Diagnostics

When the snippet refuses to load, ahize/diagnostics probes the CDN and
returns a hint:

import { diagnose } from "ahize/diagnostics"

const result = await diagnose("intercom", { appId: "abc123" })
//   { ok: false, status: 404, hint: "Snippet not found — typo in id…" }

Providers

Every provider ships the unified surface (load / identify / track /
pageView / show / hide / shutdown / destroy / ready / isReady /
state / getIdentity / onIdentityChange) plus a provider-specific
extension: on(event, handler) typed event bridge, and vendor-native
methods where they exist. Audited against live vendor docs 2026-04-16.

Provider Sub-path Identity / regions Provider-specific extras
Intercom ahize/intercom HMAC, JWT, us/eu/au showSpace, showNewMessage, startTour/Survey/Checklist, showArticle/News/Ticket, getVisitorId, onShow/onHide/onUserEmailSupplied, 11 typed boot fields
Crisp ahize/crisp HMAC, hot-reconfigure open/close/toggle, sendMessage, helpdeskSearch, setSessionSegments, setUserAvatar, 13 events, runtime config
Tawk.to ahize/tawk HMAC, login() restores history maximize/minimize/popup, addTags/removeTags, getStatus/isChat*, visitor preload, 19 event hooks
Zendesk ahize/zendesk JWT, callback open/close, setConversationTags, setCustomization, newConversation, resetWidget, 13 messenger:on events, cookies/zIndex config
Chatwoot ahize/chatwoot HMAC, self-hosted, settings setColorScheme, deleteAttribute, popoutChatWindow, setLocale, setBubbleVisibility, on(opened/closed/postback/…), 11 typed settings
HubSpot ahize/hubspot Identification token, na1/eu1/ap1 on(conversationStarted/…) (8 events), status(), refresh, 7 typed config (cookie banner, inline embed, attachment, CSP)
LiveChat ahize/livechat maximize(draft?), minimize, hideGreeting, triggerSalesTracker, getState/CustomerData/ChatData, 10 events, 7 typed __lc fields
Freshchat ahize/freshchat JWT, us/eu/in/au open/close, setLocale, setTags/setFaqTags, setBotVariables, trackPage, isOpen/isLoaded, 16 events, 8 typed init fields
Olark ahize/olark getVisitorDetails(), sendMessage/NotificationTo*, setOperatorGroup, setLocale, 12 events, group/locale boot config
HelpScout ahize/helpscout HMAC search, article, sessionData, config, reset, toggle, askQuestion, showMessage, info, once, prefill(attachments), full BeaconConfig object
LiveAgent ahize/liveagent self-hosted opt addUserDetail, addTicketField, setVisitorLocation, createForm, hasOpenedWidget, on(chatStarted/chatEnded/online/offline)
Gist ahize/gist HMAC open/close, showLauncher/hideLauncher, navigate, showArticle, trigger, setSidebar/setStandard, 12 events
JivoChat ahize/jivochat setUserToken (verification) setClientAttributes (rate-limited), setCustomData, startCall, sendOfflineMessage, sendPageTitle, 12 events, sync chatMode/getUnreadMessagesCount/getUtm
Smartsupp ahize/smartsupp open/close, prefillMessage, sendMessage, setGroup, setLanguage, getVisitorId, on(messageSent/Received/messengerClose), 12 typed _smartsupp fields
Tidio ahize/tidio tidioChatApi.track forwarding, setColorPalette, display, messageFromOperator/Visitor, addVisitorTags, setVisitorCurrency, 10 events, pre-load language/identify

Deprecated / sunset providers

Still functional but the underlying vendor has announced sunset / EOL.
Wrappers emit a one-shot console.warn on first load() and are marked
@deprecated in JSDoc. No new feature work planned.

Provider Sub-path Status
Drift ahize/drift Vendor sunset announced 2026-03-06 (Clari + Salesloft).
Sendbird ahize/sendbird AI Chatbot Widget discontinued; repo archived 2025-07-09 at v1.9.7. Consider Sendbird Desk.
Userlike ahize/userlike v1 CDN EOL 2026-08-01. Vendor rebranded to Lime Connect; v2 is @userlike/messenger with a different surface.
Zendesk Classic ahize/zendesk-classic Limited to Zendesk accounts created before 2023-06-05. Chat Web SDK removal started 2025-04-30. Use ahize/zendesk (Messenger).

Migrating

From react-use-intercom:

-import { IntercomProvider, useIntercom } from "react-use-intercom";
+import * as intercom from "ahize/intercom";
+import { createUseAhize } from "ahize/react";
+import * as React from "react";
+const useAhize = createUseAhize(React);

-<IntercomProvider appId="abc">{children}</IntercomProvider>
+const { isReady, identify, show, hide, shutdown } = useAhize({
+  provider: intercom,
+  options: { appId: "abc" },
+});
react-use-intercom ahize
boot(props) load({ appId, ...props })
update(props) identify(props)
trackEvent(name, meta) track(name, meta)
shutdown() shutdown()
hardShutdown() destroy()
boot({ user_hash }) identify({ verification: { kind: "hmac", hash } })
boot({ intercom_user_jwt }) identify({ verification: { kind: "jwt", token } })

Same shape works for react-zendesk, tawk-messenger-react,
@productdevbook/chatwoot, @livechat/widget-react. The notable change
across all of them: ahize separates boot (load) from user
identity
(identify).

Playground

A plain Vite + TypeScript playground is checked into playground/ for
trying a real widget in the browser without setting up a framework.

pnpm playground

That installs the playground’s own deps (vite, typescript) and opens the
dev server on http://localhost:5173. It imports the Chatwoot provider
directly from ../src/providers/chatwoot.ts, so any code change in
src/ reloads instantly — no build step. Paste your websiteToken (and
baseUrl if self-hosted), hit load(), then play with
identify/track/show/hide/setLocale and watch the event log.

Sponsors

Sponsors of productdevbook

If ahize saves you a few hours of live-chat integration pain, consider
sponsoring on GitHub so
work on the next provider + framework adapter keeps going.

Contributing

Issues and PRs welcome at
github.com/productdevbook/ahize.
Missing a provider? The pattern is small enough to copy from any
existing one — src/providers/livechat.ts is a good minimal template.

Credits

ahize exists because every one of these libraries solved part of the
problem and showed us the bugs to design around. Many of the design
decisions in ahize are direct responses to issues filed on these
projects — thank you to every maintainer and reporter.

undefinedIntercomundefined

undefinedCrispundefined

  • crisp-im/crisp-sdk-web — official wrapper; our $crisp.push-only contract comes from issues filed against it

undefinedTawk.toundefined

undefinedZendeskundefined

undefinedHubSpotundefined

undefinedChatwootundefined

undefinedPerformance & deferred loadundefined

undefinedLiveChat (text.com)undefined

undefinedOther providersundefined

  • userlike/messenger — Result<ok, err> pattern we propagate
  • HelpScout Beacon, Drift, Freshchat, Olark, Smartsupp, LiveAgent, Gist, JivoChat, Tidio, Sendbird — official docs & community wrappers

undefinedOther unified-chat workundefined

If your library should be on this list and isn’t,
open an issue — happy
to credit.

License

MIT © productdevbook

[beta]v0.14.0