This is a demonstration & sample application designed to be a simple multi-user web based chat system.
It provides persistent group chats, user to user private chats, a user list, idle (away from keyboard) detection and several other features.
It is built on several Azure technologies, including: Web PubSub, Functions, Static Web Apps and Table Storage
👀 Note. This was created as a personal project, created to aid learning while building something interesting. The code comes with all the caveats you might expect from such a project.
Goals:
Use cases & key features:
Try the application out here: chatr.benco.io
This is the main web frontend as used by end users via the browser.
The source for this is found in client/ and consists of a static standalone pure ES6 JS application, no bundling or Node.js is required. It is written using Vue.js as a supporting framework, and Bulma as a CSS framework.
Some notes:
client/js/app.js
shows how to create a Vue.js app with child components using this approach. The majority of client logic is here.client/js/components/chat.js
is a Vue.js component used to host each chat tab in the application.auth/
endpoint provided by Static Web Apps is used to sign users in and fetch their user details, such as userId.This is the backend, handling websocket events to and from Azure Web PubSub, and providing REST API for some operations.
The source for this is found in api/ and consists of a Node.js Azure Function App using the v4 programming model. It connects to Azure Table Storage to persist group chat and user data (Table Storage was picked as it’s simple & cheap). This is not hosted in a standalone Azure Function App but instead deployed into the Static Web App as part of it’s serverless API support
There are four HTTP functions all served from the default /api/
path
eventHandler
- Webhook receiver for “upstream” events sent from Azure Web PubSub service, contains the majority of application logic. Not called directly by the client, only Azure WebPub Sub.getToken
- Called by the client to get an access token and URL to connect via WebSockets to the Azure Web PubSub service. Must be called with userId in the URL query, e.g. GET /api/getToken?userId={user}
users
- Returns a list of signed in users from statechats
- Returns a list of active group chats from stateState is handled with state.js
which is an ES6 module exporting functions supporting state CRUD for users and chats. This module carries out all the interaction with Azure Tables, and provides a relatively transparent interface, so a different storage backend could be swapped in.
There is two way message flow between clients and the server via Azure Web PubSub and event handlers
The json.webpubsub.azure.v1 subprotocol is used rather than basic WebSockets, this provides a number of features: users can be added to groups, clients can send custom events (using type: event
), and also send messages direct to other clients without going via the server (using type: sendToGroup
)
Notes:
Events & chat are sent using the json.webpubsub.azure.v1 subprotocol
Chat messages sent from the client use sendToGroup
and a custom JSON payload with three fields message
, fromUserId
& fromUserName
, these messages are relayed client to client by Azure Web PubSub, the server is never notified of them:
{
type: 'sendToGroup',
group: <chatId>,
dataType: 'json',
data: {
message: <message text>,
fromUserId: <userId>,
fromUserName: <userName>,
},
}
Events destined for the backend server are sent as WebSocket messages from the client via the same subprotocol with the event
type, and an application specific sub-type, e.g.
{
type: 'event',
event: 'joinChat',
dataType: 'text',
data: <chatId>,
}
The types of events are:
The backend API eventHandler
function has cases for each of these user events, along with handlers for connection & disconnection system events.
Messages sent from the server have a custom Chatr app specific payload as follows:
{
chatEvent: <eventType>,
data: <JSON object type dependant>
}
Where eventType
is one of:
The client code in client/js/app.js
handles these messages as they are received by the client, and reacts accordingly.
The plan of this project was to use Azure Web PubSub and Azure Static Web Apps, and to host the server side component as a set of serverless HTTP functions in Azure Functions. Azure Static Web Apps was selected because it has amazing support for codeless and config-less user sign-in and auth, which I wanted to leverage.
Some comments on this approach:
webPubSubConnection
binding. This is partly historical now (see above bullet), but it sill remains a valid approach. For sending messages back to Web PubSub, the server SDK can simply be used within the function code rather than using the webPubSub
output binding.State in Azure Tables consists of two tables (collections) named chats
and users
As each chat contains nested objects inside the members field, each chat is stored as a JSON string in a field called data
. The PartitionKey is not used and hardcoded to a string “chatr”. The RowKey and the id field inside the data object are the same.
Example of a chat data entity
{
"id": "eab4b030-1a3d-499a-bd89-191578395910",
"name": "This is a group chat",
"members": {
"0987654321": {
"userId": "0987654321",
"userName": "Another Guy"
},
"1234567890": {
"userId": "1234567890",
"userName": "Ben"
}
},
"owner": "1234567890"
}
Users are stored as entities with the fields (columns) described below. As there are no nested fields, there is no need to encode as a JSON string. Again the PartitionKey is not used and hardcoded to a string “chatr”.
userId
field returned from Static Web Apps auth endpointtwitter
, aad
or github
See makefile
$ make
help 💬 This help message
lint 🔎 Lint & format, will not fix but sets exit code on error
lint-fix 📜 Lint & format, will try to fix errors and modify code
run 🏃 Run server locally using SWA CLI
clean 🧹 Clean up project
deploy-infra 🚀 Deploy required infra in Azure using Bicep
deploy-api 🚀 Deploy API to Azure using Function Core Tools
deploy-client 🚀 Deploy client to Azure using SWA CLI
deploy 🚀 Deploy everything!
tunnel 🚇 Start AWPS local tunnel tool for local development
Deployment is slightly complex due to the number of components and the configuration between them. The makefile target deploy
should deploy everything for you in a single step using Bicep templates in the deploy/ folder
See readme in deploy folder for details and instructions
This requires a little effort as the Azure Web PubSub service needs to be able call the HTTP endpoint on your local machine, plus several role assignments & configs need to be setup, see below. The fabulous Azure Web PubSub local tunnel tool does a great job of providing a way to to tunnel the Azure Web PubSub service to your local machine, so you can run the app locally and test it.
When running locally the Static Web Apps CLI is used and this provides a nice fake user authentication endpoint for us and will also run the API.
If these pre-reqs look a little daunting, don’t worry, just use the dev container in the repo, this has everything you need to run and deploy the app.
A short summary of the steps to getting it running:
api/local.settings.sample.json
to api/local.settings.json
and edit the required settings values.makefile
AZURE_PREFIX
should be the name of the Azure Web Pub Sub resourceAZURE_RESGRP
should be the resource group you deployed intomakefile
you can pass these values in after the make command or set them as env vars.make run
make tunnel
http://localhost:4280/