Documentation

Build a Session-memory Chatbot

Import the ready-made workflow, connect the React and Node.js example, and deliver structured replies over a secure one-time WebSocket channel.

What this example includes

The example is a working full-stack chatbot built with React, Express, and ModelRiver:

FeatureHow it works
Session memoryThe first request creates a session_id; later turns send it back so ModelRiver can inject relevant conversation history.
Asynchronous AI requestsThe Node.js backend calls /v1/ai/async and immediately returns connection details to the browser.
Real-time deliveryThe browser joins the request's Phoenix WebSocket channel with a single-use ws_token.
Structured responsesReplies include reply, summary, sentiment, confidence, topics, and action_items.
Backend callbackModelRiver sends the webhook_received event to the backend, which can persist or enrich the result before calling back.

The example keeps messages in memory for demonstration. Replace that storage with your database before using it in production.

Request flow

TEXT
1React frontend
2 POST /chat (message + optional session_id)
3
4Node.js backend
5 POST /v1/ai/async
6
7ModelRiver AI provider
8
9 returns channel_id, ws_token, websocket_url,
10 websocket_channel, and session_id
11
12 sends webhook_received to the Node.js backend
13
14 backend calls callback_url
15
16
17 final structured response is sent to React
18 through the request's WebSocket channel

Prerequisites

  • Node.js 18 or newer
  • A ModelRiver project
  • A project API key
  • Credentials for at least one supported AI provider

Step 1 — Get the application

Bash
git clone https://github.com/modelriver/chatbot-async-app.git
cd chatbot-async-app
 
cd backend
npm install
 
cd ../frontend
npm install

Step 2 — Import the chatbot template

  1. Open the Session Memory Chatbot template.
  2. Click Download JSON.
  3. Open your ModelRiver project and use Import to upload the file.
  4. Review and confirm the import.

The template creates:

  • Workflow: mr_chatbot_workflow
  • Structured output: chatbot_response
  • Session memory: enabled
  • Backend pipeline event: webhook_received
  • Primary model: openai / gpt-5.6-luna
  • Backup model: anthropic / claude-haiku-4-5-20251001

Connect the selected providers in your project before sending a live request. You can change the models on the template page before downloading it.

Required for session memory: Keep request-body logging enabled for the project. ModelRiver cannot build conversation memory when request bodies are disabled.

Step 3 — Create the API key and webhook

API key

Create a project API key and copy it into backend/.env as MODELRIVER_API_KEY.

Webhook

Create one enabled HTTP webhook for the project and copy its secret into WEBHOOK_SECRET.

When ModelRiver is running locally on port 4000, use:

TEXT
1http://localhost:4001/webhook/modelriver

When using hosted ModelRiver, the webhook must be publicly reachable. During local development you can use the ModelRiver CLI forward command or an HTTPS tunnel.

The project may contain a CLI webhook too, but it will show “No CLI client connected” when no CLI listener is running. That failure does not mean the HTTP webhook failed; disable unused CLI webhooks to keep request logs clear.

Step 4 — Configure environment variables

Copy the example files first:

Bash
cd backend
cp .env.example .env
 
cd ../frontend
cp .env.example .env

Hosted ModelRiver

backend/.env:

Bash
PORT=4000
MODELRIVER_API_KEY=mr_live_your_project_key
MODELRIVER_API_URL=https://api.modelriver.com
BACKEND_PUBLIC_URL=https://your-backend-or-tunnel.example.com
WEBHOOK_SECRET=your_webhook_secret

frontend/.env:

Bash
VITE_API_URL=http://localhost:4000

ModelRiver running locally

ModelRiver already uses port 4000, so run the chatbot backend on 4001.

backend/.env:

Bash
PORT=4001
MODELRIVER_API_KEY=mr_live_your_local_project_key
MODELRIVER_API_URL=http://127.0.0.1:4000/api
BACKEND_PUBLIC_URL=http://localhost:4001
WEBHOOK_SECRET=your_local_webhook_secret

frontend/.env:

Bash
VITE_API_URL=http://localhost:4001

The frontend development port is fixed to 3006 in frontend/vite.config.js; it is not read from the frontend .env file. BACKEND_PUBLIC_URL must point to the same backend that receives /webhook/modelriver. The application does not use an EVENT_NAME environment variable; its default event is webhook_received.

Step 5 — Run the chatbot

Open two terminals in the cloned repository.

Terminal 1 — backend:

Bash
cd backend
npm start

Terminal 2 — frontend:

Bash
cd frontend
npm run dev

Open http://localhost:3006.

If you use hosted ModelRiver with a CLI webhook, also run the CLI forwarding command in a third terminal using the port configured during modelriver login:

Bash
modelriver forward --port 4000

For local ModelRiver with the direct HTTP webhook, no CLI process is required.

Step 6 — Test session memory

Send this as the first message:

TEXT
1My name is Vishal. I have a bakery named Sunrise Bakes in Bangalore. Please remember this.

Then send this in the same conversation:

TEXT
1What is my name, business name, and city?

The reply should recall Vishal, Sunrise Bakes, and Bangalore. The session indicator in the UI should remain the same across both turns.

Click New conversation, then ask the second question again. A new session_id should be created, and the chatbot should not claim to know details from the previous conversation.

How session_id is handled

The browser does not invent a ModelRiver session ID:

  1. The first /chat request omits session_id.
  2. ModelRiver creates a session and returns session_id in the async response body.
  3. The backend returns that ID to React.
  4. React sends the same ID with every later message in that conversation.
  5. New conversation clears it so the next request creates another session.

The client-generated conversationId is application metadata. It must not be used in place of ModelRiver's session_id.

Example backend payload:

JAVASCRIPT
1const payload = {
2 workflow: 'mr_chatbot_workflow',
3 messages: [{ role: 'user', content: message }],
4 delivery_method: 'websocket',
5 webhook_url: `${BACKEND_PUBLIC_URL}/webhook/modelriver`,
6 events: ['webhook_received'],
7 metadata: {
8 conversation_id: conversationId,
9 message_id: messageId,
10 original_prompt: message
11 }
12};
13 
14if (sessionId) payload.session_id = sessionId;
15 
16const { data } = await axios.post(
17 `${MODELRIVER_API_URL}/v1/ai/async`,
18 payload,
19 { headers: { Authorization: `Bearer ${MODELRIVER_API_KEY}` } }
20);

How WebSocket delivery is handled

The app uses the Phoenix client directly because each ws_token is single-use. It deliberately disconnects after the final response so the client cannot retry with a consumed token.

JAVASCRIPT
1import { Socket } from 'phoenix';
2 
3const socket = new Socket(websocket_url, {
4 params: { token: ws_token },
5 reconnectAfterMs: () => 60_000
6});
7 
8const channel = socket.channel(websocket_channel);
9 
10socket.onOpen(() => {
11 channel.join();
12});
13 
14channel.on('response', payload => {
15 const status = payload.meta?.status || payload.status;
16 renderResponse(payload);
17 
18 if (status === 'completed' || status === 'success') {
19 channel.leave();
20 socket.disconnect();
21 }
22});
23 
24socket.connect();

Use the exact websocket_url and websocket_channel returned for that request. Do not store or reuse ws_token.

How the backend callback is handled

ModelRiver sends a task.ai_generated webhook for the webhook_received event. The backend:

  1. Verifies X-ModelRiver-Signature and X-ModelRiver-Timestamp using WEBHOOK_SECRET.
  2. Reads the structured result from ai_response.data.
  3. Adds the application's message ID.
  4. POSTs the result to callback_url with the project API key.
JAVASCRIPT
1const callbackPayload = {
2 data: {
3 ...webhook.ai_response.data,
4 id: messageId
5 },
6 task_id: messageId,
7 metadata: webhook.ai_response.meta || {}
8};
9 
10await axios.post(webhook.callback_url, callbackPayload, {
11 headers: {
12 Authorization: `Bearer ${MODELRIVER_API_KEY}`,
13 'Content-Type': 'application/json'
14 }
15});

The final callback payload is then delivered to the browser's WebSocket channel.

Structured response

The imported chatbot_response format contains:

FieldTypePurpose
replystringMain answer shown to the user
summarystringShort description of the turn
sentimentpositive, neutral, negative, or mixedSentiment badge
confidencehigh, medium, or lowConfidence indicator
topicsstring arrayTopic tags
action_itemsarray of { task, priority }Follow-up work, when present

The frontend accepts both categorical and numeric confidence values, but the downloadable template uses high, medium, or low consistently.

Backend endpoints

EndpointMethodDescription
/chatPOSTStarts an async request and returns WebSocket and session details
/webhook/modelriverPOSTVerifies and processes ModelRiver webhook events
/conversations/:idGETReturns the demo's in-memory conversation records
/healthGETShows backend and ModelRiver configuration status

POST /chat accepts:

JSON
1{
2 "message": "What did I tell you about my business?",
3 "workflow": "mr_chatbot_workflow",
4 "conversationId": "optional-application-conversation-id",
5 "session_id": "optional-modelriver-session-uuid",
6 "events": ["webhook_received"]
7}

Only message is required. The React client manages session_id automatically.

Production checklist

Before deploying this example:

  • Replace the in-memory maps with persistent storage.
  • Require WEBHOOK_SECRET; never allow unsigned production webhooks.
  • Restrict CORS to the deployed frontend origin.
  • Use HTTPS for the backend and WebSocket endpoint.
  • Associate session IDs with the authenticated user on your server.
  • Add idempotency and retry handling around database writes and callbacks.
  • Keep API keys and webhook secrets on the backend only.
  • Monitor request logs, failed webhook deliveries, callbacks, and session summaries.

Troubleshooting

WebSocket connection error or HTTP 403

  • Confirm the frontend uses the latest ws_token and does not reconnect with it.
  • Confirm it uses the returned websocket_channel, not a channel assembled in the browser.
  • When ModelRiver is local, prefer 127.0.0.1 for the WebSocket host to avoid macOS IPv6 resolution problems.
  • Restart the frontend after changing its .env file.

The chatbot does not remember

  • Confirm Session memory is enabled on mr_chatbot_workflow.
  • Confirm request-body logging is enabled for the project.
  • Confirm the second /chat request sends exactly the session_id returned by the first.
  • Confirm New conversation was not selected between turns.

HTTP webhook succeeds but CLI webhook fails

An enabled cli://localhost webhook fails when no CLI client is connected. Disable it or run the CLI. The successful HTTP webhook can still complete the request.

Resources