Bleepr

the mission.

Our vision is simple, to connect every healthcare professional on the planet.

the team.

Matthew Stubbs

Doctor & Developer

James Griffin

Developer

Alex Staton

Business Lead

Matthew Stubbs

Doctor & Developer

Dr Matthew Stubbs is a medical doctor and developer. He was lead for front-end development and medical strategy including partnerships. Matt wanted to create a clean user interface, attempting to recreate the feeling of a medical clinic.

the architecture.

CosmosDB

Azure Cosmos DB is a fully managed NoSQL (schemaless) database for modern app development. Single-digit millisecond response times, and automatic and instant scalability, guarantee the speed at any scale. This database is hosted on Azure, and is blazingly fast. As a social network, the user consumes a lot of content, far more than they produce (in general). We needed a database that could handle multiple queries, and deliver responses exceptionally fast - CosmosDB fitted the bill. Its integration with SignalR through the change feed made the database particularly useful, creating an easy method to provide live updates to the user.

the story.

Bleepr was started as a method to improve connectivity across healthcare. Never was the issue of interconnectivity between healthcare professionals more apparent than in the coronavirus pandemic. While many tools focus on siloed interoperability, for example, within a trust and hospital, Bleepr was designed to break down these siloes, creating a truly connected global healthcare system.

The Problem

There is no place for the millions of healthcare professionals to connect, conduct research and engage with meaningful content.

Healthcare has simply not kept up with other industries and existing platforms are not fit for purpose.

  1. Existing sites don't work in healthcare: 64% of healthcare professionals do not use any professional social networking platforms.
  2. Finding expertise takes too long: 6 out of 10 professionals find Identifying relevant peers 'often' or 'always' challenging.
  3. Research is complex: 80% of professionals find identifying and publishing research as challenging.
  4. Content is difficult to find: 9 out of 10 said that finding high-quality content is 'Often or 'Always' challenging.

The Solution

Bleepr sought to address these issues by creating a secure, authenticated, ever-green space for healthcare individuals to create, consume and collaborate on healthcare information.

How Bleepr would solve these problems

The coronavirus pandemic revealed an issue with healthcare collaboration and information sharing. Due to the unprecedented nature of the global coronavirus pandemic in the modern medical age, the need for rapid dissemination of information was paramount to improving outcomes for patients. However, the way information is currently shared in formal medical settings is exceptionally slow, due to a lack of platforms leveraging the power of the network effect.

Due to this lack of such a medical platform, medical professionals were resorting to ill-equipped tools to share information rapidly. Below is a real example of a message shared by a leading consultant to share methods they found effective in treating the coronavirus. This message was widely shared amongst medical WhatsApp groups. The problem with this is clear, once the message has been shared there is no auditable trail as to where that information will go. The information beyond the initial share is also completely unverified, we were often receiving information such as this, but we had no way of verifying the author, or it's accuracy.

Bleepr Coronavirus Use Case - Sharing medical information

The Bleepr app would allow individuals to share information in an ever-green manner, through verified accounts, ensuring information could be spread quickly and safely from leading healthcare professionals in their field.

The Design

Bleepr was designed to be cross-platform from the outset. This was due to various factors, but most notably in our early research we found NHS staff use a wide variety of devices, mostly android and iOS. Therefore, to not cater to both, would be against the mission statement of Bleepr, to connect every healthcare professional on the planet. Apps built on hybrid platforms such as Ionic have often been overlooked, but due to improvements in both Ionics platform and operating potential of mobile devices, these apps are now able to perform almost as well as their native counterparts. Using Ionic also gave us the ability to leverage the developing technology of progressive web apps, maximising the number of ways our platform could be downloaded.

Bleepr Architecture.png

Authentication was secured using the OAuth2.0 protocol, which was used to authenticate access to the application, SignalR interactions, as well as database access. This ensures all aspects of the application are protected by advanced security protocols. SignalR acts as a secured channel (a WebSocket) between the application and live updates, including messaging and notifications.

SignalR leverages the change feed for CosmosDB. Effectively, whenever a document is updated in the database, be this a message or a post, then the relevant notification is delivered to the user if applicable.

A further layer of cognitive skills was used over this content, including natural language processing, optical character recognition, all collated in Azure Search. This allowed for advanced search capabilities on the platform, ensuring users could access the content most relevant and useful to them.

The app was designed to have a very clean, clinical feel. The first image below shows the news feed. The feed was built of a user's connections posts, as well as industry leader posts within their chosen and calculated interests. This allowed for an evergreen content delivery system, leveraging the power of the network effect to disseminate medical information.

Bleepr_app_design.png

Image two and three show the article page. This was a medium style article presentation, initially with basic media types, including images and videos. However, the hope was to include more advanced, medical media types. For example, we were intending on producing media presentation types for MRI and CT scans, which are usually stored as DICOM images. This would allow the user to scroll through and view slices of a CT / MRI scan, in the same way a doctor would in a hospital.

The articles were created directly on the platform and could be written on both desktop and native devices. The framework quill.js was used, which is a powerful while also highly customisable rich text editor.

The final image shows the profile page for a user. The profile page allowed you to store medical rotations, education, projects, and research in an indexable and searchable way.

Bleepr Messaging

While I can't go through all components of the code base of this project, due to its size, I think the Bleepr instant messaging component is a perfect example of the use of SignalR and the CosmosDB change feed, which is a workflow we used a lot within this project.

Bleepr provided a full instant messaging service as part of its functionality. When a message was sent, a document was created in the CosmosDB database. The problem with this architecture it it's not 'instant', in that the user would have to refresh their page to get new messages. We solved this using the function below.

The following function subscribed to the change feed for the messaging collection, meaning that any time a new message document was created, the change feed would activate this function. Depending on who the message was intended to be delivered to, if that person was online, the message would then be instantly conveyed to them using a websocket via SignalR.

Using this methodoly we could also update other database documents, such as that for the conversation in question, updating the latest message in that document so to optimise for database reads when displaying a list of the users conversations.

This workflow was used in multiple places across the application, and was used to create a larger live notifications system across the application.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bleepr.Messaging.Models;
using Microsoft.Azure.Documents;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Bleepr.Notifications;
using Microsoft.Azure.Documents.Client;

namespace Bleepr.Messaging
{
    public static class SendMessages
    {
        public static string databaseName = Environment.GetEnvironmentVariable("CosmosDatabase");
        public static string conversationsCollectionName = Environment.GetEnvironmentVariable("CosmosConversationsCollection");
        public static string conversationSprocName = Environment.GetEnvironmentVariable("CosmosConversationsSproc");

        [FunctionName("sendMessages")]
        public static async Task Run(
            [CosmosDBTrigger(
                databaseName: "bleepr",
                collectionName: "messages",
                ConnectionStringSetting = "CosmosConnection",
                LeaseCollectionName = "leases",
                LeaseCollectionPrefix = "%LeasePrefix%")]IReadOnlyList<Document> newMessages,
            [SignalR(
                HubName = "%HubName%",
                ConnectionStringSetting = "SignalRConnection")] IAsyncCollector<SignalRMessage> dispatchedMessages,
            [CosmosDB(
                databaseName: "bleepr",
                collectionName: "conversations",
                ConnectionStringSetting = "CosmosConnection")] DocumentClient client,
            ILogger log)
        {
            if (newMessages != null && newMessages.Count > 0)
            {
                log.LogInformation("Documents modified " + newMessages.Count);
                log.LogInformation("First document Id " + newMessages[0].Id);
                SignalRMessage signalRMessage = new SignalRMessage();
                // Method name that the client will query for to trigger the newMessage action
                signalRMessage.Target = "newMessage";

                Dictionary<string, List<Message>> conversationData = new Dictionary<string, List<Message>>();

                foreach (var message in newMessages)
                {
                    try
                    {
                        var messageString = message.ToString();
                        var messageData = JsonConvert.DeserializeObject<Message>(messageString);

                        // if conversation not in conversation data then add convo id and instatiate list.
                        if (!conversationData.ContainsKey(messageData.ConversationId))
                        {
                            conversationData[messageData.ConversationId] = new List<Message>();
                        }

                        // add message to conversation.
                        conversationData[messageData.ConversationId].Add(messageData);

                        // If it's a 1 to 1 message, specify a userId for SignalR authentication handling
                        if (messageData.Recipients.Length == 1)
                        {
                            signalRMessage.UserId = messageData.Recipients[0];
                            log.LogInformation("Direct message to " + messageData.Recipients[0]);
                        }
                        // Otherwise it is a group message so set GroupName as the conversationId (our way of grouping chats)
                        else
                        {
                            signalRMessage.GroupName = messageData.ConversationId;
                            log.LogInformation("Group message to " + messageData.ConversationId);
                        }
                        signalRMessage.Arguments = new[] { messageData };
                        log.LogInformation($"Dispatching {signalRMessage} to SignalR");
                        await dispatchedMessages.AddAsync(signalRMessage);

                        // if read by count > 0 then this is an update action and code should not send notification or update convo doc
                        // as this would result in far too many calls to the sproc (i.e. on every single read of a message) which would 
                        // include messages that were not most recent so would be unnecessary RUs.
                        if (messageData.ReadBy == null && messageData.Type == "message")
                        {
                            NotificationsProcessor.SendMessageNotification(messageData);
                        }
                    }
                    catch (Exception ex)
                    {
                        log.LogCritical(ex, $"Failed to parse message with id {message.Id}");
                    }
                }

                // update the conversation snippet with the most recent message for each conversation:
                log.LogInformation("Conversation Count: " + conversationData.Count);
                if (conversationData.Count > 0)
                {
                    // iterate over each conversation:
                    foreach (KeyValuePair<string, List<Message>> messageList in conversationData)
                    {
                        // sort the conversation messages
                        messageList.Value.Sort((x, y) => y.TimeSent.CompareTo(x.TimeSent));

                        // convert message to string to send with sproc
                        string messageString = JsonConvert.SerializeObject(messageList.Value[0]);
                        
                        // call sproc
                        await callStoredProc(client, messageList.Key, messageString);
                    }
                }
            }
            else
            {
                log.LogError("Messaging changefeed triggered with no messages in trigger input");
            }
        }

        // update the conversation snippet
        public static async Task callStoredProc(DocumentClient client, string conversationId, string messageString)
        {
            // create sproc URI
            Uri sprocUri = UriFactory.CreateStoredProcedureUri(databaseName, conversationsCollectionName, conversationSprocName);

            // add sproc inputs
            dynamic[] procInputs = new dynamic[] { conversationId, messageString };

            // execute stored procedure
            RequestOptions options = new RequestOptions { PartitionKey = new PartitionKey(conversationId) };
            await client.ExecuteStoredProcedureAsync<Document>(sprocUri, options, procInputs);
        }
    }
}