Azure SignalR Serverless with Azure Functions v4 Node

For my Microsoft Teams apps Zeitplan.io and Team Schedule, I recently implemented auto-updates of the calendars using SignalR to trigger the reload whenever a change was made. Since both apps run with Azure Functions on Node/TypeScript as the backend, I chose the Azure SignalR service in serverless mode. The functions are connected via Managed Identity to negotiate between the React frontend and the Azure SignalR service.

To deploy the infrastructure I used the same approach via Bicep as outlined in this blog post:

To connect a client with the SignalR service, we create a negotiation function that will return the connection string to the frontend. This can be achieved with an input binding. We do not need to install any additional packages, this is already included in the @azure/functions package. Just ensure that the connection string is available in the environment variable specified in the binding (in this case AzureSignalRConnectionString). For connections via Managed Identity, it will look something like this:

Endpoint=https://mysignalrservice.service.signalr.net;AuthType=azure.msi;Version=1.0;

Since this is a multi-tenant app, I also needed to ensure that clients only get notified of updates to their calendar instance. This can be achieved by registering users in groups, in this case as an output binding during negotiation. For example, you can use the tenantId as the group name.

import { app, input, HttpRequest, HttpResponseInit, InvocationContext, output } from "@azure/functions";

// TODO: auth

const userId = "theUsersId"; // set this after authentication

// SignalR input binding for negotiation
const inputSignalR = input.generic({
  type: 'signalRConnectionInfo',
  name: 'connectionInfo',
  hubName: 'default',
  userId: userId,
  connectionStringSetting: 'AzureSignalRConnectionString',
});

// SignalR output binding for group management
const signalROutput = output.generic({
  type: 'signalR',
  name: 'signalR',
  hubName: 'default',
  connectionStringSetting: 'AzureSignalRConnectionString',
});

export async function negotiate(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> { 

  // Authenticate the request
  const { userInfo, authResponse } = await authenticateRequest(request, context);
  if (authResponse) return authResponse;

  // Determine which group the user should join based on their tenant/context
  const groupName = "tenantId"; // TODO: change this to your needs

  // Create group action to add user to the appropriate group
  const groupAction = {
    userId: userId,
    groupName: groupName,
    action: 'add'
  };

  // Set the SignalR output to add user to tenant group
  context.extraOutputs.set(signalROutput, groupAction);


return {
  body: JSON.stringify(context.extraInputs.get(inputSignalR))
};


app.post('negotiate', {
  authLevel: 'anonymous',
  handler: negotiate,
  route: 'negotiate',
  extraInputs: [inputSignalR],
  extraOutputs: [signalROutput],
});

On the frontend, we can use the @microsoft/signalr npm package for establishing the connection.

Note: Unlike suggested by other blog posts, you do not need to manually fetch the negotiate endpoint before establishing the SignalR connection. The SDK will do all that for you; just point it to your API without the /negotiate part and ensure you don’t skip the negotiation in the connection settings:

import { HubConnectionBuilder, LogLevel, MessageHeaders } from '@microsoft/signalr';


const headers: MessageHeaders = {
  // pass any information that you need to set up a SignalR connection, 
  // e.g. the userId, group name etc.
  userId: userId
};const logLevel = LogLevel.Information;
const jwtToken = ""; // pass the JWT token for authentication

// Create SignalR connection
const newConnection = new HubConnectionBuilder()
  .withUrl(url, {
    accessTokenFactory: () => jwtToken,
    withCredentials: true,
    skipNegotiation: false,
    headers: headers
  })
  .configureLogging(logLevel)
  .build();

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.