跳转到主要内容

Plugins

import RTKSDKSelector from "/components/realtimekit/RTKSDKSelector/RTKSDKSelector.astro"; import RTKCodeSnippet from "/components/realtimekit/RTKCodeSnippet/RTKCodeSnippet.astro";

This guide explains how to register, activate, and render plugins in a meeting using the Cloudflare RealtimeKit Core SDK.

Plugins are interactive real-time applications that run inside a meeting, such as a shared whiteboard or a document viewer. When a participant activates a plugin, it becomes active for everyone in the session.

:::note You must register a plugin with the same id on every platform your participants use. A plugin is only visible to a participant who has registered it, and activation state only syncs across participants who share the same plugin id. If a web participant registers a plugin under one id and a mobile participant uses a different id, the plugin will not be visible or synced across both platforms. :::

The Plugins module

The meeting plugins object is available at meeting.plugins. It exposes two collections of Plugin objects:

  • all: every plugin available to the local participant.
  • active: the plugins that are currently running in the session.

<RTKCodeSnippet id={["web-web-components", "web-angular"]}>

// All plugins available to you
const allPlugins = meeting.plugins.all.toArray();

// Plugins currently active in the session
const activePlugins = meeting.plugins.active.toArray();

// Get a single plugin by its id
const plugin = meeting.plugins.all.get(pluginId);

Use the useRealtimeKitSelector hook to read plugins reactively. The hook only works when your component tree is wrapped in RealtimeKitProvider.

import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react";

const allPlugins = useRealtimeKitSelector((m) => m.plugins.all.toArray());
const activePlugins = useRealtimeKitSelector((m) => m.plugins.active.toArray());
// All plugins available to you
val allPlugins = meeting.plugins.all

// Plugins currently active in the session
val activePlugins = meeting.plugins.active

// Get a single plugin by its id
val plugin = meeting.plugins.all.firstOrNull { it.id == pluginId }
// All plugins available to you
let allPlugins = meeting.plugins.all

// Plugins currently active in the session
let activePlugins = meeting.plugins.active

// Get a single plugin by its id
let plugin = meeting.plugins.all.first { $0.id == pluginId }

Use the useRealtimeKitSelector hook to read plugins reactively.

import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react-native";

const allPlugins = useRealtimeKitSelector((m) => m.plugins.all.toArray());
const activePlugins = useRealtimeKitSelector((m) => m.plugins.active.toArray());

// Get a single plugin by its namespaced id
const plugin = useRealtimeKitSelector((m) => m.plugins.all.get(pluginId));

Register a plugin

You register the plugins available in a session when you initialize the SDK. Each configuration provides the metadata RealtimeKit uses to list the plugin and the location it loads.

<RTKCodeSnippet id={["web-react", "web-web-components", "web-angular"]}>

Pass an array of plugin configurations as defaults.plugins. Each configuration provides the metadata RealtimeKit uses to list the plugin and the component it renders.

RealtimeKitClient.init({
	authToken: "<auth_token>",
	defaults: {
		plugins: [
			{
				// User-provided unique id. The SDK prefixes it with
				// `{meetingId}:` to create the namespaced `plugin.id`.
				id: "whiteboard",
				// Display name shown in the plugins panel
				name: "Whiteboard",
				// Icon URL or data URI shown next to the name
				icon: "https://example.com/whiteboard.png",
				// Per-plugin permissions for the local participant
				permissions: {
					canActivate: true,
					canDeactivate: true,
				},
				// The element rendered when the plugin is active
				component: document.createElement("my-whiteboard"),
			},
		],
	},
});

The component is an HTMLElement. The rtk-plugin-main component projects it into the meeting layout, so your application styles continue to apply.

Each plugin configuration accepts the following fields:

Field Description Type Required
id Unique identifier for the plugin. The SDK prefixes it with {meetingId}: to form plugin.id. string true
name Display name shown in the plugins panel. string true
icon Icon URL or data URI shown next to the name. string true
permissions Controls whether the local participant can activate or deactivate the plugin. { canActivate: boolean; canDeactivate: boolean } true
component Element rendered when the plugin is active. HTMLElement true

Pass plugin configurations as defaults.plugins when calling initMeeting. In React Native, component should be an object with a src property containing the URL to render — you can load this in a WebView or use the UI Kit to handle rendering automatically.

const [meeting, initMeeting] = useRealtimeKitClient();

await initMeeting({
  authToken: "<auth_token>",
  defaults: {
    plugins: [
      {
        // User-provided unique id. The SDK prefixes it with
        // `{meetingId}:` to create the namespaced `plugin.id`.
        id: "whiteboard",
        // Display name shown in the plugins panel
        name: "Whiteboard",
        // Icon URL shown next to the name
        icon: "https://example.com/whiteboard.png",
        // Per-plugin permissions for the local participant
        permissions: {
          canActivate: true,
          canDeactivate: true,
        },
        // URL to render when the plugin is active.
        // Pass an object with a src property: { src: string }.
        component: { src: "https://example.com/whiteboard/" },
      },
    ],
  },
});

Each plugin configuration accepts the following fields:

Field Description Type Required
id Unique identifier for the plugin. The SDK prefixes it with {meetingId}: to form plugin.id. string true
name Display name shown in the plugins panel. string true
icon Icon URL shown next to the name. string true
permissions Controls whether the local participant can activate or deactivate the plugin. { canActivate: boolean; canDeactivate: boolean } true
component Object with a src URL to render when the plugin is active. { src: string } true

Pass a list of RtkClientPluginConfig objects as plugins to RtkMeetingInfo. The SDK loads each plugin's url into a WebView when you render the plugin.

val meetingInfo = RtkMeetingInfo(
	authToken = "<auth_token>",
	plugins = listOf(
		RtkClientPluginConfig(
			// User-provided unique id. The SDK prefixes it with
			// `{meetingId}:` to create the namespaced `plugin.id`.
			id = "whiteboard",
			// Display name shown in the plugins list
			name = "Whiteboard",
			// Icon URL shown next to the name
			icon = "https://example.com/whiteboard.png",
			// URL loaded into the plugin WebView when the plugin is active
			url = "https://example.com/whiteboard/",
			// Per-plugin permissions for the local participant
			permissions = RtkClientPluginPermissions(
				canActivate = true,
				canDeactivate = true,
			),
		),
	),
)

Each plugin configuration accepts the following fields:

Field Description Type Required
id Unique identifier for the plugin. The SDK prefixes it with {meetingId}: to form plugin.id. String true
name Display name shown in the plugins list. String true
icon Icon URL shown next to the name. String true
url URL loaded into the plugin WebView when the plugin is active. Must use https://. String true
permissions Controls whether the local participant can activate or deactivate the plugin. RtkClientPluginPermissions true

Pass an array of RtkClientPluginConfig objects as plugins to RtkMeetingInfo. The SDK loads each plugin's url into a WebView when you render the plugin.

let meetingInfo = RtkMeetingInfo(
	authToken: "<auth_token>",
	plugins: [
		RtkClientPluginConfig(
			// User-provided unique id. The SDK prefixes it with
			// `{meetingId}:` to create the namespaced `plugin.id`.
			id: "whiteboard",
			// Display name shown in the plugins list
			name: "Whiteboard",
			// Icon URL shown next to the name
			icon: "https://example.com/whiteboard.png",
			// URL loaded into the plugin WebView when the plugin is active
			url: "https://example.com/whiteboard/",
			// Per-plugin permissions for the local participant
			permissions: RtkClientPluginPermissions(
				canActivate: true,
				canDeactivate: true
			)
		)
	]
)

Each plugin configuration accepts the following fields:

Field Description Type Required
id Unique identifier for the plugin. The SDK prefixes it with {meetingId}: to form plugin.id. String true
name Display name shown in the plugins list. String true
icon Icon URL shown next to the name. String true
url URL loaded into the plugin WebView when the plugin is active. Must use https://. String true
permissions Controls whether the local participant can activate or deactivate the plugin. RtkClientPluginPermissions true

Activate and deactivate a plugin

Activation lives on the Plugin object. Calling activate() enables the plugin for every participant in the session, and deactivate() disables it for everyone. Both methods respect the plugin's permissions.

<RTKCodeSnippet id={["web-web-components", "web-angular"]}>

const plugin = meeting.plugins.all.get(pluginId);

// Activate for all participants
await plugin.activate();

// Deactivate for all participants
await plugin.deactivate();

<RTKCodeSnippet id={["web-react", "mobile-react-native"]}>

const plugins = useRealtimeKitSelector((m) => m.plugins);

const plugin = plugins.all.get(pluginId);

// Activate for all participants
await plugin.activate();

// Deactivate for all participants
await plugin.deactivate();
val plugin = meeting.plugins.all.firstOrNull { it.id == pluginId } ?: return

// Activate for all participants
plugin.activate()

// Deactivate for all participants
plugin.deactivate()
guard let plugin = meeting.plugins.all.first(where: { $0.id == pluginId }) else { return }

// Activate for all participants
plugin.activate()

// Deactivate for all participants
plugin.deactivate()

:::note A participant can only activate a plugin when permissions.canActivate is true. A participant can deactivate a plugin when permissions.canDeactivate is true, or when they are the participant who activated it. :::

The Plugin object

A Plugin object represents a single plugin. You obtain it from either collection in meeting.plugins.

<RTKCodeSnippet id={["web-react", "web-web-components", "web-angular"]}>

Property Description Type
id Namespaced plugin id, in the form {meetingId}:{configId}. string
name Display name of the plugin. string
icon Icon URL or data URI. string
permissions Activation permissions for the local participant. { canActivate: boolean; canDeactivate: boolean }
component Element rendered when the plugin is active. HTMLElement
active Whether the plugin is currently running. boolean
enabledBy Id of the participant who activated the plugin. string
Property Description Type
id Namespaced plugin id, in the form {meetingId}:{configId}. String
name Display name of the plugin. String
icon Icon URL. String
permissions Activation permissions for the local participant. RtkClientPluginPermissions
isActive Whether the plugin is currently running. Boolean
enabledBy Id of the participant who activated the plugin. String?

To display an active plugin, refer to Render plugins. To exchange data with it, refer to Exchange messages with a plugin.

Property Description Type
id Namespaced plugin id, in the form {meetingId}:{configId}. String
name Display name of the plugin. String
icon Icon URL. String
permissions Activation permissions for the local participant. RtkClientPluginPermissions
isActive Whether the plugin is currently running. Bool
enabledBy Id of the participant who activated the plugin. String?

To display an active plugin, refer to Render plugins. To exchange data with it, refer to Exchange messages with a plugin.

Property Description Type
id Namespaced plugin id, in the form {meetingId}:{configId}. string
name Display name of the plugin. string
icon Icon URL. string
permissions Activation permissions for the local participant. { canActivate: boolean; canDeactivate: boolean }
component The object provided at registration. Contains a src URL that your rendering layer loads. { src: string }
active Whether the plugin is currently running. boolean
enabledBy Id of the participant who activated the plugin. string

Listen to plugin events

<RTKCodeSnippet id={["web-react", "web-web-components", "web-angular", "mobile-react-native"]}>

A Plugin object emits events as its state changes. You can listen on a single plugin, or on a map to receive events for every plugin it contains.

Event Description
stateUpdate Emitted when the plugin is activated or deactivated.
enabled Emitted when the plugin becomes active for the local participant.
closed Emitted when the plugin is deactivated for the local participant.
ready Emitted when the plugin is ready to use.
const plugin = meeting.plugins.all.get(pluginId);

plugin.on("stateUpdate", ({ active, pluginId }) => {
	console.log(`Plugin ${pluginId} active:`, active);
});

// Listen for any plugin being added to or removed from the map
meeting.plugins.all.on("pluginAdded", (plugin) => {
	console.log("Plugin added:", plugin.name);
});

meeting.plugins.all.on("pluginDeleted", (plugin) => {
	console.log("Plugin removed:", plugin.name);
});

Register an RtkPluginsEventListener to receive plugin events.

Callback Description
onPluginActivated Called when a plugin is activated for all participants.
onPluginDeactivated Called when a plugin is deactivated for all participants.
onPluginMessage Called when a plugin's web page sends a message to the app. Refer to Exchange messages with a plugin.
onPluginFileRequest Called when a plugin requests a file from the app.
val pluginsEventListener = object : RtkPluginsEventListener {
	override fun onPluginActivated(plugin: RtkPlugin) {
		// A plugin became active for all participants
	}

	override fun onPluginDeactivated(plugin: RtkPlugin) {
		// A plugin was deactivated for all participants
	}

	override fun onPluginMessage(plugin: RtkPlugin, eventName: String, data: Any?) {
		// A plugin sent a message to the app
	}

	override fun onPluginFileRequest(plugin: RtkPlugin) {
		// A plugin requested a file from the app
	}
}

meeting.addPluginsEventListener(pluginsEventListener)

// Remove the listener when the owning screen is torn down
meeting.removePluginsEventListener(pluginsEventListener)

Conform to RtkPluginsEventListener and register the listener to receive plugin events.

Callback Description
onPluginActivated Called when a plugin is activated for all participants.
onPluginDeactivated Called when a plugin is deactivated for all participants.
onPluginMessage Called when a plugin's web page sends a message to the app. Refer to Exchange messages with a plugin.
onPluginFileRequest Called when a plugin requests a file from the app.
extension MeetingViewModel: RtkPluginsEventListener {
	func onPluginActivated(plugin: RtkPlugin) {
		// A plugin became active for all participants
	}

	func onPluginDeactivated(plugin: RtkPlugin) {
		// A plugin was deactivated for all participants
	}

	func onPluginMessage(plugin: RtkPlugin, eventName: String, data: Any?) {
		// A plugin sent a message to the app
	}

	func onPluginFileRequest(plugin: RtkPlugin) {
		// A plugin requested a file from the app
	}
}

meeting.addPluginsEventListener(pluginsEventListener: self)

// Remove the listener when the owning screen is torn down
meeting.removePluginsEventListener(pluginsEventListener: self)

Render plugins

<RTKCodeSnippet id={["web-react", "web-web-components", "web-angular"]}>

If you use the UI Kit, RealtimeKit provides ready-made components for plugins:

  • rtk-plugins-toggle: a control bar button that opens and closes the plugins sidebar.
  • rtk-plugins: a list of available plugins with controls to activate or deactivate each one.
  • rtk-plugin-main: renders the component of an active plugin in the meeting layout.

These components read from meeting.plugins, so they reflect plugin state automatically once you register your plugins at initialization.

When a plugin is active, getPluginView() returns the Android WebView that hosts it. Add this view to your layout from the onPluginActivated callback of your plugins event listener.

The first call to getPluginView() after activation creates the WebView and starts loading the plugin's url. Later calls return the same view while the plugin stays active. The method returns null while the plugin is inactive. When the plugin is deactivated, the SDK discards its WebView, so remove the view from your layout and call getPluginView() again after the plugin is reactivated.

// In your RtkPluginsEventListener
override fun onPluginActivated(plugin: RtkPlugin) {
	runOnUiThread {
		// Returns an Android WebView you can add to your layout
		val pluginView = plugin.getPluginView() ?: return@runOnUiThread
		pluginContainer.addView(pluginView)
	}
}

override fun onPluginDeactivated(plugin: RtkPlugin) {
	runOnUiThread {
		// The SDK discards the WebView on deactivation
		pluginContainer.removeAllViews()
	}
}

When a plugin is active, getPluginView() returns the WKWebView that hosts it. Add this view to your view hierarchy from the onPluginActivated callback of your plugins event listener.

The first call to getPluginView() after activation creates the WebView and starts loading the plugin's url. Later calls return the same view while the plugin stays active. The method returns nil while the plugin is inactive. When the plugin is deactivated, the SDK discards its WebView, so remove the view from your view hierarchy and call getPluginView() again after the plugin is reactivated.

// In your RtkPluginsEventListener
func onPluginActivated(plugin: RtkPlugin) {
	DispatchQueue.main.async {
		// Returns a WKWebView you can add to your view hierarchy
		guard let pluginView = plugin.getPluginView() else { return }
		self.pluginContainer.addSubview(pluginView)
	}
}

func onPluginDeactivated(plugin: RtkPlugin) {
	DispatchQueue.main.async {
		// The SDK discards the WebView on deactivation
		self.pluginContainer.subviews.forEach { $0.removeFromSuperview() }
	}
}

The React Native UI Kit provides ready-made components for plugins:

  • RtkPluginsToggle: a control bar button that opens and closes the plugins sidebar.
  • RtkPlugins: a panel listing available plugins with controls to activate or deactivate each one.
  • RtkPluginMain: renders the active plugin in a WebView.

These components read from meeting.plugins automatically once plugins are registered at initialization. If you use RtkMeeting, plugin UI is included without any additional setup.

Exchange messages with a plugin

<RTKCodeSnippet id={["web-react", "web-web-components", "web-angular"]}>

On web, a plugin's component is an HTMLElement that runs in your own page, so RealtimeKit does not provide a separate messaging API. Exchange data with the plugin using the mechanisms your UI framework provides, such as properties, attributes, callbacks, or DOM events.

If your plugin component embeds an iframe, send messages to it with iframe.contentWindow.postMessage(message, targetOrigin), and listen for replies with a message event listener on window. Always pass the plugin's origin as targetOrigin, and check event.origin before you trust an incoming message.

Messaging between the app and a plugin through sendData and onPluginMessage is available in the Android and iOS Core SDKs. Select Android or iOS in the SDK selector to view this section.

<RTKCodeSnippet id={["mobile-android", "mobile-ios"]}>

On Android and iOS, a plugin is a web page that the SDK loads into a WebView. Your app and the page exchange messages through a bridge that the SDK injects into the WebView. The bridge has two independent, one-way directions. It does not pair messages into requests and responses.

Direction App API Message type Plugin page API
App → plugin RtkPlugin.sendData(eventName, data) 24 window.addEventListener("message", handler)
Plugin → app RtkPluginsEventListener.onPluginMessage(plugin, eventName, data) 20 window.parent.postMessage(message, "*")

Each message carries a string event name and optional data. Event names are your own application convention; the SDK does not reserve any. Data must be JSON-compatible: null, strings, numbers, booleans, lists, and maps with string keys.

Messages travel only between your app and the plugin WebView on the same device. They are not sent to other participants. To share plugin state across participants, use the meeting key-value store or message broadcast.

The bridge does not authenticate the page that sends messages. Only register plugin URLs that you trust, and validate every message you receive on either side.

Receive messages from the plugin

The SDK calls onPluginMessage on every registered RtkPluginsEventListener when the plugin page sends a message. Implement it in the listener you registered in Listen to plugin events. The plugin must be active, and the listener must be registered before the page sends the message.

override fun onPluginMessage(plugin: RtkPlugin, eventName: String, data: Any?) {
	when (eventName) {
		"ready" -> {
			// The plugin page has loaded and is listening for messages
			plugin.sendData("init", mapOf("theme" to "dark"))
		}
		"score.updated" -> {
			val payload = data as? Map<*, *> ?: return
			val score = payload["score"] as? Int ?: return
			// Update your app with score
		}
	}
}

The SDK converts the JSON data sent by the page to Kotlin values:

JSON value Kotlin type
Object Map<String, Any?>
Array List<Any?>
String String
Boolean Boolean
Number Int, Long, or Double
null or omitted null
func onPluginMessage(plugin: RtkPlugin, eventName: String, data: Any?) {
	switch eventName {
	case "ready":
		// The plugin page has loaded and is listening for messages
		plugin.sendData(eventName: "init", data: ["theme": "dark"])
	case "score.updated":
		guard let payload = data as? [String: Any],
			let score = payload["score"] as? NSNumber else { return }
		// Update your app with score
	default:
		break
	}
}

The SDK converts the JSON data sent by the page to Foundation values. Cast objects with as? [String: Any], arrays with as? [Any], strings with as? String, and numbers or booleans with as? NSNumber. If the page sends null or omits data, data is nil.

<RTKCodeSnippet id={["mobile-android", "mobile-ios"]}>

Send messages to the plugin

Call sendData on an active RtkPlugin to send an event to its web page.

val error = plugin.sendData(
	eventName = "init",
	data = mapOf("theme" to "dark", "locale" to "en-US"),
)

if (error != null) {
	// PluginError.NotActive: the plugin is not active
}
if let error = plugin.sendData(eventName: "init", data: ["theme": "dark", "locale": "en-US"]) {
	// PluginError.NotActive: the plugin is not active
	print(error)
}

<RTKCodeSnippet id={["mobile-android", "mobile-ios"]}>

sendData returns PluginError.NotActive if the plugin is not active. Otherwise it returns no error and delivers the message asynchronously. A missing error means the SDK accepted the message, not that the page received it. The SDK drops the message without reporting an error when:

  • You have not called getPluginView() since the plugin was activated, so no WebView exists.
  • The page has not loaded yet, or it has not registered its message listener.
  • data contains a value that is not JSON-compatible.

The SDK does not queue messages or acknowledge delivery. To avoid sending before the page is ready, have the page send a ready event after it loads, and call sendData only after onPluginMessage receives that event. The examples on this page follow this pattern.

Receive messages in the plugin

In the plugin page, listen for message events on window. The SDK delivers each message as an object with this shape:

{
	"type": 24,
	"uuid": "",
	"payload": {
		"eventName": "init",
		"data": { "theme": "dark", "locale": "en-US" }
	}
}

Register the listener in an inline script in the page <head> so that it is installed before your app sends anything:

<script>
	window.addEventListener("message", (event) => {
		const message = event.data;
		// Ignore anything that is not a message from the app
		if (!message || message.type !== 24 || !message.payload) return;

		const { eventName, data } = message.payload;
		if (eventName === "init") {
			// Validate data before using it
			document.body.dataset.theme = data?.theme ?? "light";
		}
	});
</script>

event.data is already an object. Do not call JSON.parse on it.

Send messages from the plugin

To send a message to the app, call window.parent.postMessage with an object that has type: 20 and a payload object containing a string eventName and optional data.

The SDK replaces window.parent.postMessage in the WebView each time a page starts loading, but it does not signal when the replacement is in place. Send your first message after the page's load event instead of from a script that runs while the page is parsed.

function sendToApp(eventName, data) {
	window.parent.postMessage({ type: 20, payload: { eventName, data } }, "*");
}

// Tell the app that the page is ready to receive messages
window.addEventListener("load", () => sendToApp("ready"));

// Send application events after the page has loaded
document.querySelector("#submit-score").addEventListener("click", () => {
	sendToApp("score.updated", { score: 42 });
});

The SDK ignores the second argument to postMessage. It does not restrict which origins can send messages. The app does not receive messages that use a different type, omit payload, or are sent while the plugin is inactive. If you need to correlate a reply with a request, include your own identifier in data.

:::caution Pass the message as an object. Do not call JSON.stringify on it first: the bridge serializes the object itself, and a pre-serialized string or an eventName that is not a string can cause an uncaught exception in the native SDK and crash your app. :::