Skip to content

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.

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.

  • 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
Chat permissions (all of): conversations.readmessages.readmessages.sendUser scope: chat:access
  1. 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
    };
  2. 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;
    };
    }
  3. Provide the client.

    provideChatUserClient registers the client under the CHAT_USER_CLIENT token. Its factory runs in an injection context, so it can inject() 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 providers instead when the tenant or application varies by route.

  4. Add a live updates service.

    createChatNotifications is a plain factory, not an @Injectable() class, because the package ships plain JavaScript without an Angular compiler pass. Wrap it in your own service. Passing DestroyRef disconnects 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 Signal
    readonly messageCreated$ = this.notifications.on$('message.created'); // an Observable
    connect(): void {
    this.notifications.connect();
    }
    }
  5. 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. toSignal subscribes 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();
    }
    }
  • They provide no @Injectable() classes: use provideChatUserClient and CHAT_USER_CLIENT, or your own service.
  • There is no cache. If you use a data layer, call chat.client, the promise-based ChatUserClient underneath, from it.
  • createChatUserClient(options) builds the Observable client without dependency injection, for tests or non-DI code.
  • Web push has no Angular wrapper: use ChatPushClient from your own service. See Web push.
  1. Sign in to your app as a user that your backend added to a conversation. The list shows that conversation.
  2. The text “Live updates: connected” shows once the stream opens.
  3. 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.