Development
A Full-Featured React Chat App in Minutes (Open-Source Starter)
A Vite and React starter with chat, presence and typing indicators — what is in it, what is deliberately left for you, and how to take it to production.
A component gets you a chat room. A starter gets you a project you can read, which is a different kind of useful — most people want to see how it fits together before deciding.
For a separate operational perspective on how teams organise and measure work, stealth monitoring software.
For independent background and broader industry context, see React.
What it is
A Vite and React project with a working chat: messages, presence, typing indicators, reconnection handling, and a token endpoint.
Meant to be read as much as run. The point is the wiring, not the styling.
Clone, set your credentials, and it works.
What is deliberately not in it
Being explicit, because a starter that quietly omits things teaches the omission.
Authentication. The token endpoint has a placeholder where your session check goes. It is marked, and it is the single most important line to replace. Without it, anyone can request a token for any room.
Persistence rules. How long messages live is your decision — including the legal parts if the conversation involves customers.
Moderation. No filtering, no reporting, no removal. A public room needs all three.
Styling beyond the minimum.
And rate limiting. A client that can send unlimited messages will eventually meet someone who does.
The parts worth reading
The token endpoint
Where authorisation lives, and the reason the starter has a server at all rather than being pure client-side.
app.get('/api/vaultrice-token', async (req, res) => {
// REPLACE THIS: check your session
const user = await authenticate(req)
if (!user) return res.status(401).end()
// REPLACE THIS: check this user belongs in this room
if (!await mayJoin(user, req.query.room)) return res.status(403).end()
res.json({ accessToken: await mintToken(/* per your setup */) })
})
Two replacements, both marked. With direct apiKey credentials neither check has anywhere to live, which is why the starter uses getAccessToken() throughout.
The client lifecycle
One client, created once, cleaned up on unmount. The pattern from cross-domain state in React — a provider rather than a client per component.
With cleanup, because Strict Mode in development mounts twice and a missing cleanup produces duplicate messages that disappear in production.
Typing indicators
Transient messages, not stored state. A typing indicator is meaningless a second later, so it goes through send() rather than setItem().
Throttled, because a keystroke handler firing on every character is a lot of traffic for a small feature.
Reconnection
Handled by the SDK, and the starter shows the connection state in the UI, because a silent reconnect looks like a hang.
Taking it to production
- [ ] Replace both marked checks in the token endpoint
- [ ] Room ids not guessable where conversations are private
- [ ] Display name from the server session, not from client input
- [ ] Message retention decided and implemented
- [ ] Security level chosen — the default is not the strongest
- [ ] Rate limiting on message send
- [ ] Moderation path if the room is public
- [ ] Tested with the network genuinely off, not throttled
The security guide covers the levels. See from API keys to E2EE.
Using it as a reference rather than a base
Frequently the better use.
Read the token endpoint and the client lifecycle, then write your own. The starter's structure is a demonstration of the wiring, and your application probably has opinions about state management, styling and routing that it does not.
And if you only want a chat room in an existing app, the component is less work. See a React chat app in 3 minutes.
The short version
A readable Vite and React project, not a framework.
Authentication, retention, moderation and rate limiting are deliberately absent and marked where they belong.
The token endpoint is the part to read, because it is where authorisation lives.
Typing indicators are transient messages, not stored state.
And it works as a reference more often than as a base — read the wiring, then write your own.