Integrated Chat in Angular
This quickstart is for Angular developers. It builds the same flow as part 2 with Angular’s dependency injection, Observables and Signals: a token service, an injected client, a conversation list and live updates.
What you will build
Section titled “What you will build”An Angular app where the signed-in user sees their conversations, sends a message and sees new messages arrive live. It uses the /angular entry points of ccs-chat-user-client and ccs-chat-notifications-client. They need Angular 17 or later and RxJS 7, as optional peer dependencies.
Prerequisites
Section titled “Prerequisites”-
The backend token route from part 2, step 2. Your app calls it to get a Chat token.
-
Both libraries installed from GitHub Packages (see part 1 for the
.npmrc):Terminal window npm install @central-code-station/ccs-chat-user-client @central-code-station/ccs-chat-notifications-client
conversations.readmessages.readmessages.sendUser scope: chat:access-
Add the public configuration.
environment.ts // Public values only. Nothing here is a secret, and the web app needs no client id for Chat.export const environment = {chatApiUrl: 'https://chat.centralcodestation.com/api/v1',tenantId: 'fitness',applicationId: 'fitness-app',chatTokenUrl: '/api/chat-token', // the route of your backend that exchanges the token}; -
Write a service that returns the cached Chat token.
It calls your token route and keeps the token until a minute before it expires. The arrow property can be handed to both libraries as
getAccessToken.chat-token.service.ts import { Injectable } from '@angular/core';import { environment } from './quickstart-environment';// Your own sign-in service, which already holds the user's token for your API.declare class AuthService {getAccessToken(): Promise<string>;}@Injectable({ providedIn: 'root' })export class ChatTokenService {private cached: { token: string; expiresAt: number } | null = null;constructor(private readonly auth: AuthService) {}// An arrow property, so it can be passed around as getAccessToken without losing `this`.readonly getAccessToken = async (): Promise<string> => {if (this.cached && this.cached.expiresAt - 60_000 > Date.now()) return this.cached.token;const response = await fetch(environment.chatTokenUrl, {headers: { Authorization: `Bearer ${await this.auth.getAccessToken()}` },});if (!response.ok) throw new Error('chat token unavailable');const { access_token, expires_in } = (await response.json()) as { access_token: string; expires_in: number };this.cached = { token: access_token, expiresAt: Date.now() + expires_in * 1000 };return access_token;};} -
Provide the client.
provideChatUserClientregisters the client under theCHAT_USER_CLIENTtoken. Its factory runs in an injection context, so it caninject()your own services.app.config.ts import { inject, type ApplicationConfig } from '@angular/core';import { provideChatUserClient } from '@central-code-station/ccs-chat-user-client/angular';import { ChatTokenService } from './quickstart-angular-chat-token.service';import { environment } from './quickstart-environment';export const appConfig: ApplicationConfig = {providers: [// The factory runs in an injection context, so it can inject() your own services.provideChatUserClient(() => ({chatApiUrl: environment.chatApiUrl,tenantId: environment.tenantId,applicationId: environment.applicationId,getAccessToken: inject(ChatTokenService).getAccessToken,})),],};Put the provider in a route’s or component’s
providersinstead when the tenant or application varies by route. -
Add a live updates service.
createChatNotificationsis a plain factory, not an@Injectable()class, because the package ships plain JavaScript without an Angular compiler pass. Wrap it in your own service. PassingDestroyRefdisconnects the stream when the service is destroyed.chat-live-updates.service.ts import { DestroyRef, inject, Injectable } from '@angular/core';import { createChatNotifications } from '@central-code-station/ccs-chat-notifications-client/angular';import { ChatTokenService } from './quickstart-angular-chat-token.service';import { environment } from './quickstart-environment';// createChatNotifications is a plain factory, so you wrap it in your own service.@Injectable({ providedIn: 'root' })export class ChatLiveUpdatesService {private readonly notifications = createChatNotifications({chatApiUrl: environment.chatApiUrl,tenantId: environment.tenantId,applicationId: environment.applicationId,getAccessToken: inject(ChatTokenService).getAccessToken,},inject(DestroyRef), // disconnects when this service is destroyed);readonly connectionState = this.notifications.connectionState; // a Signalreadonly messageCreated$ = this.notifications.on$('message.created'); // an Observableconnect(): void {this.notifications.connect();}} -
Use both in a component.
Every method of the injected client returns a cold Observable, like
HttpClient: nothing is sent until you subscribe, and each subscription sends the request again.toSignalsubscribes for you.conversations.component.ts import { Component, inject } from '@angular/core';import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';import { CHAT_USER_CLIENT } from '@central-code-station/ccs-chat-user-client/angular';import { ChatLiveUpdatesService } from './quickstart-angular-live-updates.service';@Component({selector: 'app-conversations',standalone: true,template: `<p>Live updates: {{ live.connectionState() }}</p>@for (conversation of conversations()?.items ?? []; track conversation.conversation_id) {<button (click)="send(conversation.conversation_id)">Say hi in {{ conversation.conversation_id }}</button>}`,})export class ConversationsComponent {private readonly chat = inject(CHAT_USER_CLIENT);protected readonly live = inject(ChatLiveUpdatesService);// Every method returns a cold Observable: nothing is sent until you subscribe.protected readonly conversations = toSignal(this.chat.conversations.list());constructor() {this.live.connect();this.live.messageCreated$.pipe(takeUntilDestroyed()).subscribe(({ message }) => console.log(message.sender_user_id, message.body));}send(conversationId: string): void {this.chat.messages.send(conversationId, { body: 'Hi!' }).subscribe();}}
What the adapters do not do
Section titled “What the adapters do not do”- They provide no
@Injectable()classes: useprovideChatUserClientandCHAT_USER_CLIENT, or your own service. - There is no cache. If you use a data layer, call
chat.client, the promise-basedChatUserClientunderneath, from it. createChatUserClient(options)builds the Observable client without dependency injection, for tests or non-DI code.- Web push has no Angular wrapper: use
ChatPushClientfrom your own service. See Web push.
Verify
Section titled “Verify”- Sign in to your app as a user that your backend added to a conversation. The list shows that conversation.
- The text “Live updates: connected” shows once the stream opens.
- Select Say hi. The other member of the conversation, signed in elsewhere, gets the message without a reload. Their browser console logs the sender and the body.
If the connection state stays error or disconnected, check that the token route returns a token and that your origin is registered. See Troubleshooting.
Next steps
Section titled “Next steps”- Full Angular wiring with providers, DI tokens and Signals.
- Live updates and list and read.
- Package pages:
ccs-chat-user-clientfor Angular andccs-chat-notifications-clientfor Angular.