| name | lfx-intercom |
| description | Everything Intercom for LFX — Angular app integration (code) and Fin AI optimization (support/CX). Use this skill for: adding or fixing Intercom in an LFX Angular app, auditing integrations against the LFX canonical pattern, correcting missing JWT pre-set, broken shutdown, missing Auth0 claim, wrong app IDs, or absent CSP entries — AND for Fin Guidance writing, Help Center optimization, resolution rate improvement, Fin escalation patterns, Copilot tips, Topics Explorer, Fin Attributes, daily review rituals, and Fin best practices. Routes to the right section based on context. Trigger on: any Intercom question, "Fin tips", "improve Fin", "Fin guidance", "Fin resolution rate", "Help Center optimization", "Copilot tips", "Angular Intercom", "IntercomService", "JWT Intercom", "Fin re-engagement", "Fin handoff", or any Intercom-related support or development question.
|
| allowed-tools | Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion |
LFX Intercom Skill
This skill covers everything Intercom at LFX — both Angular app integration and Fin AI optimization.
Step 0 — Detect Context
Before proceeding, determine what the user needs based on what they said. Only ask if genuinely ambiguous.
Code Integration (developer path)
Adding, fixing, or auditing Intercom in an LFX Angular app → continue with the steps below.
Fin & Content Optimization (support/CX path)
Writing Fin Guidance, improving resolution rates, Help Center content, escalation patterns,
Copilot, Fin Attributes, or Fin best practices → read references/fin-best-practices.md and advise from there.
Code Integration
You are bringing Intercom up to the LFX standard in an Angular application. This
skill handles both fresh installs and fixing/standardizing existing integrations.
Follow every step in order — the audit step (Step 2) determines which fixes are
needed. Do not skip the Auth0 section — without it, identity verification will
silently fail.
Step 1 — Gather Context
Ask the user:
- Goal — Are you adding Intercom for the first time, or fixing/standardizing
an existing integration?
- App name — What is the exact Auth0 client name for this app? (e.g. "LFX
Project Control Center", "CB Funding"). This must match the
case in the
Auth0 custom_claims action exactly.
- Public pages? — Is this app accessible to non-authenticated visitors?
- Yes (e.g. Mentorship, Crowdfunding, Insights) → Intercom must boot
anonymously on page load so banners/popups are visible to all visitors,
then upgrade to identified on login.
- No (e.g. PCC, Org Dashboard, Individual Dashboard, Security) → Intercom
boots only after login. No anonymous boot needed.
- Angular version — Angular 6 (ngrx) or Angular 14+ (standalone/signals)?
- LaunchDarkly — Does this app use LaunchDarkly? If yes, Intercom should be
feature-flagged behind
enable-intercom.
- Intercom App ID — Dev:
mxl90k6y, Prod: w29sqomy (shared across all
LFX apps; already set in Step 3 — just confirm the user hasn't been given
different IDs by the Intercom admin).
Step 2 — Audit Existing Integration
Search the repo for any existing Intercom integration before writing any code.
Check all of the following and produce a gap report:
| Check | What to look for | LFX Standard |
|---|
| IntercomService | Does intercom.service.ts exist? | Direct script injection, isLoaded + isBooted + isLoading + bootedWithIdentity state, boot() returns Promise<void> |
| npm package | @intercom/messenger-js-sdk or similar in package.json | ❌ Not allowed — use script injection |
| Intercom stub | Does initializeIntercomFunction() create the i.q stub? | ✅ Required — queues commands before script loads |
| JWT pre-set | Is window.intercomSettings.intercom_user_jwt set before window.Intercom('boot') is called? | ✅ Required |
| JWT stripped from boot options | Is intercom_user_jwt removed from the options passed to window.Intercom('boot')? | ✅ Required — JWT only in intercomSettings, not boot payload |
| Anonymous boot | Is bootIntercomAnonymous() called in ngOnInit() before user auth check? | ✅ Required if app has public pages; skip if auth-only app |
| Anonymous→identified upgrade | Does boot() detect anonymous session and upgrade to identified via shutdownForReboot()? | ✅ Required if anonymous boot is used — bootedWithIdentity flag tracks session type |
| Identified boot | Is identified boot() called inside userProfile$ subscription with intercomBootAttempted guard? | ✅ Required |
| Shutdown on logout | Is Intercom('shutdown') called, JWT cleared, and anonymous session re-booted on logout? | ✅ Required |
| App IDs | Dev: mxl90k6y, Prod: w29sqomy | ✅ Shared across all LFX apps |
| Auth0 claim | Is http://lfx.dev/claims/intercom used (not the deprecated HMAC)? | ✅ JWT claim only |
| CSP | Are ALL Intercom domains in the Content Security Policy, including WebSocket entries? | ✅ Required if CSP exists |
| environment vars | Are all 4 env fields present in both environment.ts and environment.prod.ts? | ✅ Required |
After the audit, tell the user what is already correct, what is missing, and what
needs to be fixed. Then proceed only with the steps that address identified gaps.
If nothing is wrong, say so and exit — do not make unnecessary changes.
Step 3 — Add Environment Variables
Add to environment.ts:
intercomId: 'mxl90k6y',
intercomApiBase: 'https://api-iam.intercom.io',
auth0IntercomClaim: 'http://lfx.dev/claims/intercom',
auth0UsernameClaim: 'https://sso.linuxfoundation.org/claims/username',
Add to environment.prod.ts:
intercomId: 'w29sqomy',
intercomApiBase: 'https://api-iam.intercom.io',
auth0IntercomClaim: 'http://lfx.dev/claims/intercom',
auth0UsernameClaim: 'https://sso.linuxfoundation.org/claims/username',
Also add these fields to the Environment interface if one exists.
Step 4 — Generate IntercomService
Create src/app/services/intercom.service.ts (or
src/app/shared/services/intercom.service.ts — match existing service
placement). This is the canonical LFX implementation validated across Mentorship,
Crowdfunding, and PCC.
⚠️ Adjust the environment import path to match the chosen folder depth,
e.g. ../../environments/environment from src/app/services/ or
../../../environments/environment from src/app/shared/services/.
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
export interface IntercomBootOptions {
api_base?: string;
app_id?: string;
user_id?: string;
name?: string;
email?: string;
created_at?: number;
intercom_user_jwt?: string;
[key: string]: any;
}
declare global {
interface Window {
Intercom?: any;
intercomSettings?: any;
}
}
@Injectable({ providedIn: 'root' })
export class IntercomService {
private isLoaded = false;
private isBooted = false;
private isLoading = false;
private bootedWithIdentity = false;
public boot(options: IntercomBootOptions): Promise<void> {
return new Promise((resolve, reject) => {
if (typeof window === 'undefined') {
reject(new Error('Window is undefined'));
return;
}
if (!environment.intercomId) {
reject(new Error('No Intercom ID configured'));
return;
}
if (this.isBooted) {
if (options.user_id && !this.bootedWithIdentity) {
this.shutdownForReboot();
} else {
const { intercom_user_jwt: _jwt, app_id: _appId, api_base: _apiBase, ...userOptions } = options;
this.update(userOptions);
resolve();
return;
}
}
if (!this.isLoaded && !this.isLoading) {
this.isLoading = true;
this.loadIntercomScript();
}
if (options.intercom_user_jwt) {
window.intercomSettings = window.intercomSettings || {};
window.intercomSettings.intercom_user_jwt = options.intercom_user_jwt;
}
const checkLoaded = setInterval(() => {
if (this.isLoaded && window.Intercom) {
clearInterval(checkLoaded);
clearTimeout(timeoutHandle);
if (this.isBooted) {
if (options.user_id && !this.bootedWithIdentity) {
this.shutdownForReboot();
} else {
const { intercom_user_jwt: _jwt, app_id: _appId, api_base: _apiBase, ...userOptions } = options;
this.update(userOptions);
resolve();
return;
}
}
this.isBooted = true;
try {
const { intercom_user_jwt: _jwt, ...bootOptions } = options;
window.Intercom('boot', {
api_base: environment.intercomApiBase,
app_id: environment.intercomId,
...bootOptions,
});
this.bootedWithIdentity = !!bootOptions.user_id;
if (bootOptions.user_id) {
try {
window.Intercom('update', {
user_id: bootOptions.user_id,
name: bootOptions.name,
email: bootOptions.email,
});
} catch (updateError) {
console.warn('IntercomService: Update after boot failed', updateError);
}
}
resolve();
} catch (error) {
this.isBooted = false;
console.error('IntercomService: Boot failed', error);
reject(error);
}
}
}, 100);
const timeoutHandle = setTimeout(() => {
clearInterval(checkLoaded);
if (!this.isBooted) {
this.isLoading = false;
reject(new Error('Intercom script failed to load — check network, CSP, or ad blockers'));
}
}, 10000);
});
}
public update(data?: Partial<IntercomBootOptions>): void {
if (typeof window !== 'undefined' && window.Intercom && this.isBooted) {
try {
window.Intercom('update', data || {});
} catch (error) {
console.error('IntercomService: Update failed', error);
}
}
}
public show(): void {
if (typeof window !== 'undefined' && window.Intercom && this.isBooted) {
try {
window.Intercom('show');
} catch (error) {
console.error('IntercomService: Show failed', error);
}
}
}
public hide(): void {
if (typeof window !== 'undefined' && window.Intercom && this.isBooted) {
try {
window.Intercom('hide');
} catch (error) {
console.error('IntercomService: Hide failed', error);
}
}
}
public shutdown(): void {
if (typeof window !== 'undefined') {
if (window.intercomSettings?.intercom_user_jwt) {
delete window.intercomSettings.intercom_user_jwt;
}
if (window.Intercom && this.isBooted) {
try {
window.Intercom('shutdown');
this.isBooted = false;
this.bootedWithIdentity = false;
} catch (error) {
console.error('IntercomService: Shutdown failed', error);
}
}
}
}
private shutdownForReboot(): void {
if (typeof window !== 'undefined' && window.Intercom) {
try {
window.Intercom('shutdown');
} catch (error) {
console.warn('IntercomService: Shutdown for reboot failed', error);
}
}
this.isBooted = false;
this.bootedWithIdentity = false;
}
public trackEvent(eventName: string, metadata?: Record<string, any>): void {
if (typeof window !== 'undefined' && window.Intercom && this.isBooted) {
try {
window.Intercom('trackEvent', eventName, metadata);
} catch (error) {
console.error('IntercomService: Track event failed', error);
}
}
}
public isIntercomBooted(): boolean {
return this.isBooted;
}
private loadIntercomScript(): void {
if (this.isLoaded || typeof window === 'undefined') {
return;
}
this.initializeIntercomFunction();
window.intercomSettings = {
api_base: environment.intercomApiBase,
app_id: environment.intercomId,
};
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = `https://widget.intercom.io/widget/${environment.intercomId}`;
script.onload = () => {
this.isLoaded = true;
this.isLoading = false;
};
script.onerror = error => {
this.isLoading = false;
console.error('IntercomService: Failed to load script', error);
};
const firstScript = document.getElementsByTagName('script')[0];
if (firstScript?.parentNode) {
firstScript.parentNode.insertBefore(script, firstScript);
} else {
(document.head || document.body).appendChild(script);
}
}
private initializeIntercomFunction(): void {
if (typeof window === 'undefined') {
return;
}
const w = window as any;
const ic = w.Intercom;
if (typeof ic === 'function') {
ic('reattach_activator');
ic('update', w.intercomSettings);
} else {
const i: any = (...args: any[]) => { i.c(args); };
i.q = [];
i.c = (args: any) => { i.q.push(args); };
w.Intercom = i;
}
}
}
Step 5 — Wire into app.component.ts
The Intercom lifecycle has three phases: anonymous boot on page load, identified
upgrade on login, and shutdown + anonymous re-boot on logout.
5a — Class field and anonymous boot in ngOnInit
private intercomBootAttempted = false;
If the app has public pages (Step 1, question 3 = Yes), add the anonymous
boot call in ngOnInit() BEFORE the auth subscription:
ngOnInit() {
this.bootIntercomAnonymous();
this.userSettings();
}
If the app is auth-only (Step 1, question 3 = No), skip the anonymous boot —
Intercom will boot only when the user logs in (Step 5b).
5b — Identified boot on login + shutdown on logout
Inside the auth.userProfile$ subscription in userSettings():
if (userProfile) {
if (!this.intercomBootAttempted && environment.intercomId) {
const intercomJwt = userProfile[environment.auth0IntercomClaim];
const userId = userProfile[environment.auth0UsernameClaim];
if (userId && intercomJwt) {
this.intercomBootAttempted = true;
this.intercomService
.boot({
api_base: environment.intercomApiBase,
app_id: environment.intercomId,
intercom_user_jwt: intercomJwt,
user_id: userId,
name: userProfile.name,
email: userProfile.email,
})
.catch((error: any) => {
console.error('AppComponent: Failed to boot Intercom', error);
this.intercomBootAttempted = false;
});
} else {
console.warn('AppComponent: Intercom not booted — missing required claim(s)', {
hasUserId: !!userId,
hasIntercomJwt: !!intercomJwt,
});
}
}
} else if (userProfile == null) {
if (this.intercomBootAttempted) {
this.intercomService.shutdown();
this.intercomBootAttempted = false;
this.bootIntercomAnonymous();
}
}
⚠️ Use == null (loose equality) for the logout check — this catches both
null and undefined, which different auth services may emit.
5c — Anonymous boot helper method (public-page apps only)
Include this method only if the app has public pages (Step 1, question 3 = Yes).
Auth-only apps do not need this method — remove the bootIntercomAnonymous() calls
from ngOnInit and the logout block if the app is auth-only.
private bootIntercomAnonymous() {
if (environment.intercomId) {
this.intercomService
.boot({
app_id: environment.intercomId,
api_base: environment.intercomApiBase,
})
.catch((error: any) => {
console.warn('AppComponent: Anonymous Intercom boot failed', error);
});
}
}
Boot lifecycle summary
Public-page apps (Mentorship, Crowdfunding, Insights):
Page Load
→ bootIntercomAnonymous() // banners visible to all visitors
→ Intercom boots with no user_id // bootedWithIdentity = false
User Logs In (userProfile$ emits user)
→ boot({ user_id, intercom_user_jwt, ... })
→ IntercomService detects bootedWithIdentity === false
→ shutdownForReboot() // clears anonymous session
→ Intercom re-boots with identity // bootedWithIdentity = true
User Logs Out (userProfile$ emits null)
→ shutdown() // clears identified session + JWT
→ intercomBootAttempted = false
→ bootIntercomAnonymous() // banners visible again
Auth-only apps (PCC, Org Dashboard, Individual Dashboard, Security):
Page Load
→ (nothing — user must log in first)
User Logs In (userProfile$ emits user)
→ boot({ user_id, intercom_user_jwt, ... })
→ Intercom boots with identity // bootedWithIdentity = true
User Logs Out (userProfile$ emits null)
→ shutdown() // clears identified session + JWT
→ intercomBootAttempted = false
If the app uses LaunchDarkly, wrap both the anonymous and identified boot
blocks:
if (this.ldClient.variation('enable-intercom', false)) {
} else {
console.info('Intercom: Disabled by LaunchDarkly feature flag');
}
Step 6 — Auth0 Configuration (REQUIRED)
⚠️ This step is required. Without it, the http://lfx.dev/claims/intercom
JWT claim will not be present in the user's token and Intercom will boot without
identity verification — a security issue.
What needs to happen
The Auth0 custom_claims Action in the auth0-terraform repo must be updated
to add your app to the switch statement that generates the Intercom JWT claim.
File to modify
auth0-terraform/src/actions/custom_claims.js
Change required
Add a new case block to the switch (event.client.name) statement:
case "Your App Name Here": {
api.idToken.setCustomClaim(
`${lfPrefix}intercom`,
intercomHMAC(event.user.username),
);
api.idToken.setCustomClaim(
`${lfxPrefix}intercom`,
await intercomJWT(event.user.username),
);
break;
}
Replace "Your App Name Here" with the exact Auth0 client name for your
app (case-sensitive, must match event.client.name exactly).
How the JWT is generated
- Secret: Stored in AWS Secrets Manager at
/cloudops/managed-secrets/cloud/intercom/secret_key
- Algorithm: HS256
- Expiry: 12 hours
- Payload:
{ user_id, email, name? }
- The secret is automatically injected into the Auth0 Action via Terraform —
no manual secret management needed once it's in the switch statement.
Who to contact
Raise a PR against auth0-terraform or ask the platform/infra team to add your
app. This is a Terraform-managed change and requires deployment to dev, staging,
and prod Auth0 tenants.
How to verify
After the Auth0 change is deployed, decode a fresh ID token for your app (e.g.
using jwt.io) and confirm http://lfx.dev/claims/intercom is present and
contains a valid JWT with user_id, email fields.
Step 7 — Verify the Integration
- Run the app locally using
127.0.0.1 (not localhost — see Notes below)
- Before logging in: verify Intercom loads (check console for
IntercomService: Script loaded successfully) — banners/popups should be
visible to anonymous visitors
- Log in and check that the console shows
IntercomService: Upgrading from anonymous to identified session
- Verify the Intercom chat bubble appears with your identity
- Open browser console and run:
window.Intercom('getVisitorId') — should
return a string, not an error
- Log out and confirm the console shows
Intercom('shutdown') followed by a
fresh anonymous boot — banners should remain visible
- Decode the Auth0 ID token and confirm
http://lfx.dev/claims/intercom is
present (if Auth0 change is deployed)
- In Intercom dashboard, confirm the user appears with correct name/email
Keeping This Skill Up to Date
Canonical reference app: LFX Mentorship (jobspring / lfx-mentorship-upgrade
repo) is the source of truth for the LFX Intercom pattern. When in doubt about
what "correct" looks like, check how Mentorship implements it — Crowdfunding and
PCC follow the same pattern and can be used for cross-validation.
If you find this skill is outdated: Update SKILL.md in the same PR where
you fix the app. Do not defer it. The skill is wrong for everyone until it's
fixed.
Last validated: 2026-03-24 against LFX Mentorship (PRs #147, #148),
Crowdfunding (PRs #31-#38), and PCC.
Notes