Skip to main content

How to Subscribe to Salesforce Platform Events in LWC Using empApi: Step-by-Step Guide

In plain words: Platform Events in Salesforce use a secure publish-subscribe architecture to broadcast notifications across systems. In Lightning Web Components (LWC), the built-in lightning/empApi module allows your component to listen to these streaming events in real time without refreshing the page or repeatedly polling the server.

Modern enterprise applications rely on event-driven architecture to keep user interfaces updated in real time. By subscribing to Platform Events directly inside an LWC via streaming APIs, you can trigger UI toast alerts, refresh datatables, or update progress bars the moment external systems or asynchronous Apex jobs complete.

Prerequisites

  • A Salesforce Developer Edition, Sandbox, or Scratch Org.
  • Salesforce CLI (sf) installed and authenticated to your org.
  • Basic understanding of LWC component lifecycle hooks (connectedCallback and disconnectedCallback).

Step 1: Define the Platform Event Schema

First, create the Platform Event definition in Salesforce Setup:

Configuration Steps:
  • Navigate to Setup > Platform Events and click New Platform Event.
  • Set Label to Order Notification and Plural Label to Order Notifications (API Name: Order_Notification__e).
  • Set Publish Behavior to Publish After Commit.
  • Create a custom text field: Message__c (Length: 255).
  • Save the event definition.

Step 2: Build the LWC Event Subscriber Component

Generate a new Lightning Web Component named platformEventListener:

sf lightning generate component -n platformEventListener -d force-app/main/default/lwc --type lwc

In platformEventListener.html, create the UI to display subscription status and received real-time event payloads:

<template>
    <lightning-card title="Real-Time Platform Event Monitor" icon-name="utility:broadcast">
        <div class="slds-p-around_medium">
            <div class="slds-m-bottom_small">
                <p class="slds-text-body_regular">
                    Listening Channel: <code class="ap-code">{channelName}</code>
                </p>
                <p class="slds-text-body_regular slds-m-top_xx-small">
                    Subscription Status: 
                    <strong class={subscriptionStatusClass}>{subscriptionStatusText}</strong>
                </p>
            </div>

            <div class="slds-button-group slds-m-bottom_medium" role="group">
                <lightning-button 
                    variant="brand" 
                    label="Subscribe" 
                    onclick={handleSubscribe} 
                    disabled={isSubscribed}>
                </lightning-button>
                <lightning-button 
                    variant="neutral" 
                    label="Unsubscribe" 
                    onclick={handleUnsubscribe} 
                    disabled={isNotSubscribed}>
                </lightning-button>
                <lightning-button 
                    variant="destructive-text" 
                    label="Clear Log" 
                    onclick={handleClearLogs} 
                    disabled={isLogEmpty}>
                </lightning-button>
            </div>

            <!-- Event Log Output -->
            <div class="slds-box slds-theme_shade">
                <h3 class="slds-text-heading_small slds-m-bottom_x-small">Received Event Stream:</h3>
                <template lwc:if={hasEvents}>
                    <ul class="slds-list_dotted">
                        <template for:each={receivedEvents} for:item="evt">
                            <li key={evt.id} class="slds-m-bottom_xx-small">
                                <span class="slds-text-color_weak">[{evt.time}]</span> 
                                <strong>{evt.message}</strong> (Replay ID: {evt.replayId})
                            </li>
                        </template>
                    </ul>
                </template>
                <template lwc:else>
                    <p class="slds-text-color_weak">No events received yet. Waiting for incoming stream...</p>
                </template>
            </div>
        </div>
    </lightning-card>
</template>

Step 3: Implement empApi Streaming Logic in JavaScript

In platformEventListener.js, import subscribe, unsubscribe, and onError from lightning/empApi. Ensure the subscription object is retained for clean teardown during disconnectedCallback:

import { LightningElement, track } from 'lwc';
import { subscribe, unsubscribe, onError } from 'lightning/empApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class PlatformEventListener extends LightningElement {
    channelName = '/event/Order_Notification__e';
    isSubscribed = false;
    subscription = {};
    @track receivedEvents = [];

    get isNotSubscribed() {
        return !this.isSubscribed;
    }

    get hasEvents() {
        return this.receivedEvents.length > 0;
    }

    get isLogEmpty() {
        return this.receivedEvents.length === 0;
    }

    get subscriptionStatusText() {
        return this.isSubscribed ? 'Active (Listening)' : 'Inactive';
    }

    get subscriptionStatusClass() {
        return this.isSubscribed ? 'slds-text-color_success' : 'slds-text-color_error';
    }

    connectedCallback() {
        this.registerErrorListener();
        this.handleSubscribe();
    }

    disconnectedCallback() {
        this.handleUnsubscribe();
    }

    handleSubscribe() {
        if (this.isSubscribed) return;

        // Callback invoked whenever an event is published
        const messageCallback = (response) => {
            const payload = response.data.payload;
            const newEvent = {
                id: response.data.schema + '-' + response.data.event.replayId,
                message: payload.Message__c || 'No message provided',
                replayId: response.data.event.replayId,
                time: new Date().toLocaleTimeString()
            };

            this.receivedEvents = [newEvent, ...this.receivedEvents];
            this.showToast('Event Received', payload.Message__c, 'info');
        };

        // Replay from -1 (new events only)
        subscribe(this.channelName, -1, messageCallback).then((response) => {
            this.subscription = response;
            this.isSubscribed = true;
        }).catch(error => {
            this.showToast('Subscription Error', JSON.stringify(error), 'error');
        });
    }

    handleUnsubscribe() {
        if (!this.isSubscribed || !this.subscription.subscription) return;

        unsubscribe(this.subscription, (response) => {
            this.isSubscribed = false;
            this.subscription = {};
        }).catch(error => {
            this.showToast('Unsubscribe Error', JSON.stringify(error), 'error');
        });
    }

    handleClearLogs() {
        this.receivedEvents = [];
    }

    registerErrorListener() {
        onError((error) => {
            console.error('Streaming API Error: ', JSON.stringify(error));
        });
    }

    showToast(title, message, variant) {
        this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
    }
}
Warning Trap: Passing the channel string directly to unsubscribe() will fail. You must pass the subscription object returned by the subscribe() promise resolution. Always clear subscriptions inside disconnectedCallback() to prevent memory leaks and orphaned listeners.
360 Architecture Summary:
  • Replay Options: Pass -1 to receive only new events generated after subscribing, or -2 to replay all retained events within the 24-hour retention window.
  • Streaming Limits: Event delivery counts against your daily CometD/Bayeux streaming client allocation limits.
  • Publishing Engine: Use Apex EventBus.publish(), Salesforce Flows, or external REST/PubSub APIs to publish event payloads.

Step 4: Expose, Deploy, and Test the Event Stream

Update platformEventListener.js-meta.xml to expose the component to Lightning App Builder:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>60.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>
Deployment & Testing Procedure:
  • Deploy changes: sf project deploy start.
  • Open Lightning App Builder and drag platformEventListener onto your target page layout.
  • Open the Developer Console (or execute via VS Code CLI) and publish a test event using Anonymous Apex:
// Test Platform Event Publish in Anonymous Apex
Order_Notification__e evt = new Order_Notification__e(
    Message__c = 'Order #89421 successfully processed by billing gateway.'
);
Database.SaveResult sr = EventBus.publish(evt);
System.debug('Event published successfully: ' + sr.isSuccess());
  • Observe the incoming notification card update in real time on the UI without reloading the browser tab.
Core Takeaway: Subscribing to Platform Events in LWC via lightning/empApi provides clean, real-time client-side event notifications, eliminating the overhead of manual polling and server round-trips.