The codebase label says "MEAN stack" but the stack is actually:
| Layer | Technology | Project |
|---|---|---|
| A Angular | Angular 18.2 | ice-visitor |
| E Express | Express 4.15 | ice-nodejs |
| N Node.js | Node 18.20 (Volta-pinned) | ice-nodejs |
| M |
MS SQL Server (via mssql 3.3) |
ice-nodejs |
There is no MongoDB. The "M" is MSSQL. Redis is also heavily used as a session store and message broker. MySQL (mysql 2.13) exists in package.json but MSSQL is the primary database — almost all dalayer.js operations go to SQL Server via stored procedures.
Browser Visitor (Embedded Widget)
│
│ HTTP load page
▼
ice-visitor (Angular 18, SPA)
│
├── REST calls ──► ICEInternalAPI (ice-internal-api, .NET WebAPI)
│ └── reads/writes SQL Server
│
└── Socket.IO ──► ice-nodejs (Node + Express + Socket.IO)
├── Redis (sessions, pub/sub, WFM queue)
├── SQL Server (chat data, user state)
├── Google Cloud Translate (real-time translation)
├── Twilio (SMS-to-Chat)
├── Twitter/Twit (Twitter DM channel)
└── FCM (mobile push notifications)
Agent / Supervisor (Operator Console)
│ — separate Angular app: ice-mvc-admin
│ — also connects to same ice-nodejs Socket.IO server
│ — also calls same ice-internal-api endpoints
Admin (Configuration UI)
└── ice-mvc-admin (ASP.NET MVC + JS)
└── calls ice-internal-api
Two API namespaces the Angular code hits:
ICEInternalAPI/Api/ → REST, handled by ice-internal-api (.NET)socket.io → real-time chat events, handled by ice-nodejsThe Chat REST API documented at enterice.com/ICEAPI/api/ is a public/external API — your internal Angular code calls ICEInternalAPI instead.
ice-nodejs — Deep Divenode app.js
└── cluster.js ← Master process
├── forks N workers (cpu count)
└── each worker runs:
expressserver.js ← Express + HTTPS server
└── socket.js ← Socket.IO initialization
cluster.js (ice-nodejs/cluster.js):
cluster moduleprocess.env.WORKERS || os.cpus().length)disconnect → immediately forks a replacement (self-healing)WORKERS env variableexpressserver.js (ice-nodejs/expressserver.js):
config.js (sslKey:path, sslKey:passphrase) — uses a PFX cert (enterice.pfx)https.createServer (TLS) on the configured porthttp.createServer (plain) — no cert needed locallyAccess-Control-Allow-Origin: * on all responsesmaxAge: 7776000)POST /{env}/ChatBtn → serverChat.js.initialize() — serves the chat button config/scriptPOST /{env}/RuleServer → RuleConditionHandler.js.initialize() — evaluates rule engine conditionsbuttonClientScript.js, uuid.js, json2.js, RuleHandler.js (and WordPress variant)socket.js.initialize(server, cluster)config/ directory uses nconf for layered config:
| File | Purpose |
|---|---|
config/development.json |
Local dev: port 3000, local Redis/SQL |
config/production.json |
Prod settings |
config/flashfree.json |
Flash-free deployment variant |
config/language.json |
Language code mappings |
Key config keys (from development.json):
{
"node": { "env": "development" },
"express": { "port": 3000 },
"sqlConn": {
"user": "...", "password": "...",
"server": "...", "database": "EChat_Staging",
"connectionTimeout": 1500000, "requestTimeout": 1500000,
"pool": { "max": 20, "min": 1, "idleTimeoutMillis": 30000 }
},
"ApplicationRedis": { "server": "127.0.0.1", "port": "6379" },
"api": {
"url": "https://dev.enterice.com/ICEInternalApi/api/Visitor/SaveVisitorInformationFromApi",
"secretKey": "ASDFGHJK@#$%^l"
}
}
socket.js — The God Nodesocket.js is 1.5 MB. It is the runtime core of the entire chat platform. Changing anything here has blast radius across every connected client. It owns:
Socket.IO setup:
// socket.js attaches to the HTTPS/HTTP server Express created
// Uses Redis pub/sub adapter so all cluster workers share socket state
io.adapter(RedisStore({ pubClient: pubR, subClient: subR }));
pubR, subR, clientR, redisClient_RTAWFMsocket.io-redis adapter: when one worker emits an event, Redis broadcasts it to all workers — this is how Socket.IO works in a multi-process clusterLanguage support hardcoded at top of socket.js:
const language = ['English', 'Polish', 'Spanish', 'French', 'Czech', 'Arabic', 'Japanese'];
Seven languages for system messages (join messages, greetings, transcript labels) are stored as parallel arrays. Real-time translation uses Google Cloud Translate API (@google-cloud/translate v2).
Key functions in socket.js:
| Function | Purpose |
|---|---|
initialize(server, cluster) |
Bootstraps Socket.IO, attaches event handlers |
eWfmRedisPush() |
Pushes Workforce Management (WFM) real-time data to Redis |
RedisOperationCustomer() |
Redis operations for visitor/customer state |
RedisOperationOperator() |
Redis operations for agent state |
getQueue() |
Retrieves chat queue data |
getWFMData() |
Fetches WFM RTA data (most connected function, 9 edges) |
PerformCallQueueCleanup() |
Cleans up stale queue entries |
translateText() |
Calls Google Translate for real-time message translation |
detectLanguage() |
Detects language of incoming message |
Third-party integrations wired inside socket.js:
Twilio — SMS-to-Chat inbound/outboundTwit — Twitter/X DM channelFCM (fcm-node) — Firebase push to mobile appAWS.SNS — Error alerting (currently commented out in code)sanitize-html — XSS-sanitizes chat messages before savingmmdb-reader + GeoLite2-City.mmdb — IP geolocationdalayer.js (97 KB — the main DB module):
mssql npm packageusp_usersUpdateNew, usp_tbl_VisitorUniqueIdentifierInsert_new)logger.jsExample pattern:
async function updateUserStatus(userId, agentState, currentIp, inCall, callback) {
var connection = await new sql.Connection(sqlconn);
connection.connect(async function(err) {
var request = await new sql.Request(connection);
request.input('userID', sql.Int, userId);
request.input('agentState', sql.VarChar, agentState);
// ...
request.execute('usp_usersUpdateNew', async function(err, recordsets, returnValue) {
connection.close();
await callback(err);
});
});
}
dalayer_repo.js (31 KB): Additional DB operations, same pattern as dalayer.js.
serverChatDBLogic.js: DB operations specific to the chat button/visitor identification flow. Called by the /ChatBtn POST route.
login_process.js — Handles agent logout via a Redis-based message queue:
Redis DB 15, key: "login-list"
│
│ BRPOP (blocking pop — waits for messages)
▼
login_process.js.waitForPush()
│
├── action == "logoutUser" → SMEMBERS {user}{orgid} → logoutSession() for each
└── action == "logoutSession" → GET session:{id} → logoutSession() after 30s delay
logoutSession(user, orgid, session):
SREM {user}{orgid} {session} — remove session from user's setDEL session:{session} — delete session dataPUBLISH 'logout' { uuid: session } — broadcast to all workers via Redis pub/subThis means a force-logout from the Admin UI results in a Redis message → login_process.js picks it up → publishes to logout channel → all Socket.IO workers disconnect that session.
sandra_process.js (19 KB):
watchI2S() — watches for in-chat survey triggers (5 connections in graph)getQuestions() — fetches survey questions from DBsaveResponse() / saveResponseDone() — persists survey answerscheckInchatSurvey() — checks whether an in-chat survey should be shownIceSandraTestChat.js — Core chat session management:
AcceptCall() — agent accepts an incoming chatOperatorDisconnect() — agent disconnects from chatSaveChatHistory() — persists chat transcript to DBUpdateOperatorStatus() — updates agent availability in DBRuleConditionHandler.js (13 KB) — Called by the /RuleServer POST route:
RuleEngineDb.js (16 KB) — DB operations for rule engine (load rules, log invitations).
IceRuleEngine.js (1.7 KB) — Orchestrator that ties RuleConditionHandler and RuleEngineDb together.
chatbuttonscript/RuleHandler.js — Client-side counterpart: runs in the visitor's browser, periodically POSTs context to the /RuleServer endpoint.
logger.js:
DailyRotateFile transportErrorLogs/Errorlogs.log (rotates by date)exceptionHandlers)Assurant_WFM_RTA/ — A sub-module for workforce management Real-Time Adherence integration with Assurant's WFM system. Connected to socket.js via getWFMData() / eWfmRedisPush(). Pushes live agent stats to the WFM system in real time.
cd ice-nodejs
# Set NODE_ENV=development so it reads config/development.json
NODE_ENV=development node cluster.js
Dependencies for local dev:
127.0.0.1:6379config/development.json (server field)18.20.4 in ice-visitor — use same for ice-nodejs)ice-visitor — Deep Diveice-visitor is the embedded chat widget — the Angular 18 SPA that gets injected into a customer's website. When a visitor opens a page with the ICE chat button, this Angular app loads inside an iframe or as an embedded component.
Project name in package.json: vs-console (Visitor Console).
main.ts
└── platformBrowserDynamic().bootstrapModule(AppModule)
└── AppModule (app.module.ts)
└── RouterModule (HashLocationStrategy)
├── /visitor → VisitorComponent
├── /chatbot → ChatbotComponent
└── /blank → BlankComponent
Hash-based routing (#/visitor) because this app runs embedded in third-party pages — hash routing avoids server-side routing conflicts.
AppSettingssrc/app/config/app.settings.ts — no Angular environment.ts files are used. Instead, AppSettings.getEnvironmentVariable(key) switches on window.location.hostname:
| Hostname | Environment |
|---|---|
localhost |
Local dev (Node on :3000, internal API on localhost) |
dev.enterice.com |
Development server |
staging.enterice.com |
Staging server |
ha.enterice.com |
High-availability / production alternate |
enterice.com / www.enterice.com |
Production |
chat.enterice.com |
Chat-specific production domain |
URL keys returned by getEnvironmentVariable():
| Key | Points To |
|---|---|
API_URL |
https://{env}/ICEInternalAPI/Api/ — ice-internal-api (.NET REST) |
NODE_API_URL |
https://{env}/api/ — ice-nodejs REST routes |
API_EMAILURL |
https://{env}/ICEInternalApi/Email/api/ — email API |
SOCKET_URL |
https://{env} — Socket.IO connection base URL |
LOGINURL |
Agent login page |
LOGOUTURL |
Agent logout URL |
KB_BOT |
Array of Knowledge Base bot Skill IDs |
DEV_KB_Server |
AI knowledge base API endpoint (qevalpro.ai) |
For local dev: NODE_API_URL = 'http://127.0.0.1:3000/api/' and SOCKET_URL = 'http://127.0.0.1:3000' — so ice-nodejs must be running locally.
VisitorService (src/app/visitor.service.ts)The central service — every component talks to the server through it. Two responsibilities:
1. HTTP calls to API_URL (ice-internal-api):
// Pattern: encrypt body → POST → decrypt response
getToken(orgId): Observable<any> // GET visitor JWT token
offlineSurveyExist(SkillID) // check if offline survey configured
exitSurveyExist(SkillID) // check exit survey
prechatSurveyExist(SkillID) // check pre-chat survey
getSubcampaignDetails(SkillID) // load campaign/skill config
// ... many more
2. Socket.IO connection to SOCKET_URL (ice-nodejs):
// VisitorService.socket = io.connect(AppSettings.getEnvironmentVariable('SOCKET_URL'))
// All real-time events go through this single socket instance
Encryption: every request body is encrypted via utilService.encryptRequest() and every response is decrypted via utilService.decryptResponse() — using crypto-js (AES). If you inspect network traffic, request/response bodies will be ciphertext.
UtilService (src/app/util.service.ts)Utility helpers used across the app:
encryptRequest(body) / decryptResponse(response) — AES encryption via crypto-jsgetLocalData(key) / setLocalData(key, value) — localStorage wrapperGetCookie(), SetCookie(), DeleteCookie()ScrollEnd(), ScrollStart() — chat scroll managementAddDragableEvent() — draggable chat widgetMinimizePopup() — minimize/restore chat windowModalService (src/app/modal.service.ts)Controls three modals:
AlertModalComponent — generic alert/error displayEndChatModalComponent — confirm end-chat dialogTranscriptModalComponent — email transcript dialog (has transcriptEmailId and sendEmailTranscript)AppComponent (shell, handles routing)
└── Router Outlet
├── VisitorComponent (186 edges — god node of Angular side)
│ ├── Uses: VisitorService, UtilService, ModalService
│ ├── Controls all socket event handlers
│ ├── Manages chat message flow
│ └── Triggers: ChatbotComponent, all Modals
│
├── ChatbotComponent (18 edges)
│ └── Bot intent/story flow before human handoff
│
└── BlankComponent (placeholder/loading state)
Additional (standalone routes from AppModule):
├── ChatbotsurveyComponent → post-chatbot survey flow
└── PaypalComponent → payment-via-chat flow
VisitorComponent has 186 graph edges — it directly references almost everything. This is normal for a single-page chat widget where one component manages the entire chat session lifecycle.
app.module.ts)| Module | Purpose |
|---|---|
RouterModule + HashLocationStrategy |
Routing with hash URLs |
ModalModule (ngx-bootstrap) |
Bootstrap modal dialogs |
NgSelectModule |
Dropdown selects (skill selection, etc.) |
NgxSliderModule |
Sliders (volume control, etc.) |
NgScrollbarModule |
Custom scrollbar for chat transcript |
SocialLoginModule |
Google/Facebook login (currently commented out) |
BlockUIModule |
Loading overlay while API calls resolve |
TranslateModule |
i18n via ngx-translate, loads from assets/i18n/*.json |
FormsModule + ReactiveFormsModule |
Template + reactive forms |
cd ice-visitor
npm install
npm start # ng serve → http://localhost:4200
With localhost hostname, AppSettings auto-selects local dev URLs. Requires ice-nodejs running on port 3000 and ice-internal-api (.NET) accessible on localhost.
ng build # output to dist/vs-console/
The built files are served by the ASP.NET host — the Angular app lives inside the .NET host's wwwroot or equivalent directory.
ice-visitor Talks to ice-nodejsVisitor opens page
│
├─ VisitorService calls API_URL/Visitor/GenerateTokenForVisitor
│ → gets visitor JWT token from ice-internal-api
│
├─ VisitorService connects Socket.IO to SOCKET_URL
│ → ice-nodejs socket.js accepts connection
│
├─ VisitorComponent emits socket event (e.g. "initChat")
│ → ice-nodejs processes, queries SQL Server via dalayer.js
│ → ice-nodejs emits back confirmation
│
├─ Agent accepts chat (from operator console)
│ → ice-nodejs emits to visitor socket: "operatorJoined" (or similar)
│ → VisitorComponent updates UI to show agent connected
│
├─ Chat messages: emit/on "message" events bidirectionally
│
└─ Chat ends: dispose events → ice-nodejs saves transcript via dalayer.js
ice-visitor → ice-internal-api (NOT ice-nodejs)Most configuration and survey data comes from the .NET API (ICEInternalAPI/Api/), not ice-nodejs. Examples from VisitorService:
// Visitor/GenerateTokenForVisitor — get auth token
// Visitor/CheckPrechatSurveyExist — does a pre-chat survey exist for this skill?
// Visitor/CheckExitSurveyExist
// Visitor/CheckOfflineSurveyExist
All these POST to ICEInternalAPI/Api/Visitor/{Action} with encrypted bodies.
| Project | Stack | Your Involvement |
|---|---|---|
ice-nodejs |
Node.js 18, Express 4, Socket.IO 2, Redis, MSSQL | High — real-time chat server, all socket logic |
ice-visitor |
Angular 18, TypeScript 5.4 | High — visitor chat widget |
ice-internal-api |
.NET 6 WebAPI, C# | Low — you'll call it but probably not modify it |
ice-mvc-admin |
ASP.NET MVC + vanilla JS | Low — admin UI, not your domain |
.md Files01-ICE-Product-Overview.md — Verdict: Mostly Accurate| Claim | Status | Notes |
|---|---|---|
| Browser-based, no client install | Correct | |
| SSL/256-bit encrypted chat | Correct | PFX cert in expressserver.js, HSTS via helmet |
| Role IDs: 1=Admin, 2=Agent, 3=Supervisor | Correct | Confirmed in CreateUser API + app.settings.ts (OPERATOR_PERMISSION = 2) |
| JWT-style Token returned by Login | Correct | jsonwebtoken in ice-nodejs deps |
| Rule engine on client side | Correct | chatbuttonscript/RuleHandler.js runs in browser, POSTs to /RuleServer |
| Bot is intent/story-based, not LLM | Partially outdated | sandra_process.js handles classic intent bot; app.settings.ts now has DEV_KB_Server pointing to qevalpro.ai (an LLM endpoint) — AI KB integration exists |
| Proactive chat via Rule Engine | Correct | |
| Socket.io for real-time | Not mentioned — worth knowing |
02-ICE-API-Reference.md — Verdict: Accurate for Public API; Incomplete for Internal| Claim | Status | Notes |
|---|---|---|
POST https://www.enterice.com/ICEAPI/api/... base URL |
Correct for public API | Angular code hits ICEInternalAPI/Api/ (different namespace) |
| Token in body, not header | Correct | Confirmed from source |
OrganisationID on Login, OrganizationID elsewhere |
Correct — genuine inconsistency | Handle with case-insensitive parsing |
Message: "Success" vs message: "Failed" inconsistency |
Correct | |
GetChatTranscript is GET, others are POST |
Correct | |
| Three separate auth tokens (Chat/Email/Data API) | Correct | |
| No MongoDB — MSSQL is the DB | Not stated | DB is MSSQL with stored procedures, NOT MongoDB |
| Socket.IO API not documented | Gap | The md files cover REST only; real-time events via Socket.IO are undocumented |
ice-nodejs serves NODE_API_URL (/api/) |
Not mentioned | The visitor widget calls two separate API namespaces |
# All socket events are in socket.js
grep -n "socket.on\|io.on" ice-nodejs/socket.js | head -40
VisitorServiceAPI_URL (→ ice-internal-api, .NET) or NODE_API_URL (→ ice-nodejs)NODE_API_URL: find Express route in expressserver.js — it's either /ChatBtn or /RuleServerAPI_URL: you're in ice-internal-api territory (C#, separate project)// In visitor.service.ts, follow existing pattern:
myNewCall(param: string): Observable<any> {
let body = { 'Token': this.utilService.getLocalData('visitorToken'), 'MyParam': param };
const encryptedBody = this.utilService.encryptRequest(body);
return this.http.post(
AppSettings.getEnvironmentVariable('API_URL') + 'Visitor/MyNewEndpoint',
encryptedBody,
{ headers: this.headers }
).pipe(map((response: any) => {
if (response) return this.utilService.decryptResponse(response);
return response;
}));
}
ice-nodejs/ErrorLogs/Errorlogs.log ← rotates daily
ice-visitor uses socket.io-client 2.4.0. ice-nodejs uses socket.io 2.0.2. Do not upgrade either independently — the client and server socket.io major versions must stay matched. A 2.x client is incompatible with a 3.x/4.x server.