Sign-ups paused

Sign-ups and billing are temporarily unavailable while we rebuild our infrastructure. The SDK and documentation remain available, and existing accounts are unaffected.

Vaultrice

Blog / Tutorials

Tutorials

Build a Production-Ready React Chat App in 3 Minutes with One Component

A drop-in ChatRoom component with presence and typing indicators, then what to change before it goes in front of real users.

5 min read Updated 2026-08-XX @vaultrice/react-components

Chat is one of those features that looks small and is not. Message delivery, ordering, presence, typing indicators, reconnection after a dropped network, history on join.

For a separate operational perspective on how teams organise and measure work, remote employee monitoring software.

For independent background and broader industry context, see Socket.IO.

The component below handles the plumbing. The second half of this article is about the parts a component cannot decide for you, which is where "works in a demo" and "in front of users" diverge.

The three minutes

npm install @vaultrice/react-components
import { ChatRoom } from '@vaultrice/react-components'

export default function Support() {
  return (
    <ChatRoom
      credentials={{
        projectId: 'your-project-id',
        getAccessToken: async () => {
          const res = await fetch('/api/vaultrice-token')
          if (!res.ok) throw new Error('Failed to fetch token')
          const { accessToken } = await res.json()
          return accessToken
        }
      }}
      id="support-room-42"
      user={{ name: currentUser.name }}
    />
  )
}

That is a working room — messages, presence, typing indicators, and reconnection.

The id is the room. Two clients with the same id are in the same conversation. Two clients with different ids are not.

The token endpoint

Do not skip this to save time.

The SDK also accepts apiKey and apiSecret directly, which is convenient and puts your secret in every browser that loads the page.

getAccessToken() calls your own endpoint, which authenticates the user however your app already does and returns a short-lived token. The SDK refreshes it automatically.

Your endpoint is the place where you decide whether this user may join this room. That decision cannot live in the client, and if you use direct credentials, there is no place for it at all.

What the component does not decide

Who is allowed in

The room id is the address. A client with valid project credentials and the id reaches the room.

Which means access control is your token endpoint's job. Check that this user belongs in this room before issuing a token scoped to it. A support room named support-room-42 is guessable, and that matters.

What happens to message history

Decide deliberately: how long messages persist, whether a new joiner sees what came before, and whether anyone can delete.

This has a legal dimension if the conversation is with a customer — retention, access requests, deletion.

Identity

The user prop is whatever you pass. The component displays it; it does not verify it.

Take the display name from your session on the server side when minting the token, not from client-supplied input, unless anonymous naming is genuinely what you want.

Moderation

Nothing filters content. For public rooms you need a plan — at minimum a way to remove a message and to stop a participant.

Encryption level

The default is not the strongest available. Security levels go up to end-to-end encryption, where the service does not hold readable content.

For anything sensitive, read the security guide and choose deliberately, rather than accepting the default because it worked in testing.

Going beyond the component

When the drop-in is not enough, the same thing is available at the lower level:

import { NonLocalStorage } from '@vaultrice/sdk'

const nls = new NonLocalStorage(credentials, 'support-room-42')

// join with your own presence payload
nls.join({ name: currentUser.name, role: 'agent' })

nls.on('presence:join', (conn) => {
  console.log(conn.data.name, 'joined')
})

const connections = await nls.getJoinedConnections()

// messaging
nls.send({ chat: 'Hello' })

nls.on('message', (msg) => {
  render(msg.chat)
})

Use this when you need custom rendering, your own message shape, or behaviour the component does not expose. The component is built on it, so there is no capability cliff.

A short checklist before shipping

  • [ ] Token endpoint in place, no apiSecret in client code
  • [ ] Endpoint checks the user may join this room
  • [ ] Room ids not guessable where the contents are private
  • [ ] Display name taken from the server session
  • [ ] Message retention decided and implemented
  • [ ] Security level chosen deliberately
  • [ ] Moderation path for public rooms
  • [ ] Behaviour on connection loss checked with the network throttled

The short version

One component gets you a working room with presence, typing and reconnection.

Use getAccessToken() — the token endpoint is also where you decide who may join.

The room id is the address, and a guessable id is a public room.

Take the display name from the server, not from the client.

And decide retention and encryption level before shipping, not after someone asks.