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.0.40

Published

Embeddable real-time chat widget for the Relay platform

Downloads

6,679

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 (no npm, no build)

<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.

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="#4F63F5"
/>

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 attributes:

<script
  src="https://relay.paramms.com/embed.js"
  data-relay-app="YOUR_PROFILE_ID"
  data-relay-launcher-message="Questions? Chat with us"
  data-relay-launcher-subtitle="Start a conversation"
></script>

Internationalisation

<ChatWidget
  url="https://api.relay.paramms.com"
  profileId="YOUR_PROFILE_ID"
  i18n={{
    placeholder: 'Écrivez un message…',
    send:        'Envoyer',
    offline:     'Nous sommes hors ligne pour l\'instant',
    poweredBy:   '',   // empty string hides the footer
  }}
/>

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 | #f5713c | 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) |

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