npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@paramms/chat-widget

v1.9.8

Published

Embeddable real-time chat widget for the Relay platform

Downloads

984

Readme

@paramms/chat-widget

Real-time embeddable chat widget for the Relay platform. Drop into any website with a single script tag — no build step required.

Install

npm install @paramms/chat-widget

Quick start — CDN, ESM mount() (no npm, no build, manual container)

This is the ESM path — you control the mount point yourself. For a floating bubble on any site with zero JavaScript (or a config object), see Script tag below instead — it's usually simpler unless you specifically need to place the widget inline in your own container (data-relay-*/Relay('boot', ...) don't support choosing a container element; this ESM form does, via el).

<div id="chat"></div>
<script type="module">
  import { mount } from 'https://relay.paramms.com/index.js'
  mount({
    el:        document.getElementById('chat'),
    url:       'https://api.relay.paramms.com',   // ONE url, any scheme — ws + REST derived
    profileId: 'YOUR_PROFILE_ID',
  })
</script>

React / Next.js

General support chat (ChatWidget)

'use client'
import { ChatWidget } from '@paramms/chat-widget/react'

export default function SupportPage({ session }) {
  return (
    <ChatWidget
      url={process.env.NEXT_PUBLIC_RELAY_URL}
      profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}
      userId={session?.user.id}        // optional — anonymous if omitted
      userName={session?.user.name}    // optional — shown to agents
      userEmail={session?.user.email}  // optional — shown to agents
    />
  )
}

Marketplace / multi-listing chat (MarketplaceChat)

Two modes — one component:

On a listing page (with listingId) → opens that item's chat directly. No list. Buyer is already looking at the item.

'use client'
import { MarketplaceChat } from '@paramms/chat-widget/react'

// Listing detail page — floating bubble in bottom-right corner
export default function ListingPage({ car, session }) {
  return (
    <>
      <YourPageContent />

      <MarketplaceChat
        url={process.env.NEXT_PUBLIC_RELAY_URL}
        profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}
        listingId={car.id}
        listingTitle={car.title}          // shown in chat header
        listingPrice={car.price}          // shown as "$12,500" tag
        listingMeta="214,500 km · LPG"   // one line of detail
        listingStatus="Available"
        userId={session?.user.id}         // optional — anonymous if omitted
        userName={session?.user.name}     // optional
        launcher                          // floating bubble button
        position="bottom-right"
      />
    </>
  )
}

Without listingId → opens the buyer's single general (non-listing) thread.

For a /messages inbox page (WhatsApp-style thread list, tap to open, ✎ to start a new chat) use ChatApp — by default it lists every conversation the user has with your business, across all your chatrooms, and opens each against its own chatroom (scope="tenant"; pass scope="profile" for one chatroom only). It's a single-pane stack, like a native messaging app: the list fills the surface, tapping a row pushes the chatroom over it, and the chatroom's back chevron (‹) returns to the list — the same on desktop, in a launcher panel, and on mobile. The list updates live over the socket (a subscribe_inbox stream keyed to the user), so new messages and threads appear without a refresh.

Add an inbox to a single-thread widget. Pass inbox to ChatWidget or MarketplaceChat to add a back chevron in the chatroom header that opens the full ChatApp list (and the list's ✕ returns to the thread). inboxScope="tenant" (default) lists the user's threads across all chatrooms; "profile" limits it to the current one:

<MarketplaceChat url={RELAY} profileId={PID} listingId={car.id} listingTitle={car.title} inbox />
// Dedicated inbox page — e.g. /messages
import { ChatApp } from '@paramms/chat-widget/react'

export default function MessagesPage({ session }) {
  return (
    <div style={{ height: '600px' }}>
      <ChatApp
        url={process.env.NEXT_PUBLIC_RELAY_URL}
        tenantId={process.env.NEXT_PUBLIC_RELAY_TENANT_ID}  // or profileId={…} — either works
        userId={session?.user.id}
        userName={session?.user.name}
      />
    </div>
  )
}

Prefer a floating bubble that opens the same app in a panel? <ChatAppLauncher … floating />.

ChatAppLauncher (both floating and inline) shows an always-visible close (✕) in the panel's top-right, so it dismisses reliably on mobile too (where a fullscreen panel has no Escape key or reachable backdrop). If you embed a bare <ChatApp /> inside your own overlay, pass onClose={() => …} to get the same close control; omit it for a plain inline embed you chrome yourself.

Multi-tenancy note: conversations are created and keyed under the tenant the server resolves from profileId — guests can't spoof it. For reading your own inbox, ChatApp/ChatAppLauncher (and useRelayChatList/mountChatList/GET /conversations/mine) also accept a tenantId directly: it lists every conversation the user has with that business across all of its chatrooms, and is equally safe because the list is always self-scoped to the caller's own identity. scope="tenant" / tenantId never crosses into another business's data.

Script tag (any website — plain HTML, WordPress, Shopify, PHP/Rails/Django/Laravel, or literally anything)

This is the path for every site that isn't React: one <script> tag, no build step, no framework. It works two ways — pick based on how much JavaScript you're willing to write:

  • Zero-JavaScript — data-relay-* attributes right on the <script> tag. This is the one that matters most for a plain-HTML site, a WordPress block, or a no-code builder, because it's the only option when there's no JavaScript on the page at all — a server template just fills in attribute values.
  • Full config — a JS object (window.relaySettings = {...} or Relay('boot', {...}), same shape either way). Needed the moment you want something an HTML attribute can't hold: a nested object, an array, a function, or changing the widget's identity after the page has loaded (e.g. once a visitor logs in).

Both configure the exact same widget with the exact same field names as the React components above — profileId, userName/userEmail/userAvatar, contextTitle/contextSubtitle/contextStatus (or listingTitle/listingMeta/listingPrice/listingStatus for a marketplace-style card), accent, launcher, position, launcherMessage. If you already know the React props, you already know these.

Zero-JavaScript — data-relay-* attributes

<script
  async
  src="https://relay.paramms.com/embed.js"
  data-relay-app="YOUR_PROFILE_ID"
  data-relay-user="user_123"
  data-relay-user-name="Jane Doe"
  data-relay-user-email="[email protected]"
  data-relay-accent="#6c5ce7"
  data-relay-position="bottom-right"
></script>

Drop that before </body> and you have a working floating bubble — no other JavaScript needed anywhere on the page. Every attribute is optional except data-relay-app.

| Attribute | Matches field | Notes | |---|---|---| | data-relay-app | profileId | Required. data-relay-profile-id works identically — same thing, matches the React prop name | | data-relay-user | userId | Unauthenticated stable id. Omit for an anonymous guest | | data-relay-token | token | Signed ES256 JWT — the production identity tier | | data-relay-user-name | userName | Shown to agents | | data-relay-user-email | userEmail | Shown to agents; also powers the offline email fallback | | data-relay-user-avatar | userAvatar | Shown to agents | | data-relay-listing | listingId | Sugar for subjectId: "listing_<id>" | | data-relay-context-title / -subtitle / -status | contextTitle / contextSubtitle / contextStatus | Context card (general — an order, ticket, booking) | | data-relay-listing-title / -meta / -price / -status | listingTitle / listingMeta / listingPrice / listingStatus | Marketplace card (a specific item — price + status badge). Use these OR the context-* set, both build the same card | | data-relay-accent | accent | Brand colour hex | | data-relay-url | url | Only needed for self-hosted Relay | | data-relay-position | position | bottom-right | bottom-left | | data-relay-launcher | launcher | Set to "false" for an inline (non-floating) widget | | data-relay-launcher-message / data-relay-launcher-subtitle | launcherMessage | Two flat attributes combine into the teaser card — see note below | | data-relay-target | el | CSS selector for an existing element to mount into. Ignored in launcher mode | | data-relay-height | height | Inline container height. Ignored in launcher mode | | data-relay-inbox | inbox | "true" adds a back-chevron → full conversation list. Requires data-relay-launcher="false" — see the note below the table | | data-relay-inbox-start | inboxStart | "true" opens directly on the conversation list instead of a single thread — for a dedicated "Messages" page. Implies inbox. Requires data-relay-launcher="false" | | data-relay-inbox-scope | inboxScope | tenant (default) | profile | | data-relay-translate-lang | translateLang | ISO code (e.g. es, ja) to auto-translate incoming messages into — shows a 🌐 button per message | | data-relay-tenant-id | tenantId | Your business id. Lists threads across all your chatrooms without naming one — use instead of data-relay-app on an inbox page |

What's NOT available as an attribute: user/subject as nested objects, quickReplies, i18n, and refreshToken — an HTML attribute can only hold a string, so these need the JS-object form below. (launcherMessage's two attributes are a workaround for exactly this: the React prop takes one { title, subtitle } object, which an attribute can't express, so it's split into two flat attributes that get recombined.)

inbox requires data-relay-launcher="false" — same limit React has (a launcher+inbox combination needs a different component there, ChatAppLauncher); with the default floating launcher, data-relay-inbox is silently ignored.

A whole "Messages" page, zero JavaScript. data-relay-inbox alone only puts the list behind a back-chevron — the widget still lands on one thread, which is right for a product page and wrong for a page whose entire job is the inbox. data-relay-inbox-start makes the list the landing view, and data-relay-tenant-id spans every chatroom you run:

<div id="relay-inbox" style="height:600px"></div>

<script
  async
  src="https://relay.paramms.com/embed.js"
  data-relay-tenant-id="acme"
  data-relay-inbox-start="true"
  data-relay-launcher="false"
  data-relay-target="#relay-inbox"
  data-relay-user="user_123"
></script>

Tapping a row opens that conversation against its own chatroom; its back-chevron returns to the list. There's no ✕ in this mode — the list is the root view, so there'd be nothing behind it to close to. ✎ compose opens against your default chatroom (the server reports which). This is the script-tag equivalent of React's <ChatApp tenantId={...} />.

Full config — JS object

Same field names, all in one place, same object whether it's set on page load or reacts to something happening later:

<script async src="https://relay.paramms.com/embed.js"></script>
<script>
  window.relaySettings = {
    profileId:        'YOUR_PROFILE_ID',   // required — your chatroom id
    url:               undefined,          // optional — only for self-hosted Relay
    apiUrl:            undefined,          // optional — only if REST lives on a different origin than the socket

    token:             undefined,          // optional — signed ES256 JWT, production identity (wins over userId)
    userId:            'user_123',         // optional — unauthenticated stable id; omit both → anonymous guest
    refreshToken:      () => fetchNewToken(),  // optional — called when a signed token expires

    userName:          'Jane Doe',              // optional — shown to agents, not identity
    userEmail:         '[email protected]',      // optional — also powers the offline email fallback
    userAvatar:        'https://…/jane.png',    // optional
    // or, if it's easier to build one object: user: { name, email, avatar }

    subjectId:         undefined,          // optional — pins a dedicated thread; usually just use listingId below
    listingId:         '4821',             // optional — sugar for subjectId: "listing_4821"
    contextTitle:       'Order #4821',              // optional — context card title (general use)
    contextSubtitle:    'Placed Mar 3 · $129.00',   // optional — context card subtitle
    contextStatus:       'Shipped',                  // optional — status badge
    // or, for a marketplace-style card: listingTitle / listingMeta / listingPrice / listingStatus
    // or build the card yourself:        subject: { title, subtitle, tags, status }

    accent:            '#6c5ce7',           // optional — brand colour
    launcher:           true,               // optional — floating bubble vs inline; default true
    position:          'bottom-right',      // optional — 'bottom-right' | 'bottom-left'
    launcherMessage:   { title: 'Questions? Chat with us', subtitle: 'Start a conversation' },  // optional — a bare string also works (title only)

    quickReplies:      ['Track my order', 'Return an item'],   // optional — reply chips above the input
    i18n:              { send: 'Enviar' },                      // optional — UI string overrides
    translateLang:      undefined,          // optional — auto-translate incoming messages (ISO code)

    // Inline placement (ignored in launcher mode — a floating bubble is a
    // fixed popup mount() owns, not something placed at a point in the page):
    el:                 '#chat',            // optional — CSS selector or element; omit → an auto-created host
    height:             '600px',            // optional — inline container height
    inbox:              false,              // optional — back-chevron → full conversation list (inline only, see below)
    inboxScope:        'tenant',            // optional — 'tenant' (default, all your chatrooms) | 'profile' (this one only)
  }
</script>

Relay('boot', {...}) (below) takes this exact same object — use whichever form fits how the page is built.

inbox matches React exactly, including its one limit: it only works with launcher: false. A floating launcher bubble is a fixed-size popup the widget owns; React itself needs a different component (ChatAppLauncher) for a launcher-with-inbox experience, and the script-tag path draws the same line. With inbox: true and launcher: false, the widget's header shows a back-chevron; tapping it swaps to the full conversation list (reusing the same list engine the dashboard's own inbox runs on); tapping a row opens that conversation; a ✕ in the list view returns to this widget's original thread.

Commands — the same object, applied later

For identity that arrives after the page loads (a visitor logs in), or SPA-style navigation between pages without a full reload, call Relay(...) directly instead of (or in addition to) setting window.relaySettings. Same field names as above — Relay('boot'/'identify'/'update', {...}) all take a RelaySettings object, just at a different moment:

<script async src="https://relay.paramms.com/embed.js"></script>
<script>
  Relay('boot', { profileId: 'YOUR_PROFILE_ID' })      // mount immediately, anonymous

  // later, once the visitor logs in:
  Relay('identify', { userId: currentUser.id, userName: currentUser.name })   // merges their guest history in

  // on navigation to a different listing/order page:
  Relay('update', { listingId: newListing.id, listingTitle: newListing.title })

  // on logout:
  Relay('shutdown')                                     // removes the widget + local session
</script>

Relay(...) is queue-safe — calls made before embed.js finishes loading are never lost. identify/update merge onto whatever's currently running (a fresh boot replaces it entirely).

Network requirement: if the host page sets a Content-Security-Policy, it must allow the relay connection — connect-src https://api.paramms.com wss://api.paramms.com; (both are separate origins to the browser; include both), or your self-hosted equivalent.

Old section below (kept for reference)


'use client'
import { ChatWidget } from '@paramms/chat-widget/react'

export default function SupportChat() {
  return (
    <ChatWidget
      url="https://api.relay.paramms.com"
      profileId="YOUR_PROFILE_ID"
    />
  )
}

Identified users (no separate login needed)

Pass your own user's ID as the token and their details via user. Anonymous users work without any configuration.

<ChatWidget
  url="https://api.relay.paramms.com"
  profileId="YOUR_PROFILE_ID"
  token={currentUser.id}          // your own stable user ID — ties history across devices
  user={{
    name:   currentUser.name,     // shown to agents instead of opaque ID
    email:  currentUser.email,    // agents can follow up even after disconnect
    avatar: currentUser.avatarUrl,
    meta: {
      plan:      currentUser.plan,
      accountId: currentUser.id,
    },
  }}
/>

Cryptographically verified identity (Tier 4)

For marketplaces and financial services — the guest's identity is verified against your ECDSA key so it cannot be spoofed.

// 1. Generate a key pair (once, server-side)
//    openssl ecparam -genkey -name prime256v1 -noout | openssl pkcs8 -topk8 -nocrypt -out private.pem
//    openssl ec -in private.pem -pubout | base64 -w0  → paste in Dashboard → Domain → Guest identity linking

// 2. Sign a token per user (your backend)
import { createSign } from 'node:crypto'
const hdr = Buffer.from(JSON.stringify({ alg: 'ES256', typ: 'JWT' })).toString('base64url')
const pay = Buffer.from(JSON.stringify({ sub: userId, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600 })).toString('base64url')
const sig = createSign('SHA256').update(`${hdr}.${pay}`).sign(privateKey, 'base64url')
const signedToken = `${hdr}.${pay}.${sig}`

// 3. Pass to widget — server verifies the signature automatically
<ChatWidget url="..." profileId="..." token={signedToken} />

Marketplace / multi-item (one thread per listing)

<ChatWidget
  url="https://api.relay.paramms.com"
  profileId="YOUR_PROFILE_ID"
  subjectId={`car_${listing.id}`}   // one conversation per item
  showChatList={true}                // guest can switch between their threads
/>

Launcher (floating button)

<ChatWidget
  url="https://api.relay.paramms.com"
  profileId="YOUR_PROFILE_ID"
  launcher={true}
  position="bottom-right"
  accent="#6c5ce7"
/>

Launcher message (the teaser card)

An optional card shown above the closed bubble to invite a conversation. Visitors can dismiss it with the ×; it stays dismissed for the browser session and retires automatically once the chat is opened.

<ChatWidget
  url="https://api.relay.paramms.com"
  profileId="YOUR_PROFILE_ID"
  launcher
  launcherMessage={{ title: 'Questions? Chat with us', subtitle: 'Start a conversation' }}
/>

A bare string works too (title only): launcherMessage="Need help?".

You usually don't need this prop. Set the message once per chatroom in the dashboard (Chatrooms → your chatroom → Widget message & availability) and it arrives automatically over the socket. Pass launcherMessage only to override the dashboard value for one embed (it also renders instantly, before the socket connects).

Script-tag embeds use data-relay-launcher-message / data-relay-launcher-subtitle (or the launcherMessage: {title,subtitle} object in JS-object form) — see the Script tag section above for the full reference.

Internationalisation

<ChatWidget
  url="https://api.relay.paramms.com"
  profileId="YOUR_PROFILE_ID"
  i18n={{
    placeholder: 'Écrivez un message…',
    send:        'Envoyer',
    offline:     'Nous sommes absents pour le moment',   // the away notice
    poweredBy:   '',   // empty string hides the footer
  }}
/>

offline is the fallback text for the away notice shown above the composer outside the chatroom's office hours. It never blocks anything: guests can always send, the message is delivered like any other, and an agent replies when they're back. The chatroom's own "offline message" (dashboard → Widget message & availability) takes precedence over this string.

RTL is detected automatically for Arabic, Hebrew, Persian and Urdu browsers.

All options

| Option | Type | Default | Description | |---|---|---|---| | el | HTMLElement | required | Mount target | | url | string | required | Relay URL — ONE url, any scheme (https://api.relay.paramms.com); the WebSocket URL and REST base are derived | | profileId | string | required | Domain profile ID | | token | string | auto-generated | Guest identity token — pass your user's stable ID to tie history across devices | | user | UserInfo | — | Name, email, avatar, custom metadata — shown to agents | | subjectId | string | — | Item ID for marketplace mode | | showChatList | boolean | false | Show conversation switcher in header | | accent | string | #6c5ce7 | Brand colour (hex) | | launcher | boolean | false | Render as floating button | | position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Launcher position | | launcherMessage | string \| { title, subtitle? } | chatroom setting | Teaser card above the closed launcher. Overrides the dashboard's per-chatroom message; dismissible per session | | translateLang | string | — | Auto-translate incoming messages (ISO language code) | | quickReplies | string[] | — | Pre-set reply chips shown above input | | i18n | I18nStrings | English | UI string overrides | | inbox | boolean | false | Add a ‹ back in the chatroom header that opens the full ChatApp conversation list (inline widgets only) | | inboxScope | 'tenant' \| 'profile' | 'tenant' | With inbox: list threads across all chatrooms (tenant) or just this one (profile) | | inboxStart | boolean | false | Land ON the conversation list instead of a single thread (a dedicated "Messages" page). Implies inbox; requires launcher: false. Script-tag/embed only — in React use <ChatApp/> directly | | tenantId | string | — | Script-tag/embed only. Your business id: lists threads across ALL your chatrooms with no chatroom named. Only meaningful with inboxStart — a single thread still needs a profileId |

Development

npm install
npm run dev          # Vite preview at http://localhost:5174/
npm run build        # tsc → dist/
npm run build:bundle # Vite → standalone CDN bundle
npm test             # 75 tests via vitest
npm run typecheck