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
npm install @cstar.help/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.
// 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
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.
// 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();
} // 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.
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. 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.