Your interface. A real conversation.

Use ChatClient to build chat that feels at home in your product. Customers talk to your team through your interface. Your crew answers in cStar.

1. Set up the client

Install
npm install @cstar.help/js

chat.js
import { ChatClient } from '@cstar.help/js/chat';

const chat = new ChatClient({
  teamSlug: 'your-team-slug'
});

Use your team's slug from cStar settings. Realtime is enabled by default. Message subscriptions use Supabase Realtime when ready, otherwise polling every 3 seconds. Set realtime: false and optionally pollingInterval in milliseconds to use polling explicitly.

2. Authenticate your customer

Choose the path that fits your app. Chat requires an authenticated customer; creating a client alone does not create an anonymous session.

Customers who sign in through cStar

Pass the email and password from your customer sign-in form. The account must already exist and meet any email-confirmation requirements. The customer auth guide covers signup and login.

Customer sign-in
// email and password come from your customer's sign-in form.
await chat.loginWithCredentials({ email, password });

If you already have a customer access token issued by cStar's auth service, use chat.loginWithSession({ accessToken }). A token from another Supabase project or an unrelated auth provider will not work.

Customers who sign in through your app

Keep your existing login and sign their identity on your server. Use that signed identity with chat.identify(). Keep your team's identity secret and secret REST API keys on the server.

3. Open a conversation

Start a conversation
const ticket = await chat.tickets.create({
  subject: 'Help with my order',
  message: 'Can I change my delivery address?'
});

// Render any messages returned with the new conversation.
const firstMessages = ticket.messages ?? [];

// Or let the customer choose an existing conversation.
const { data: tickets, pagination } = await chat.tickets.list({ limit: 20 });

if (pagination.hasMore && pagination.cursor) {
  const nextPage = await chat.tickets.list({
    limit: 20,
    cursor: pagination.cursor
  });
}

The list contains this customer's conversations. Use subject for the title. List options are limit and cursor; the chat client does not expose tickets.get() or a status filter.

4. Read, send and listen

For the selected conversation, load its history and subscribe while it is open. Merge messages by their server-issued ID so a send response and a subscription event appear as one message.

Open conversation
// Use the ticket selected or created above.
// renderMessages is your own UI's rendering function.
const messagesById = new Map();
let closed = false;

function receive(message) {
  if (closed) return;
  messagesById.set(message.id, message);
  renderMessages([...messagesById.values()].sort((a, b) =>
    a.created_at.localeCompare(b.created_at)
  ));
}

const stop = chat.onMessage(ticket.id, receive);

try {
  // messages.list returns an array directly.
  const history = await chat.messages.list(ticket.id);
  history.forEach(receive);
} catch (error) {
  stop();
  throw error;
}

// Call when leaving this conversation.
function closeConversation() {
  closed = true;
  stop();
}

Send from your composer
// Keep the draft until the server acknowledges this send.
// Disable the send button while this request is pending.
const sent = await chat.messages.send(ticket.id, draft);
receive(sent);
// Clear only the draft that was sent, not newer text.

Handle rejected requests in your UI. Keep the draft, show an understandable error, and let the customer check the conversation. A failed response does not always mean the message was not saved, so do not automatically resend it.

Call closeConversation() when switching threads and chat.disconnect() when leaving chat or signing out. Also sign out of your own authentication provider and clear the previous customer's UI. Keep each async load and send tied to the customer and conversation that started it.

Typing is optional: onTyping(ticketId, callback) and sendTyping(ticketId, true) require an initialized realtime connection. A typing subscription made before realtime is ready is a no-op. Events contain agent_id, agent_name and is_typing. Message polling does not provide typing events.

Message history can include attachment metadata. This client sends text only and has no upload helper. The ready-made floating widget has no upload either. For customer uploads, build the chat with data-cstar-* attributes and add a file input with data-cstar-attach. Files go on replies (the first message of a conversation can't carry one), and Custom Mode runs only while Identity Verification is off.

Using your existing login

On your server, read the customer from your authenticated session. Sign only their identity, never an arbitrary identity sent by the browser. Use the team's live identity secret from cStar's identity-verification settings.

Your server, inside an authenticated route
import { createHmac } from 'node:crypto';

// user comes from YOUR verified server-side session.
const customer = {
  email: user.email,
  externalId: String(user.id),
  name: user.name ?? undefined,
  timestamp: Math.floor(Date.now() / 1000)
};

// Sign these fields in alphabetical order, omitting undefined values.
const signedFields = Object.fromEntries(
  Object.entries(customer)
    .filter(([, value]) => value !== undefined)
    .sort(([a], [b]) => a.localeCompare(b))
);

const signature = createHmac('sha256', process.env.CSTAR_IDENTITY_SECRET)
  .update(JSON.stringify(signedFields))
  .digest('hex');

// Return { customer, signature } only to this authenticated customer.
// Set Cache-Control: no-store. Never return the identity secret.

Your browser, after fetching that signed payload
await chat.identify(customer, signature);
// Continue with the same tickets and messages methods above.

Generate the signature just before identifying. The timestamp is the current time in seconds (at most 60 seconds ahead), and signatures expire after five minutes in live mode. If you add metadata or an avatar, keep them out of the signed fields. Agents see metadata in a "From their site" card marked not verified, since the browser can change it; send at most 25 fields of text, numbers or true/false, each up to 500 characters. Use the matching test identity secret only for a deliberate test-mode integration.

Chats from a test-mode identity are saved as test data. Your team sees them in Tickets only while the dashboard's Test mode is on (Settings → API Keys).

Keep building.

Prefer a framework wrapper? Start with React's useChat or Svelte's ChatState. Add help articles for customers who would rather find the answer themselves.

Hit a snag? Ask for help with the step and error you're seeing. Leave out customer details, keys and tokens.