Site icon Salesforce Diaries

Agentforce – MIAW Custom Client on Vercel

This project implements a custom web-based chat client for Salesforce Agentforce using the Salesforce Messaging for In-App and Web API v2. The application provides a real-time chat interface where users can interact with Salesforce AI agents. I have hosted it on Vercel. Find the complete code here: https://github.com/SalesforceDiariesBySanket/sample-agentforce-custom-client/tree/main

This blog and project are inspired by the original work in charlesw-salesforce/sample-agentforce-custom-client. I’ve built upon that foundation to add explanations, examples, and enhancements for better understanding.

Live Demo: Deployed on Vercel
Technology Stack: React, TypeScript, Node.js, Vite, TailwindCSS


Architecture

Frontend (Client)

Backend (Server)

API Layer

Six main endpoints handle the chat lifecycle:

  1. Initialize (/api/chat/initialize)
    • Authenticates with Salesforce
    • Creates new conversation
    • Returns access token and conversation ID
  2. Message (/api/chat/message)
    • POST: Sends user messages
    • GET: Retrieves conversation entries
  3. Typing (/api/chat/typing)
    • Sends typing indicators
  4. Conversation (/api/chat/conversation)
    • Retrieves conversation details
  5. SSE (/api/chat/sse)
    • Server-Sent Events endpoint (not used due to Vercel limitations)
  6. End (/api/chat/end)
    • Closes conversation

Salesforce Integration

Salesforce Messaging for In-App and Web API v2

Base URL: https://{SALESFORCE_SCRT_URL}/iamessage/api/v2/

Authentication Flow

  1. Get Access Token
   POST /iamessage/api/v2/token/authorize
   Body: {
     organizationId: process.env.SALESFORCE_ORG_ID,
     developerName: process.env.SALESFORCE_DEVELOPER_NAME
   }
  1. Extract Organization ID from JWT
   const tokenPayload = accessToken.split('.')[1];
   const decoded = JSON.parse(Buffer.from(tokenPayload, 'base64').toString());
   const orgId = decoded.orgId; // 15-character format
  1. Create Conversation
   POST /iamessage/api/v2/conversation
   Headers: Authorization: Bearer {token}
   Body: { conversationId: crypto.randomUUID() }

Key Integration Points


Environment Variables

Required Configuration

# Salesforce Configuration
SALESFORCE_SCRT_URL=your-salesforce-scrt-url
SALESFORCE_ORG_ID=your-18-char-org-id
SALESFORCE_DEVELOPER_NAME=CustomClientNode

# API Configuration
API_URL=http://localhost:3000

Vercel Deployment

All environment variables must be configured in Vercel Project Settings.


Key Technical Challenges & Solutions

1. Conversation ID Format

Challenge: Salesforce requires lowercase UUID format for conversationId.

Error: "Specify the conversationId in UUID format. The UUID must be in lowercase."

Solution:

const conversationId = crypto.randomUUID(); // Generates lowercase UUID

2. Organization ID Mismatch

Challenge: Environment variable contains 18-character orgId (00DgL00000XXXXXUA2), but Salesforce JWT token contains 15-character format (00DgL00000XXXXX).

Solution: Extract orgId from JWT token instead of using environment variable:

const tokenPayload = data.accessToken.split('.')[1];
const decoded = JSON.parse(Buffer.from(tokenPayload, 'base64').toString());
const orgId = decoded.orgId; // Use this for SSE and other API calls

3. Server-Sent Events (SSE) on Vercel

Challenge: Vercel serverless functions buffer responses, preventing true SSE streaming to browser.

Symptoms:

Attempted Solutions:

Root Cause: Fundamental Vercel platform limitation – serverless functions buffer responses regardless of configuration.

Final Solution: Polling-based approach

// Poll every 2 seconds
const pollMessages = async () => {
  const response = await fetch(`${API_URL}/chat/message`, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'X-Conversation-Id': conversationId
    }
  });
  const data = await response.json();

  // Filter for bot messages
  const botEntries = data.conversationEntries.filter(
    entry => entry.entryType === "Message" && entry.sender.role === "Chatbot"
  );

  // Update state with deduplication
  botEntries.forEach(entry => {
    const payload = JSON.parse(entry.entryPayload);
    const messageText = payload.abstractMessage.staticContent.text;
    const messageId = payload.abstractMessage.id;

    setMessages(prev => {
      if (prev.find(m => m.id === messageId)) return prev;
      return [...prev, { id: messageId, type: "ai", content: messageText }];
    });
  });
};

pollingRef.current = setInterval(pollMessages, 2000);
pollMessages(); // Immediate first poll

4. Message Deduplication

Challenge: Polling could retrieve the same messages multiple times.

Solution: Check message ID before adding:

setMessages(prev => {
  if (prev.find(m => m.id === messageId)) return prev;
  return [...prev, newMessage];
});

Project Structure

sample-agentforce-custom-client/
├── api/                          # Vercel serverless functions
│   └── chat/
│       ├── initialize.ts         # Auth & conversation creation
│       ├── message.ts            # Send/receive messages
│       ├── conversation.ts       # Get conversation details
│       ├── typing.ts             # Typing indicators
│       ├── sse.ts                # SSE endpoint (unused)
│       └── end.ts                # Close conversation
│
├── client/                       # React frontend
│   ├── src/
│   │   ├── components/
│   │   │   └── chat/
│   │   │       ├── ChatWindow.tsx
│   │   │       ├── ChatMessage.tsx
│   │   │       ├── ChatInput.tsx
│   │   │       ├── ChatHeader.tsx
│   │   │       └── ChatMessageList.tsx
│   │   ├── hooks/
│   │   │   ├── useChat.ts        # Main chat logic
│   │   │   └── useSalesforceMessaging.ts
│   │   ├── contexts/
│   │   │   └── ThemeContext.tsx
│   │   ├── App.tsx
│   │   └── main.tsx
│   ├── index.html
│   ├── vite.config.ts
│   └── package.json
│
├── server/                       # Local development server
│   └── src/
│       ├── index.ts
│       ├── routes.ts
│       └── handlers/
│
├── package.json                  # Root package
├── pnpm-workspace.yaml
├── vercel.json                   # Vercel configuration
└── README.md

Message Flow

1. User Sends Message

User Input → ChatInput component
  ↓
useChat.sendMessage()
  ↓
POST /api/chat/message
  ↓
Salesforce API v2
  ↓
200 OK

2. Bot Response (Polling)

setInterval (2000ms)
  ↓
GET /api/chat/message
  ↓
Salesforce API v2
  ↓
Filter conversationEntries for role="Chatbot"
  ↓
Parse entryPayload → abstractMessage.staticContent.text
  ↓
Deduplicate by message ID
  ↓
Update React state
  ↓
Render in ChatMessage component

3. Complete Conversation Flow

  1. Initialize: POST /api/chat/initialize
    • Get access token
    • Extract orgId from JWT
    • Create conversation with UUID
    • Return credentials
  2. Start Polling: Begin 2-second interval
  3. User Message: POST /api/chat/message
    • Send message with conversationId
    • Salesforce routes to agent
  4. Poll ResponseGET /api/chat/message
    • Retrieve all entries
    • Filter for bot messages
    • Update UI
  5. Close: POST /api/chat/end
    • End conversation
    • Clear polling interval

Data Structures

Message Type

interface Message {
  id: string;
  type: "user" | "ai" | "system";
  content: string;
  timestamp: Date;
}

Conversation Entry (Salesforce Response)

interface ConversationEntry {
  entryType: "Message" | "ParticipantChanged";
  sender: {
    role: "EndUser" | "Chatbot" | "Agent";
    appType: string;
  };
  entryPayload: string; // JSON string
  clientTimestamp: string;
}

Entry Payload (Bot Message)

interface EntryPayload {
  abstractMessage: {
    id: string;
    staticContent: {
      text: string;
      formatType: "PlainText" | "RichText";
    };
  };
}

API Credentials

interface Credentials {
  accessToken: string;      // JWT from Salesforce
  conversationId: string;   // Lowercase UUID
  orgId: string;           // 15-character from JWT
  lastEventId?: string;    // For SSE (not used)
}

Development

Prerequisites

Setup

  1. Clone Repository
   git clone https://github.com/SalesforceDiariesBySanket/sample-agentforce-custom-client.git
   cd sample-agentforce-custom-client
  1. Install Dependencies
   pnpm install
  1. Configure Environment
   # Create .env file
   SALESFORCE_SCRT_URL=your-url
   SALESFORCE_ORG_ID=your-org-id
   SALESFORCE_DEVELOPER_NAME=CustomClientNode
  1. Run Development Server
   pnpm dev
  1. Build for Production
   pnpm build

Testing

Manual Testing with cURL

1. Initialize Conversation

curl -X POST https://your-app.vercel.app/api/chat/initialize

2. Send Message

curl -X POST https://your-app.vercel.app/api/chat/message \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Conversation-Id: YOUR_UUID" \
  -H "Content-Type: application/json" \
  -d '{"message":"Hi"}'

3. Get Messages

curl https://your-app.vercel.app/api/chat/message \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Conversation-Id: YOUR_UUID"

Deployment

Vercel Deployment

  1. Connect Repository
    • Link GitHub repository to Vercel
    • Auto-deploy on push to main
  2. Configure Build Settings
 {
     "buildCommand": "cd client && pnpm build",
     "outputDirectory": "client/dist",
     "installCommand": "pnpm install"
   }  
  1. Set Environment Variables
    • Add all required variables in Vercel dashboard
    • Redeploy after changes
  2. Deployment Configuration (vercel.json)
   {
     "rewrites": [
       { "source": "/api/:path*", "destination": "/api/:path*" },
       { "source": "/(.*)", "destination": "/client/dist/$1" }
     ],
     "functions": {
       "api/**/*.ts": {
         "maxDuration": 30
       }
     }
   }

Debugging

Browser Console Logs

The application includes comprehensive logging:

// Initialization
console.log('Chat initialized:', { conversationId, orgId, lastEventId });

// Polling
console.log('Polling response:', data);
console.log('Found bot entries:', entries.length);
console.log('Bot message:', messageText);

// Errors
console.error('Failed to send message:', error);

Common Issues

1. “Failed to fetch”

2. “Specify the conversationId in UUID format”

3. No bot responses

4. Messages not displaying


Performance Optimizations

1. Polling Interval

2. Message Deduplication

3. React Optimizations


Security Considerations

1. Access Token Handling

2. CORS Configuration

3. Environment Variables


Future Enhancements

  1. WebSocket Support
    • If moving away from Vercel
    • True real-time bidirectional communication
  2. Message Persistence
    • Store conversation history
    • Resume previous conversations
  3. Rich Media Support
    • File uploads
    • Images and attachments
    • Formatted messages
  4. Typing Indicators
    • Show when bot is typing
    • User typing notifications
  5. Error Recovery
    • Automatic reconnection
    • Message retry logic
    • Offline support
  6. Analytics
    • Track conversation metrics
    • User engagement analytics
    • Bot performance monitoring

Lessons Learned

1. Platform Limitations

2. API Version Compatibility

3. Token Handling

4. Debugging Strategy


References

Documentation

Do you need help?

Are you stuck while working on this requirement? Do you want to get review of your solution? Now, you can book dedicated 1:1 with me on Lightning Web Component and Agentforce completely free.

GET IN TOUCH

Schedule a 1:1 Meeting with me

Exit mobile version