<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marc Duiker</title>
    <description>The latest articles on DEV Community by Marc Duiker (@marcduiker).</description>
    <link>https://dev.to/marcduiker</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F220922%2Fb486eebe-5f4a-4d1d-8239-693aababb16a.png</url>
      <title>DEV Community: Marc Duiker</title>
      <link>https://dev.to/marcduiker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcduiker"/>
    <language>en</language>
    <item>
      <title>Join the Diagrid Catalyst AWS Hackathon!</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Mon, 29 Apr 2024 08:13:50 +0000</pubDate>
      <link>https://dev.to/diagrid/join-the-diagrid-catalyst-aws-hackathon-4849</link>
      <guid>https://dev.to/diagrid/join-the-diagrid-catalyst-aws-hackathon-4849</guid>
      <description>&lt;p&gt;We are excited to invite you to the Diagrid Catalyst AWS Hackathon—a four-week, virtual event where you can unleash your creativity and showcase your coding skills by building or modernizing a cutting-edge application using the powerful Diagrid Catalyst platform. Get ready to collaborate, compete, and win cash prizes! &lt;a href="http://pages.diagrid.io/catalyst-hackathon"&gt;Register now&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Diagrid Catalyst?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.diagrid.io/catalyst"&gt;Diagrid Catalyst&lt;/a&gt; is a Developer API platform providing a brand-new approach to distributed application development. Using the Catalyst APIs, powered by the &lt;a href="https://dapr.io/"&gt;Dapr&lt;/a&gt; open source project, developers can overcome the complexity of rewriting common software patterns and achieve higher productivity by offloading infrastructure concerns from their code to Catalyst.&lt;/p&gt;

&lt;p&gt;Say hello to making API calls from any platform, in any language to build modern, cloud-applications with ease. This hackathon is your chance to explore the potential of Diagrid Catalyst while bringing your solution ideas to life! For more on Catalyst, watch our most recent &lt;a href="https://youtu.be/mLpIfX3hcwg?list=PLdl4NkEiMsJscq00RLRrN4ip_VpzvuwUC"&gt;webinar&lt;/a&gt; for a platform overview and API deep dive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Participate?
&lt;/h2&gt;

&lt;p&gt;Join the Diagrid Catalyst Hackathon to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Innovate with an Emerging Product:&lt;/strong&gt; Explore the ways Diagrid Catalyst can expedite your development process by building an application to solve real-world challenges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaborate with Peers:&lt;/strong&gt; Connect with fellow developers, designers, and entrepreneurs. Collaborate, share ideas, and learn from each other's experiences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhance Your Skills:&lt;/strong&gt; Sharpen your application development and architecture skills using the coding languages of your choice and deploying your solution on Amazon Web Services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Win Prizes:&lt;/strong&gt; Compete for impressive cash prizes! We have rewards lined up for the most exceptional projects and for providing product feedback.
‍
All participation is evaluated based on the criteria in the Hackathon &lt;a href="https://22146261.fs1.hubspotusercontent-na1.net/hubfs/22146261/Catalyst%20Hackathon/Diagrid%20Catalyst%20AWS%20Hackathon%20Rules.docx.pdf"&gt;Rules, Terms &amp;amp; Conditions&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/zUbCl5_OHOU"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Build?
&lt;/h2&gt;

&lt;p&gt;Build or update an open-source microservice-based software solution utilizing at least two Diagrid Catalyst APIs. The solution should be composed of multiple application services and either be newly written or significantly enhanced to utilize Diagrid Catalyst. The solution should be architected using AWS infrastructure and hosting services. The solution must align with one of the areas below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polyglot compute/ Cross-platform service discovery:&lt;/strong&gt; Application services are deployed to different AWS hosting platforms, which interact via the Catalyst APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateful business process orchestration:&lt;/strong&gt; Application services are instrumented as part of a Dapr Workflow using the Dapr Workflow authoring SDK paired with the Catalyst Workflow API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless architecture:&lt;/strong&gt; Application services are optimized to run on AWS PaaS/SaaS services, which abstracts infrastructure concerns and enables high-scalability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hackathon Timeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registration Open:&lt;/strong&gt; April 19th 2024&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project Submissions Open:&lt;/strong&gt; May 10th 2024&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback Submissions Open:&lt;/strong&gt; May 24th 2024&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registration Closes:&lt;/strong&gt; May 17th 2024&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project Submissions Deadline:&lt;/strong&gt; June 7th 2024&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback Submissions Deadline:&lt;/strong&gt; June 14th 2024&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; Virtual Only&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Catalyst Reference Apps
&lt;/h2&gt;

&lt;p&gt;Looking for inspiration? See below for a couple of sample projects we’ve built using Diagrid Catalyst. More samples, including an AWS-specific reference, will be available to participants on the Hackathon site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tetris Game
&lt;/h3&gt;

&lt;p&gt;The classic “blocks falling from the sky” game! The &lt;a href="https://github.com/diagrid-labs/catalyst-tetris"&gt;solution&lt;/a&gt; consists of two services:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Python Flask &lt;em&gt;user&lt;/em&gt; service which serves the JavaScript/HTML front-end for the user sign up and leaderboard and handles user state management and leaderboard updates.&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;game&lt;/em&gt; service written in Go, which serves the JavaScript/HTML frontend for the game itself and manages the game session lifecycle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These two services need to manage state and communicate to ensure games are started with the appropriate users, and user profiles are updated with the game results. The service interaction and data storage is all implemented using the APIs provided by Catalyst. Read the &lt;a href="https://www.diagrid.io/blog/build-multiplayer-tetris-in-the-browser-using-python-and-go"&gt;blog post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/VS036hE6cvg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Pizza Store Management System
&lt;/h3&gt;

&lt;p&gt;A distributed pizza store with Catalyst APIs. The solution comprises two .NET microservices, two serverless JavaScript functions and a Vue front-end. The applications depend on Diagrid Catalyst as the serverless API layer to connect to the underlying data and messaging infrastructure. Read the blog.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/SVEXCUUlqsk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Participate?
&lt;/h2&gt;

&lt;p&gt;Participation is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Register:&lt;/strong&gt; Submit a registration request form for the hackathon. Eligible participants will receive a direct invite to the Hackathon site, hosted on Devpost. Accept your invitation to Devpost and click the blue “Register” button on the Hackathon landing page to register.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a project:&lt;/strong&gt; Create a project to frame out your hackathon solution ideas for others to see and potentially join in to help execute. You have a few options when it comes to projects:

&lt;ol&gt;
&lt;li&gt;Create a closed project to participate as an individual.&lt;/li&gt;
&lt;li&gt;Create an open project to enable other Hackathon participants to join you in your mission. You can even choose to invite people to participate in your project.&lt;/li&gt;
&lt;li&gt;Join an existing, open project if one is available and intrigues you.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn:&lt;/strong&gt; Join our ongoing &lt;a href="https://pages.diagrid.io/diagrid-webinar-registration-catalyst"&gt;Catalyst webinar series&lt;/a&gt; or &lt;a href="https://www.youtube.com/playlist?list=PLdl4NkEiMsJscq00RLRrN4ip_VpzvuwUC"&gt;watch on-demand&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build:&lt;/strong&gt; Develop your application using Diagrid Catalyst. Let your creativity shine, and don't forget to integrate innovative features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submit:&lt;/strong&gt; Submit your project with the required deliverables by the deadline, June 7&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For more information on resource access, project requirements and judging criteria, view the Hackathon &lt;a href="https://22146261.fs1.hubspotusercontent-na1.net/hubfs/22146261/Catalyst%20Hackathon/Diagrid%20Catalyst%20AWS%20Hackathon%20Rules.docx.pdf"&gt;Rules, Terms &amp;amp; Conditions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prizes
&lt;/h2&gt;

&lt;p&gt;We have exciting cash prizes up for grabs! Prizes range from a Grand Prize of $2000 to Runner-up Prizes of $1000 each and Bonus Prizes from $250-$500! Get swag for participating &amp;amp; provide valuable product feedback for the chance to win another $100.&lt;/p&gt;

&lt;p&gt;If you participate in the hackathon, you are also eligible for a $100 referral bonus for spreading the word! Share the Hackathon details with your friends and encourage them to sign up. The participant with the most referrals wins!&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Ready to Hack
&lt;/h2&gt;

&lt;p&gt;Don’t miss this unique opportunity to explore Diagrid Catalyst, build innovative apps, and compete for cash prizes. Whether you're a seasoned developer or a curious beginner, the Diagrid Catalyst Hackathon is your time to shine.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://pages.diagrid.io/catalyst-hackathon"&gt;Sign up&lt;/a&gt; to participate today.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>programming</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>Building a distributed pizza store in .NET with serverless Dapr APIs</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Thu, 01 Feb 2024 10:57:11 +0000</pubDate>
      <link>https://dev.to/diagrid/building-a-distributed-pizza-store-in-net-with-serverless-dapr-apis-j1j</link>
      <guid>https://dev.to/diagrid/building-a-distributed-pizza-store-in-net-with-serverless-dapr-apis-j1j</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;When developers build distributed systems, they need to think about cross-cutting concerns such as security, observability, and resiliency. Developers also write a lot of glue code to orchestrate services, create long-running and durable workflows, and connect to external resources, such as message brokers and state stores. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dapr.io/" rel="noopener noreferrer"&gt;Dapr&lt;/a&gt;, the Distributed Application Runtime, is ideal for building distributed systems since it has built-in support for cross-cutting concerns, and it offers a suite of APIs for communication, state and workflow, that make developers more efficient.  According to the &lt;a href="https://www.diagrid.io/blog/the-state-of-dapr-2023-report" rel="noopener noreferrer"&gt;State of Dapr Report&lt;/a&gt;, 95% of Dapr developers claim that Dapr saves time, and 55% of the developers surveyed claimed it saved them more than 30% of development time.&lt;/p&gt;

&lt;p&gt;The usage of Dapr is also increasing thanks to a growing number of platforms that support the Dapr APIs. Dapr is no longer limited to running as a sidecar in Kubernetes, as is described in &lt;a href="https://www.diagrid.io/blog/dapr-deployment-models" rel="noopener noreferrer"&gt;Dapr Deployment Models&lt;/a&gt;. In this article, &lt;a href="https://www.diagrid.io/catalyst" rel="noopener noreferrer"&gt;Diagrid Catalyst&lt;/a&gt;, a suite of serverless Dapr APIs, is used to orchestrate services with &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/" rel="noopener noreferrer"&gt;Dapr workflow&lt;/a&gt;, and interact with message brokers and key-value stores.&lt;/p&gt;

&lt;p&gt;Throughout this article, you’ll be using the following to configure a distributed system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two Dapr services (.NET)&lt;/li&gt;
&lt;li&gt;Two serverless functions (JavaScript)&lt;/li&gt;
&lt;li&gt;A front-end (Vue)&lt;/li&gt;
&lt;li&gt;A key value state store&lt;/li&gt;
&lt;li&gt;A pub/sub message broker&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.diagrid.io/catalyst" rel="noopener noreferrer"&gt;Diagrid Catalyst&lt;/a&gt; as the serverless API layer to connect everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Check the code on &lt;a href="https://github.com/diagrid-labs/catalyst-pizza-demo" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and join the &lt;a href="https://pages.diagrid.io/catalyst-early-access-waitlist" rel="noopener noreferrer"&gt;Catalyst early access program&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagrid Pizza Store
&lt;/h2&gt;

&lt;p&gt;The demo that is used in this article revolves around an online pizza store, where the user selects pizzas, orders them, and follows the progress of the order in realtime. It’s a simplified example where the durable workflow focuses on the inventory of ingredients, and communicating with the kitchen. The source code is &lt;a href="https://github.com/diagrid-labs/catalyst-pizza-demo" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm7abjviwgbse7hnhc36g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm7abjviwgbse7hnhc36g.gif" alt="Pizza store front-end" width="1200" height="1019"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The solution includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two &lt;a href="http://dapr.io/" rel="noopener noreferrer"&gt;Dapr&lt;/a&gt; web services written in .NET, &lt;em&gt;PizzaOrderService&lt;/em&gt; and &lt;em&gt;KitchenService&lt;/em&gt;. In this demo, they’re running locally, but these can be hosted in any cloud that supports ASP.NET services.&lt;/li&gt;
&lt;li&gt;The website (based on Vue) and two serverless functions written in JavaScript, &lt;em&gt;getAblyToken&lt;/em&gt; and &lt;em&gt;placeOrder&lt;/em&gt;. These are hosted on &lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ably.com/" rel="noopener noreferrer"&gt;Ably&lt;/a&gt; as the realtime communication component between the &lt;em&gt;PizzaOrderService&lt;/em&gt; and the website.&lt;/li&gt;
&lt;li&gt;A key/value store to manage inventory (managed by Diagrid).&lt;/li&gt;
&lt;li&gt;A pub/sub message broker to communicate between the &lt;em&gt;PizzaOrderService&lt;/em&gt; and the &lt;em&gt;KitchenService&lt;/em&gt; (managed by Diagrid).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.diagrid.io/catalyst" rel="noopener noreferrer"&gt;Diagrid Catalyst&lt;/a&gt; that offers serverless APIs for communication, state, and workflow powered by Dapr.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1ornquwh30esbeu2k8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1ornquwh30esbeu2k8d.png" alt="Pizza store architecture" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow
&lt;/h3&gt;

&lt;p&gt;The &lt;em&gt;PizzaOrderService&lt;/em&gt; contains a Dapr workflow that orchestrates the activities for checking the inventory and communicating with the &lt;em&gt;KitchenService.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1gdgcp6n4hiq9plzxrm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1gdgcp6n4hiq9plzxrm.png" alt="Pizza store workflow" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The workflow also contains activities for sending realtime messages to the website, these have been omitted from the workflow diagram to keep it concise.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Pizza Store locally
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;The following services, tools, and frameworks are required for this demo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.diagrid.io/catalyst" rel="noopener noreferrer"&gt;Diagrid Catalyst account&lt;/a&gt; (&lt;a href="https://pages.diagrid.io/catalyst-early-access-waitlist" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; for early access) and the &lt;a href="https://docs.diagrid.io/catalyst/references/cli-reference/intro" rel="noopener noreferrer"&gt;Diagrid CLI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vercel.com/signup" rel="noopener noreferrer"&gt;Vercel account (hobby)&lt;/a&gt; and the &lt;a href="https://vercel.com/docs/cli" rel="noopener noreferrer"&gt;Vercel CLI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ably.com/signup" rel="noopener noreferrer"&gt;Ably account (free)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dotnet.microsoft.com/download/dotnet/8.0" rel="noopener noreferrer"&gt;.NET 8 SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en/download/" rel="noopener noreferrer"&gt;Node 18&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the code for the website, serverless functions, and back-end services is available in the &lt;a href="https://github.com/diagrid-labs/catalyst-pizza-demo" rel="noopener noreferrer"&gt;catalyst-pizza-demo&lt;/a&gt; GitHub repository.  This repository contains a devcontainer configuration that has the following preinstalled: .NET 8, Node LTS, Vercel CLI, Diagrid CLI. &lt;/p&gt;

&lt;p&gt;You can use this devcontainer &lt;a href="https://code.visualstudio.com/docs/devcontainers/containers" rel="noopener noreferrer"&gt;locally in VSCode&lt;/a&gt; (requires &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;) or directly in &lt;a href="https://github.com/features/codespaces" rel="noopener noreferrer"&gt;GitHub Codespaces&lt;/a&gt;. The &lt;em&gt;npm install&lt;/em&gt; and &lt;em&gt;dotnet build&lt;/em&gt; commands described in this article can be skipped if the devcontainer is used.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/diagrid-labs/catalyst-pizza-demo/fork" rel="noopener noreferrer"&gt;Fork&lt;/a&gt; the &lt;a href="https://github.com/diagrid-labs/catalyst-pizza-demo" rel="noopener noreferrer"&gt;catalyst-pizza-demo&lt;/a&gt; and clone it locally, or use GitHub Codespaces (recommended).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Ably
&lt;/h3&gt;

&lt;p&gt;Ably is used as the serverless realtime messaging component. There is a default Ably app when you sign up for an account that can be used for this demo. Alternatively, you can create a new Ably app.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into the &lt;a href="https://ably.com/accounts/" rel="noopener noreferrer"&gt;Ably portal&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Using the Ably portal: copy the &lt;a href="https://ably.com/docs/ids-and-keys#api-key" rel="noopener noreferrer"&gt;Root API key&lt;/a&gt; from the Ably app. This will be copied later as an environment variable for both Vercel and Diagrid.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Vercel
&lt;/h3&gt;

&lt;p&gt;The Vue-based &lt;a href="https://github.com/diagrid-labs/catalyst-pizza-demo/tree/main/front-end/src" rel="noopener noreferrer"&gt;front-end&lt;/a&gt; and two &lt;a href="https://github.com/diagrid-labs/catalyst-pizza-demo/tree/main/front-end/api" rel="noopener noreferrer"&gt;JavaScript functions&lt;/a&gt; are hosted on Vercel. The &lt;a href="https://vercel.com/docs/cli" rel="noopener noreferrer"&gt;Vercel CLI&lt;/a&gt; is used to configure and run these resources locally.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open a terminal in the root of the repository and login with the Vercel CLI:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vercel login
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go to the &lt;em&gt;front-end&lt;/em&gt; folder and run:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go back to the root of the repository and set up the Vercel project by running:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vercel
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Follow the CLI prompts, and select the following options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup and deploy: &lt;code&gt;Y&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Scope: &lt;code&gt;&amp;lt;account name&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Link to existing project: &lt;code&gt;N&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;What's your project's name? &lt;code&gt;catalyst-pizza-project&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In which directory is your code located? &lt;code&gt;./front-end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Want to modify these settings? [y/N] &lt;code&gt;n&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Wait for the deployment to complete.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;An environment variable is used in the &lt;em&gt;getAblyToken&lt;/em&gt; function to generate a token for the website to communicate with the Ably realtime service. Add the &lt;em&gt;Ably API token&lt;/em&gt; variable by running &lt;code&gt;vercel env add&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variable name: &lt;code&gt;ABLY_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Variable value: &lt;em&gt;Use the Ably API key obtained from the Ably portal earlier&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Development&lt;/code&gt; as the environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Another environment variable is used in the &lt;em&gt;placeOrder&lt;/em&gt; function to send a request to the &lt;em&gt;PizzaOrderService&lt;/em&gt; that will start the workflow. Add the &lt;em&gt;WORKFLOW_URL&lt;/em&gt; variable by running &lt;code&gt;vercel env add&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variable name: &lt;code&gt;WORKFLOW_URL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Variable value: &lt;code&gt;http://localhost:5064/workflow/orderReceived&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Development&lt;/code&gt; as the environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;vercel pull&lt;/code&gt; to pull the configuration from Vercel to your local environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;vercel build&lt;/code&gt; to build the website and the serverless functions to ensure there are no errors.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Diagrid &lt;strong&gt;Catalyst&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Diagrid Catalyst provides serverless Dapr APIs that enables developers to quickly build distributed applications with workflow, pub/sub messaging, service invocation, and state management capabilities. Diagrid also provides managed infrastructure for storing data in a key/value store, pub/sub messaging, and workflow, which are all used in this solution.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.diagrid.io/catalyst/references/cli-reference/intro" rel="noopener noreferrer"&gt;Diagrid CLI&lt;/a&gt; is used to configure the resources and run the .NET services locally.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open another terminal in the root of the repository and use the Diagrid CLI to log in to Diagrid:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid login
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a new Catalyst project named &lt;em&gt;catalyst-pizza-project&lt;/em&gt; and use the Diagrid managed pub/sub broker &amp;amp; KV store, and enable the managed workflow API:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid project create catalyst-pizza-project &lt;span class="nt"&gt;--deploy-managed-pubsub&lt;/span&gt; &lt;span class="nt"&gt;--deploy-managed-kv&lt;/span&gt; &lt;span class="nt"&gt;--enable-managed-workflow&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To set this project as the default in the CLI run:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid project use catalyst-pizza-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a new App ID for the &lt;em&gt;PizzaOrderService&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid appid create pizzaorderservice
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a new App ID for the &lt;em&gt;KitchenService&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid appid create kitchenservice
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Before continuing, check the App ID status to make sure they are ready:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid appid list
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a pub/sub subscription for the &lt;em&gt;KitchenService&lt;/em&gt; to receive messages from the &lt;em&gt;PizzaOrderService&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid subscription create pizzaorderssub &lt;span class="nt"&gt;--connection&lt;/span&gt; pubsub &lt;span class="nt"&gt;--topic&lt;/span&gt; pizza-orders &lt;span class="nt"&gt;--route&lt;/span&gt; /prepare &lt;span class="nt"&gt;--scopes&lt;/span&gt; kitchenservice
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a pub/sub subscription for the &lt;em&gt;PizzaOrderService&lt;/em&gt; to receive messages from the &lt;em&gt;KitchenService&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid subscription create preparedorderssub &lt;span class="nt"&gt;--connection&lt;/span&gt; pubsub &lt;span class="nt"&gt;--topic&lt;/span&gt; prepared-orders &lt;span class="nt"&gt;--route&lt;/span&gt; /workflow/orderPrepared &lt;span class="nt"&gt;--scopes&lt;/span&gt; pizzaorderservice
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify that creation of the subscriptions is completed:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid subscription list
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;diagrid dev scaffold&lt;/code&gt; to create a new local dev environment file. This creates a yaml file, named &lt;em&gt;dev-.yaml&lt;/em&gt; with the following content:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;catalyst-pizza-project&lt;/span&gt;
&lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kitchenservice&lt;/span&gt;
&lt;span class="na"&gt;appPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DAPR_API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diagrid://&amp;lt;dapr_api_token&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;DAPR_APP_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kitchenservice&lt;/span&gt;
    &lt;span class="na"&gt;DAPR_GRPC_ENDPOINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;grpc_endpoint&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;DAPR_HTTP_ENDPOINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;http_endpoint&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;workDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kitchenservice&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pizzaorderservice&lt;/span&gt;
&lt;span class="na"&gt;appPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DAPR_API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diagrid://&amp;lt;dapr_api_token&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;DAPR_APP_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pizzaorderservice&lt;/span&gt;
    &lt;span class="na"&gt;DAPR_GRPC_ENDPOINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;grpc_endpoint&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;DAPR_HTTP_ENDPOINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;http_endpoint&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;workDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pizzaorderservice&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;span class="na"&gt;appLogDestination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This file is gitignored since it contains secrets from Diagrid Catalyst. The &lt;em&gt;appPort&lt;/em&gt;, &lt;em&gt;command&lt;/em&gt;, and &lt;em&gt;workDir&lt;/em&gt; attributes still need to be updated as follows: &lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Update the &lt;em&gt;appPort&lt;/em&gt; for the &lt;em&gt;kitchenservice&lt;/em&gt; to &lt;code&gt;5066&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Update the &lt;em&gt;appPort&lt;/em&gt; for the &lt;em&gt;pizzaorderservice&lt;/em&gt; to &lt;code&gt;5064&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Update the &lt;em&gt;command&lt;/em&gt; arguments to &lt;code&gt;["dotnet", "run"]&lt;/code&gt; for both apps.&lt;/li&gt;
&lt;li&gt;Update the &lt;em&gt;workDir&lt;/em&gt; argument to point to &lt;code&gt;back-end/KitchenService&lt;/code&gt; and &lt;code&gt;back-end/PizzaOrderService&lt;/code&gt; respectively.&lt;/li&gt;
&lt;li&gt;Update the &lt;em&gt;appLogDestination&lt;/em&gt; to &lt;code&gt;console&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add an &lt;code&gt;ABLY_API_KEY&lt;/code&gt; environment variable for the &lt;em&gt;pizzaorderservice&lt;/em&gt; appId and set the value to the Ably API key obtained from the Ably portal.&lt;/li&gt;
&lt;li&gt;Save the changes to the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Update the key/value store to allow shared state&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The two .NET services share the same state in the key/value store to manage inventory and orders. Since this is not the default usage of state stores and services when using Dapr, an attribute needs to be set to enable this. The default behavior is that a key is prefixed with the app ID of the service that is using it. In this case, however, the &lt;em&gt;keyPrefix&lt;/em&gt; is set to &lt;em&gt;name&lt;/em&gt; to make sure both services use the same keys.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Run the following command to update the managed key/value store:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diagrid connection apply &lt;span class="nt"&gt;-f&lt;/span&gt; ./infra/kv.yml
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This will upload the &lt;em&gt;kv.yml&lt;/em&gt; file to Diagrid and update the configuration of the &lt;em&gt;kvstore&lt;/em&gt; connection so &lt;em&gt;keyPrefix&lt;/em&gt; is set to &lt;em&gt;name&lt;/em&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Inspect the DaprClient configuration
&lt;/h3&gt;

&lt;p&gt;The two .NET services use the Dapr .NET client SDK for workflow, state management and pub/sub messaging. The &lt;em&gt;DaprClient&lt;/em&gt; is configured to use the endpoints provided by Diagrid Catalyst in the &lt;em&gt;&lt;a href="https://github.com/diagrid-labs/catalyst-pizza-demo/blob/main/back-end/PizzaOrderService/Program.cs" rel="noopener noreferrer"&gt;Program.cs&lt;/a&gt;&lt;/em&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;apiToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DAPR_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;grpcEndpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DAPR_GRPC_ENDPOINT"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;httpEndpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DAPR_HTTP_ENDPOINT"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDaprClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseDaprApiToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseGrpcEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grpcEndpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHttpEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpEndpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To use service invocation with the HTTP API, the &lt;em&gt;HttpClient&lt;/em&gt; is configured with the &lt;em&gt;DAPR_APP_ID&lt;/em&gt; and &lt;em&gt;DAPR_API_TOKEN&lt;/em&gt; environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;appId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DAPR_APP_ID"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;apiToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DAPR_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"daprEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpEndpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MediaTypeWithQualityHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dapr-app-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dapr-api-token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apiToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running &amp;amp; testing the solution
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open a terminal in the root of the repository.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To restore and build the .NET projects run:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet build ./back-end/PizzaOrderService
dotnet build ./back-end/KitchenService
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;diagrid dev start&lt;/code&gt; to start the &lt;em&gt;PizzaOrderService&lt;/em&gt; and the &lt;em&gt;KitchenService&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using another terminal in the root of the repository, run &lt;code&gt;vercel dev&lt;/code&gt; to start the website and the serverless functions locally.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigate to the URL provided by the Vercel CLI to view the website.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select some pizzas, place an order, and watch the progress of the workflow in realtime.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Using the Catalyst API explorer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;You can use the API explorer in the Catalyst web UI to interact with the APIs. If you want to retrieve the order item from the key/value store that has just been processed, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the developer tools console of the browser that is running the demo (a pizza order must be started or completed).&lt;/li&gt;
&lt;li&gt;The order is logged to the console as a JSON object. Copy the &lt;em&gt;OrderId&lt;/em&gt; property value.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F82q8tifruveh69ym3ek1.png" alt="Console order" width="800" height="199"&gt;
&lt;/li&gt;
&lt;li&gt;Navigate to the &lt;a href="https://catalyst.diagrid.io/" rel="noopener noreferrer"&gt;Catalyst web UI&lt;/a&gt;. Ensure you're in the &lt;em&gt;catalyst-pizza-project&lt;/em&gt; project.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg58vyhxqrhxj63rntjp0.png" alt="Catalyst API explorer" width="800" height="557"&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;em&gt;App IDs&lt;/em&gt; in and click on the &lt;em&gt;&lt;a href="https://catalyst.diagrid.io/app-ids/api-explorer/state-api/" rel="noopener noreferrer"&gt;API explorer&lt;/a&gt;&lt;/em&gt; tab.

&lt;ul&gt;
&lt;li&gt;Select the &lt;em&gt;State API&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Select the &lt;em&gt;pizzaorderservice&lt;/em&gt; as the app ID.&lt;/li&gt;
&lt;li&gt;Select &lt;em&gt;GET&lt;/em&gt; as the API operation.&lt;/li&gt;
&lt;li&gt;Select &lt;em&gt;kvstore&lt;/em&gt; as the state connection.&lt;/li&gt;
&lt;li&gt;Enter &lt;em&gt;Order-&lt;/em&gt; as the key, where &lt;em&gt;&lt;/em&gt; is substituted with the value copied from the developer tools console.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;em&gt;Send&lt;/em&gt;. The response should show the state of the order item.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What’s next?
&lt;/h2&gt;

&lt;p&gt;Congratulations! You’ve now used the Diagrid Catalyst serverless Dapr APIs for workflow, pub/sub messaging, service invocation, and state management. The ability to use these APIs from anywhere, without the overhead of managing Kubernetes clusters, brings great flexibility to developers on any platform to build distributed applications. The Dapr applications used in this demo can be hosted on any cloud. The demo uses the Diagrid managed infrastructure for key/value storage and pub/sub messaging, but these can be swapped out for other cloud-based resources, similarly to switching open-source Dapr components. You can extend this demo with an alternative pub/sub broker or state store by configuring other &lt;a href="https://docs.diagrid.io/catalyst/concepts/connections" rel="noopener noreferrer"&gt;infrastructure connections&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Any questions or comments about this demo? Join the &lt;a href="https://bit.ly/dapr-discord" rel="noopener noreferrer"&gt;Dapr Discord&lt;/a&gt; and drop us a message in the #diagrid channel!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dapr</category>
      <category>tutorial</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Dapr v1.11 Release Highlights</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Mon, 19 Jun 2023 12:27:24 +0000</pubDate>
      <link>https://dev.to/diagrid/dapr-v111-release-highlights-46b</link>
      <guid>https://dev.to/diagrid/dapr-v111-release-highlights-46b</guid>
      <description>&lt;p&gt;The Dapr maintainers released a new version of &lt;strong&gt;&lt;a href="https://dapr.io/"&gt;Dapr, the distributed application runtime&lt;/a&gt;&lt;/strong&gt;, last week. This post highlights the major new features and changes for the APIs and components in release 1.11.&lt;/p&gt;

&lt;h2&gt;
  
  
  APIs
&lt;/h2&gt;

&lt;p&gt;Dapr provides a common set of APIs for building microservices quickly and reliably. The new building block APIs, and major upgrades to existing APIs, are described in this section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration API stable
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VdpW4zpr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dy7r8v52ctan8ncjfopp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VdpW4zpr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dy7r8v52ctan8ncjfopp.png" alt="Dapr Configuration API" width="800" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/configuration/configuration-api-overview/"&gt;Configuration building block&lt;/a&gt; allows developers to easily consume application configuration values as key/value pairs. Use this API to retrieve values that are likely to change over time but are coupled to the service. For instance, this API can be used to get App IDs, which are used when invoking other Dapr services. Or to get a component name of a state store when using the State Management API.&lt;/p&gt;

&lt;p&gt;Applications can subscribe to changes of these configuration values, so the apps always use the latest value. The Configuration API was introduced in Dapr v1.5, and it’s great to see it now stable in v1.11.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get a configuration value&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;GET http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0/configuration/&lt;span class="nt"&gt;&amp;lt;storeName&amp;gt;&lt;/span&gt;?key=&lt;span class="nt"&gt;&amp;lt;keyName&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Subscribe to configuration changes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;GET http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0/configuration/&lt;span class="nt"&gt;&amp;lt;storeName&amp;gt;&lt;/span&gt;/subscribe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Service Invocation for non-Dapr endpoints 🆕
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SdrKIgp7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/odkcnl3ywq1xawm8k1gb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SdrKIgp7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/odkcnl3ywq1xawm8k1gb.png" alt="Non-Dapr service invocation" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An exciting new preview feature is the ability to invoke &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/service-invocation/howto-invoke-non-dapr-endpoints/"&gt;non-Dapr endpoints&lt;/a&gt;. This feature enables developers to apply resiliency policies, access control via scoping, header authentication, and middleware to any endpoint. This gives you nearly all the benefits of service invocation when calling any HTTP API. Invoking non-Dapr endpoints can also be very useful when working on existing code bases where Dapr can be introduced incrementally.&lt;/p&gt;

&lt;p&gt;Non-Dapr endpoints can be invoked in two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Using an &lt;code&gt;HTTPEndpoint&lt;/code&gt; resource type.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;GET http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0/invoke/&lt;span class="nt"&gt;&amp;lt;HTTPEndpoint-name&amp;gt;&lt;/span&gt;/method/&lt;span class="nt"&gt;&amp;lt;my-method&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The &lt;a href="https://docs.dapr.io/reference/resource-specs/httpendpoints-reference/"&gt;HTTPEndpoint resource&lt;/a&gt; is specified in a yaml file:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dapr.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTPEndpoint&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;NAME&amp;gt;&lt;/span&gt;  
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1alpha1&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;REPLACE-WITH-BASEURL&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# Required. Use "http://" or "https://" prefix.&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Optional&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;REPLACE-WITH-A-HEADER-NAME&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;REPLACE-WITH-A-HEADER-VALUE&amp;gt;&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;REPLACE-WITH-A-HEADER-NAME&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;REPLACE-WITH-SECRET-NAME&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;REPLACE-WITH-SECRET-KEY&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Optional&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;REPLACE-WITH-SCOPED-APPIDS&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Optional&lt;/span&gt;
  &lt;span class="na"&gt;secretStore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;REPLACE-WITH-SECRETSTORE&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Using the Fully Qualified Domain Name (FQDN) of the endpoint.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;GET http://localhost:&amp;lt;daprPort&amp;gt;/v1.0/invoke/&amp;lt;FQDN_URL&amp;gt;/method/&amp;lt;my-method&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Workflow updates
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--s5E7Kd7v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o95i0pkp765rbwkuoere.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--s5E7Kd7v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o95i0pkp765rbwkuoere.png" alt="Dapr Workflow management API" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Dapr Workflow management API has been updated to allow workflows to be paused, resumed, and purged. Purging workflows means the workflow state will be deleted. Once a workflow has been completed, it’s a good practice to purge the workflow state to keep the storage requirements to a minimum.  In addition, workflows can now wait for external events. This is particularly useful for business processes that involve human interaction, such as approval workflows, or waiting for callbacks from other long-running processes. &lt;/p&gt;

&lt;p&gt;These are the current Workflow management HTTP API methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;POST http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0-alpha1/workflows/dapr/&lt;span class="nt"&gt;&amp;lt;instanceID&amp;gt;&lt;/span&gt;/start
POST http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0-alpha1/workflows/dapr/&lt;span class="nt"&gt;&amp;lt;instanceID&amp;gt;&lt;/span&gt;/terminate
POST http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0-alpha1/workflows/dapr/&lt;span class="nt"&gt;&amp;lt;instanceID&amp;gt;&lt;/span&gt;/pause
POST http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0-alpha1/workflows/dapr/&lt;span class="nt"&gt;&amp;lt;instanceID&amp;gt;&lt;/span&gt;/resume
POST http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0-alpha1/workflows/dapr/&lt;span class="nt"&gt;&amp;lt;instanceID&amp;gt;&lt;/span&gt;/purge
POST http://localhost:&lt;span class="nt"&gt;&amp;lt;daprPort&amp;gt;&lt;/span&gt;/v1.0-alpha1/workflows/dapr/&lt;span class="nt"&gt;&amp;lt;instanceID&amp;gt;&lt;/span&gt;/raiseEvent/&lt;span class="nt"&gt;&amp;lt;eventName&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the Workflow management API, which is still in preview, has breaking changes compared to v1.10. The changes are outlined in this &lt;a href="https://github.com/dapr/dapr/pull/6218"&gt;GitHub issue&lt;/a&gt;. Another breaking change is the ability to support multiple workflow apps in a single cluster, as mentioned in this &lt;a href="https://github.com/dapr/dapr/issues/6013"&gt;GitHub issue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Dapr Python SDK has been upgraded to allow &lt;a href="https://docs.dapr.io/developing-applications/sdks/python/python-workflow/"&gt;authoring of workflows&lt;/a&gt;, so now workflow applications can be written in either C# or Python.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cryptography API 🆕
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gHNzyqDz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/344mt6chof11exh2qu80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gHNzyqDz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/344mt6chof11exh2qu80.png" alt="Dapr cryptography API" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/"&gt;Cryptography API&lt;/a&gt; is a new preview building block that enables developers to encrypt/decrypt data in a safe and consistent way. Cryptography can be used within key vaults or the Dapr sidecar without exposing cryptographic keys to the application. With this new API, a set of new Cryptography components is made available that is listed in the &lt;em&gt;New Components&lt;/em&gt; section.&lt;/p&gt;

&lt;p&gt;These examples show how data can be encrypted and decrypted with the Cryptography API using the Dapr JavaScript SDK:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When passing data (a buffer or string), `encrypt` returns a Buffer with the encrypted message&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Name of the Dapr component (required)&lt;/span&gt;
    &lt;span class="na"&gt;componentName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mycryptocomponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Name of the key stored in the component (required)&lt;/span&gt;
    &lt;span class="na"&gt;keyName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mykey&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Algorithm used for wrapping the key, which must be supported by the key named above.&lt;/span&gt;
    &lt;span class="c1"&gt;// Options include: "RSA", "AES"&lt;/span&gt;
    &lt;span class="na"&gt;keyWrapAlgorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RSA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Decryption&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When passing data as a buffer, `decrypt` returns a Buffer with the decrypted message&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plaintext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only required option is the component name&lt;/span&gt;
    &lt;span class="na"&gt;componentName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mycryptocomponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more info on how to use the Cryptography API, see the &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/cryptography/howto-cryptography/"&gt;Dapr docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components
&lt;/h2&gt;

&lt;p&gt;Dapr decouples the functionality of the common set of APIs with their underlying implementations via &lt;a href="https://docs.dapr.io/concepts/components-concept/"&gt;components&lt;/a&gt;. Components of the same type are interchangeable since they implement the same interface. Release 1.11 contains both new components as improvements to existing components.&lt;/p&gt;

&lt;h3&gt;
  
  
  New components
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bindings&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/wasm/"&gt;WASM&lt;/a&gt;: allows invoking applications compiled to &lt;a href="https://developer.mozilla.org/en-US/docs/WebAssembly"&gt;WebAssembly&lt;/a&gt; and run in modern browsers.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/kitex/"&gt;Kitex&lt;/a&gt;: allows invoking Generic Calls in &lt;a href="https://www.cloudwego.io/docs/kitex/overview/"&gt;Kitex&lt;/a&gt;, a high-performance and strong-extensibility Golang RPC framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;State Stores&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-etcd/"&gt;etcd&lt;/a&gt;: enables state store functionality using &lt;a href="https://etcd.io/"&gt;etcd&lt;/a&gt;, a distributed, reliable key-value store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cryptography&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-cryptography/azure-key-vault/"&gt;Azure Key Vault&lt;/a&gt;: enables cryptography functionality using &lt;a href="https://azure.microsoft.com/en-us/products/key-vault/"&gt;Azure Key Vault&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-cryptography/local-storage/"&gt;Local storage&lt;/a&gt;: enables cryptography functionality using local files. Supported file formats are PEM, JSON Web Key, and raw key data for symmetric keys.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-cryptography/json-web-key-sets/"&gt;JWKS&lt;/a&gt;: enables cryptography functionality using &lt;a href="https://www.rfc-editor.org/rfc/rfc7517"&gt;JSON Web Key&lt;/a&gt; Sets (JWKS).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-cryptography/kubernetes-secrets/"&gt;Kubernetes Secrets&lt;/a&gt;: enables cryptography functionality using &lt;a href="https://kubernetes.io/docs/concepts/configuration/secret/"&gt;Kubernetes secrets&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Component improvements
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9Ug1fGiK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/btnok059nh0go6vvnzi3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9Ug1fGiK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/btnok059nh0go6vvnzi3.png" alt="Dapr component lifecycle" width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With 1.11 many components have graduated to the &lt;em&gt;Stable&lt;/em&gt; certification level, which means these can be used in production confidently. All components have a certification level as mentioned in the &lt;strong&gt;&lt;a href="https://docs.dapr.io/operations/components/certification-lifecycle/"&gt;Certification Lifecycle&lt;/a&gt;&lt;/strong&gt;. The &lt;em&gt;Stable&lt;/em&gt; level means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The component must have component &lt;strong&gt;&lt;a href="https://docs.dapr.io/operations/components/certification-lifecycle/#certification-tests"&gt;certification tests&lt;/a&gt;&lt;/strong&gt; validating functionality and resiliency.&lt;/li&gt;
&lt;li&gt;The component is maintained by Dapr maintainers and supported by the community.&lt;/li&gt;
&lt;li&gt;The component is well documented and tested.&lt;/li&gt;
&lt;li&gt;The component has been available as Alpha or Beta for at least 1 minor version release of Dapr runtime prior.&lt;/li&gt;
&lt;li&gt;A maintainer will address component security, core functionality and test issues according to the Dapr support policy and issue a patch release that includes the patched stable component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following components have progressed to &lt;em&gt;Stable&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bindings&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/s3/"&gt;AWS S3&lt;/a&gt;: output binding to store data in AWS S3 buckets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Middleware&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-middleware/middleware-rate-limit/"&gt;Rate limit&lt;/a&gt;: restricts the maximum allowed HTTP requests per second.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-middleware/middleware-bearer/"&gt;OpenID Connect&lt;/a&gt;: verifies a bearer token using &lt;a href="https://openid.net/connect/"&gt;OpenID Connect&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pub/Sub&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-gcp-pubsub/"&gt;GCP Pub/Sub&lt;/a&gt;: enables pub/sub messaging using Google Cloud Platform &lt;a href="https://cloud.google.com/pubsub/"&gt;Pub/Sub&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;State stores&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-firestore/"&gt;GCP Firestore&lt;/a&gt;: enables state store functionality using Google Cloud Platform &lt;a href="https://cloud.google.com/firestore/"&gt;Firestore&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-sqlite/"&gt;SQLite&lt;/a&gt;: enables state store functionality using &lt;a href="https://www.sqlite.org/index.html"&gt;SQLite 3&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration stores&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-configuration-stores/postgresql-configuration-store/"&gt;PostgreSQL&lt;/a&gt;: enables configuration store functionality using &lt;a href="https://www.postgresql.org/"&gt;PostgreSQL&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-configuration-stores/redis-configuration-store/"&gt;Redis&lt;/a&gt;: enables configuration store functionality using &lt;a href="https://redis.io/"&gt;Redis&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're using any of these components and have a demo to show, drop a link in the &lt;em&gt;#show-and-tell&lt;/em&gt; channel on the &lt;strong&gt;&lt;a href="http://bit.ly/dapr-discord"&gt;Dapr Discord&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;*&lt;strong&gt;&lt;em&gt;Azure components authentication improvements&lt;/em&gt;&lt;/strong&gt;*&lt;/p&gt;

&lt;p&gt;Azure AD authentication has been added to the &lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/storagequeues/"&gt;Azure Storage Queues binding&lt;/a&gt; and the &lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-sqlserver/"&gt;SQL Server state store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All Azure components that support Azure AD now support authentication using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview"&gt;Azure AD Workload Identity&lt;/a&gt; on Kubernetes&lt;/li&gt;
&lt;li&gt;Azure CLI &lt;a href="https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli"&gt;credentials&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What is next?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This post is not a complete list of features and changes released in version 1.11. Read the official &lt;strong&gt;&lt;a href="https://blog.dapr.io/posts/2023/06/12/dapr-v1.11-is-now-available/"&gt;Dapr release notes&lt;/a&gt;&lt;/strong&gt; for more information. The release notes also contain information on how to upgrade to this latest version.&lt;/p&gt;

&lt;p&gt;Excited about these features and want to learn more? I'll cover the new features in more detail in future posts. Until then, join the &lt;strong&gt;&lt;a href="http://bit.ly/dapr-discord"&gt;Dapr Discord&lt;/a&gt;&lt;/strong&gt; to connect with thousands of Dapr users.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cloudnative</category>
      <category>cloud</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Creating a Dapr pluggable component for Supabase</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Tue, 25 Apr 2023 14:56:41 +0000</pubDate>
      <link>https://dev.to/diagrid/creating-a-dapr-pluggable-component-for-supabase-32kj</link>
      <guid>https://dev.to/diagrid/creating-a-dapr-pluggable-component-for-supabase-32kj</guid>
      <description>&lt;p&gt;In this post, you’ll learn how to use the Dapr pluggable component .NET SDK and the Supabase C# library to build a pluggable state store component that uses Supabase tables.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you prefer watching a video instead of reading this blog post, head over to YouTube and watch this:&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/L0RRnWfGCzo"&gt;
&lt;/iframe&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Dapr, the open-source, portable, event-driven runtime for building distributed applications, comes with over &lt;a href="https://docs.dapr.io/concepts/components-concept/"&gt;100 built-in components&lt;/a&gt; which can be used to integrate resources across many clouds or on-premise systems. If those components are not enough, or you want to create something custom for your specific needs, you can create a &lt;a href="https://docs.dapr.io/operations/components/pluggable-components/pluggable-components-overview/"&gt;pluggable component&lt;/a&gt;. The Dapr building block APIs that are pluggable are: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State management&lt;/li&gt;
&lt;li&gt;Publish and subscribe&lt;/li&gt;
&lt;li&gt;Input and output bindings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since Dapr &lt;a href="https://www.diagrid.io/blog/dapr-1-10-release-highlights"&gt;release 1.10&lt;/a&gt; there are &lt;a href="https://docs.dapr.io/developing-applications/develop-components/pluggable-components/pluggable-components-sdks/pluggable-components-dotnet/"&gt;pluggable component SDKs&lt;/a&gt; available that make it easier to create them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://supabase.com/"&gt;Supabase&lt;/a&gt; is a popular, open-source, Firebase alternative that offers Postgres databases, authentication, Edge functions, real-time messaging and file storage. The Supabase community has released a &lt;a href="https://github.com/supabase-community/supabase-csharp"&gt;C#/.NET library&lt;/a&gt; that developers can use to integrate their .NET apps with the Supabase platform. For managing Postgres databases, the library uses the &lt;a href="https://github.com/supabase-community/postgrest-csharp"&gt;postgrest-csharp&lt;/a&gt; library that allows developers to use strongly typed models and LINQ queries.&lt;/p&gt;

&lt;p&gt;This post describes the steps required to make a proof of concept Dapr pluggable component using .NET 7 that combines both the Dapr pluggable component .NET SDK and the Supabase C# library. The result is an ASP.NET application, that is run locally, and tested by making HTTP requests to interact with the state stored in a Supabase table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Z3XLZdzX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/089owyntaiwxeiunwhtz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z3XLZdzX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/089owyntaiwxeiunwhtz.png" alt="Image description" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Prerequisites
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dotnet.microsoft.com/download/dotnet/7.0"&gt;.NET 7 SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dapr.io/getting-started/install-dapr-cli/"&gt;Dapr CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A Supabase account, you can &lt;a href="https://app.supabase.com/sign-up"&gt;sign up&lt;/a&gt; for free.&lt;/li&gt;
&lt;li&gt;Supported OS: macOS, Linux, &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/install"&gt;WSL&lt;/a&gt; on Windows.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. Creating the DaprPluggableSupabase .NET project
&lt;/h2&gt;

&lt;p&gt;If you’re more interested in using the pluggable component instead of creating it from scratch, continue with section &lt;strong&gt;3. Supabase Setup&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Clone &lt;a href="https://github.com/diagrid-labs/dapr-pluggable-component-supabase"&gt;this repo&lt;/a&gt;, open a terminal and navigate to the &lt;code&gt;src&lt;/code&gt; folder. &lt;/p&gt;

&lt;p&gt;The cloned repository already contains an &lt;a href="http://ASP.NET"&gt;ASP.NET&lt;/a&gt; app in the &lt;code&gt;DaprPluggableSupabase&lt;/code&gt; folder that is the pluggable component. You can use this as a reference. &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a new .NET web project, and make sure to use a different name than &lt;code&gt;DaprPluggableSupabase&lt;/code&gt; (e.g. &lt;code&gt;MyDaprPluggableSupabase&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet new web --name MyDaprPluggableSupabase&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change to the &lt;code&gt;MyDaprPluggableSupabase&lt;/code&gt; folder and add the &lt;code&gt;Dapr.PluggableComponents.AspNetCore&lt;/code&gt; and &lt;code&gt;supabase-csharp&lt;/code&gt; NuGet packages:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cd MyDaprPluggableSupabase&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet add package Dapr.PluggableComponents.AspNetCore&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet add package supabase-csharp&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a new class file in the &lt;code&gt;MyDaprPluggableSupabase&lt;/code&gt; folder named &lt;code&gt;KeyValue.cs&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add the following content to this new class:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Postgrest.Attributes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Postgrest.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;DaprPluggableSupabase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dapr_state_store"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KeyValue&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;PrimaryKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;KeyValue&lt;/code&gt; class defines the data type that will be stored in the Supabase table. It inherits from &lt;code&gt;BaseModel&lt;/code&gt;, which is part of the Supabase postgrest-csharp library. This library uses attributes that can be used to decorate the class and properties, which makes it easier for developers to use strongly typed models when interacting with the database table. Dapr state stores are key/value stores and Postgres databases are object-relational data stores. Since this is not a perfect match, the Dapr key/value-based state management API is mapped to this &lt;code&gt;KeyValue&lt;/code&gt; model to fit the Postgres schema.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new class file in the &lt;code&gt;MyDaprPluggableSupabase&lt;/code&gt; folder named &lt;code&gt;SupabaseStateStore.cs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the following content to this new class:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dapr.PluggableComponents.Components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dapr.PluggableComponents.Components.StateStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;DaprPluggableSupabase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SupabaseStateStore&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IStateStore&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="n"&gt;disable&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt; &lt;span class="n"&gt;_supabaseClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt;

        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;PROJECT_APIKEY_KEYWORD&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"projectApiKey"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;PROJECT_URL_KEYWORD&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"projectUrl"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DeleteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StateStoreDeleteRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_supabaseClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;StateStoreGetResponse&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StateStoreGetRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetKV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;valueAsBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StateStoreGetResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;valueAsBytes&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StateStoreGetResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MetadataRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROJECT_URL_KEYWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;projectUrl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Missing required property \"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROJECT_URL_KEYWORD&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\" in component file."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROJECT_APIKEY_KEYWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;projectApiKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Missing required property \"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROJECT_APIKEY_KEYWORD&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\" in component file."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;_supabaseClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projectUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;projectApiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_supabaseClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StateStoreSetRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newKV&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KeyValue&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;existingKV&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetKV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingKV&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;newKV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existingKV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_supabaseClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newKV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_supabaseClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newKV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetKV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_supabaseClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="n"&gt;disable&lt;/span&gt;
                            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt;
                            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the class inherits from &lt;code&gt;IStateStore&lt;/code&gt;, this is the interface that all Dapr state store components implement. The interface contains three methods that require implementing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DeleteAsync&lt;/code&gt;, this will delete a value from the store.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GetAsync&lt;/code&gt;, this will retrieve a value from the store.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SetAsync&lt;/code&gt;, this will insert/update a value to the store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All methods use the Supabase C# client to manage the data via the &lt;code&gt;KeyValue&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IStateStore&lt;/code&gt; interface inherits the &lt;code&gt;IPluggableComponent&lt;/code&gt; interface that contains only one method, &lt;code&gt;InitAsync&lt;/code&gt;, and this method used to initialize the state store. In this case, it is used to create an instance of the &lt;code&gt;Supabase.Client&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update the existing &lt;code&gt;Program.cs&lt;/code&gt; file with the following content:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dapr.PluggableComponents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;DaprPluggableSupabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DaprPluggableComponentsApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"supabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;serviceBuilder&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;serviceBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterStateStore&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SupabaseStateStore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This class defines the application as a &lt;code&gt;DaprPluggableComponentApplication&lt;/code&gt; and registers the &lt;code&gt;SupabaseStateStore&lt;/code&gt; as a new state store that will communicate with Dapr applications via a Unix domain socket named &lt;code&gt;supabase&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Build the application to make sure there are no compilation errors:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet build&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. &lt;strong&gt;Supabase Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The pluggable component will interact with a Supabase Postgres table. This section describes how to create the table using the Supabase dashboard and where to find the project URL and API key required in the next section.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Login to Supabase and create a new project.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a new database table with the following specifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;dapr_state_store&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Primary Key: &lt;code&gt;id&lt;/code&gt;, (int8, not null)&lt;/li&gt;
&lt;li&gt;Columns:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; (timestamptz, not null)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;key&lt;/code&gt; (text, not null)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;value&lt;/code&gt; (text, null)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RLS (Row Level Security) is disabled&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that for production use, &lt;a href="https://supabase.com/docs/guides/auth/row-level-security"&gt;Row Level Security (RLS)&lt;/a&gt; should be enabled, and access policies should be added to the table.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Table definition:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dapr_state_store&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;dapr_state_store_pkey&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;dapr_state_store_id_key&lt;/span&gt; &lt;span class="k"&gt;unique&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;tablespace&lt;/span&gt; &lt;span class="n"&gt;pg_default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll need the Supabase project URL and public API key to configure the Dapr component file in the next section. This information is found in the Supabase dashboard in the project &lt;code&gt;Settings &amp;gt; API&lt;/code&gt; tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;strong&gt;Update the Dapr pluggable Supabase component file&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Dapr uses a modular design where functionality is delivered as a &lt;a href="https://docs.dapr.io/concepts/components-concept/"&gt;component&lt;/a&gt;. A component file contains the specification of a component, including the name, the component type, and related metadata that is specific to connecting with the underlying resource. The component file for the Supabase state store looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dapr.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Component&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pluggable-supabase&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;state.supabase&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projectUrl&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projectApiKey&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value of the &lt;code&gt;spec.type&lt;/code&gt; field, &lt;code&gt;state.supabase&lt;/code&gt;, consists of two parts: the component type (&lt;code&gt;state&lt;/code&gt;), and the socket name (&lt;code&gt;supabase&lt;/code&gt;). The socket name needs to match with the socket name argument provided in the &lt;code&gt;RegisterService&lt;/code&gt; method in the &lt;code&gt;Program.cs&lt;/code&gt; class of section 2. A template of this component file is available in the repository, follow these steps to update the file, so it can be used locally.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;code&gt;resources&lt;/code&gt; folder in this repository.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rename the &lt;code&gt;resources\pluggableSupabase.yml.template&lt;/code&gt; file to &lt;code&gt;resources\pluggableSupabase.yml&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The pluggableSupabase.yml file is added to .gitignore, so it won't be accidentally committed to source control for this demo app. For production use, the yaml files should be checked into source control and &lt;a href="https://docs.dapr.io/operations/components/component-secrets/"&gt;secret store references&lt;/a&gt; should be used, instead of plain text values.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the &lt;code&gt;pluggableSupabase.yml&lt;/code&gt; file and update the values for &lt;code&gt;projectUrl&lt;/code&gt; and &lt;code&gt;projectApiKey&lt;/code&gt; obtained in the previous section.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Save the file and copy it to the user's Dapr components folder: &lt;code&gt;~/.dapr/components&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the Dapr CLI is run, all the component files in this folder will be loaded, so the pluggable Supabase component should be available.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. &lt;strong&gt;Run the DaprPluggableSupabase application&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Regular Dapr components are part of the Dapr runtime and don't require additional processes to run. Pluggable components, however, are not part of the Dapr runtime and need to be run separately, which is done in this section.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DaprPluggableSupabase&lt;/code&gt; project in this repo implements the Dapr state store interface and uses the &lt;a href="https://github.com/supabase-community/supabase-csharp"&gt;Supabase C# library&lt;/a&gt; to access a Supabase table. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a terminal and navigate to the 

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;src/DaprPluggableSupabase&lt;/code&gt; folder to use the included pluggable component project 
&lt;em&gt;or&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/MyDaprPluggableSupabase&lt;/code&gt; folder to use the pluggable component project you’ve just created yourself.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Build the project:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet build&lt;/code&gt;&lt;/p&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Run the project:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet run&lt;/code&gt;&lt;/p&gt;


&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. &lt;strong&gt;Run the Dapr process and test the Supabase state store&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open a new terminal and use the Dapr CLI to run the Dapr process&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dapr run --app-id myapp --dapr-http-port 3500&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Expected output:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The output should contain an INFO message that the pluggable-supabase component is loaded:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;INFO[0000] component loaded. name: pluggable-supabase, type: state.supabase/v1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The log should end with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ℹ️  Dapr sidecar is up and running.&lt;br&gt;
✅  You're up and running! Dapr logs will appear here.&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set a new state by making a POST request to the state management endpoint:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl --request POST --url http://localhost:3500/v1.0/state/pluggable-supabase --header 'content-type: application/json' --data '[{"key": "key1","value": "This is stored in Supabase!"}]'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Expected output:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HTTP 204 No Content&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Retrieve the new state using a GET request:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl --request GET --url http://localhost:3500/v1.0/state/pluggable-supabase/key1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Expected output:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HTTP 200 OK&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"This is stored in Supabase!"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or have a look at the Supabase dashboard to see the new state record in the table.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You've now successfully created and used the Dapr pluggable Supabase state store component. 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;This proof of concept implementation of a pluggable component demonstrates how the Dapr pluggable component .NET SDK makes it easier for developers to create their custom components. You can use &lt;a href="https://github.com/diagrid-labs/dapr-pluggable-component-supabase"&gt;this solution&lt;/a&gt; as a starting point, and expand it with additional state management API features, such as entity tag (ETag), metadata, and options, as described in the &lt;a href="https://docs.dapr.io/reference/api/state_api"&gt;specification&lt;/a&gt;. A requirement for using this pluggable component in a real project is to &lt;a href="https://docs.dapr.io/operations/hosting/"&gt;deploy it&lt;/a&gt; to a (managed) Kubernetes environment or use it in a self-hosted mode.&lt;/p&gt;

&lt;p&gt;The Supabase platform offers a great variety of useful back-end features, and with the set of client libraries it’s very accessible to a diverse group of developers. Take a look at &lt;a href="https://supabase.com/launch-week"&gt;Supabase Launch Week&lt;/a&gt; to read about their latest feature announcements.&lt;/p&gt;

&lt;p&gt;From my perspective, I’d like to explore further how Dapr can integrate with other Supabase features. It would also be great to see a Supabase state store as a built-in component that’s available in the Dapr runtime without the need of running the pluggable component separately. I also hope the proposed &lt;a href="https://github.com/dapr/dapr/issues/5146"&gt;DocumentStore building block&lt;/a&gt; will get some traction this year, since this will pair up very nicely with Supabase and other PostgreSQL stores. &lt;/p&gt;

&lt;p&gt;Do you have any questions or comments about this blog post or the code? Join the &lt;strong&gt;&lt;a href="https://bit.ly/dapr-discord"&gt;Dapr discord&lt;/a&gt;&lt;/strong&gt; and post a message in the &lt;code&gt;#pluggable-components&lt;/code&gt; channel. Have you made something with Dapr? Post a message in the &lt;code&gt;#show-and-tell&lt;/code&gt; channel, we love to see your creations!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>cloud</category>
      <category>tutorial</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Create event-driven applications with Cloudflare queues and Dapr</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Thu, 09 Mar 2023 14:20:59 +0000</pubDate>
      <link>https://dev.to/diagrid/create-event-driven-applications-with-cloudflare-queues-and-dapr-3an8</link>
      <guid>https://dev.to/diagrid/create-event-driven-applications-with-cloudflare-queues-and-dapr-3an8</guid>
      <description>&lt;p&gt;In this post, you’ll learn how to build a cloud to edge event-driven application with Dapr and Cloudflare. You’ll learn how to create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Cloudflare queue.&lt;/li&gt;
&lt;li&gt;A consumer Cloudflare worker (in TypeScript) that reads messages from the queue.&lt;/li&gt;
&lt;li&gt;A producer Dapr app (in TypeScript) that uses the Cloudflare Queue binding to publish messages to the queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hNKtXXn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4co04m0zqy3cis62o3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hNKtXXn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4co04m0zqy3cis62o3m.png" alt="Sending messages from a Dapr app to Cloudflare Queues" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Event-driven applications FTW 🚀
&lt;/h2&gt;

&lt;p&gt;Event-driven applications are becoming increasingly popular. The decoupling of event producers and event consumers makes applications more resilient and flexible, since they allow independent implementation and scaling of the applications. &lt;/p&gt;

&lt;p&gt;Cloudflare recently announced &lt;a href="https://developers.cloudflare.com/queues/"&gt;Queues&lt;/a&gt;, allowing developers to send and receive messages with guaranteed delivery and integrating with Cloudflare Workers, a fast edge computing platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dapr.io/"&gt;Dapr&lt;/a&gt;, the open-source distributed application runtime, is often used in event-driven applications. Dapr provides a set of standardized API building blocks that simplify microservice development. By using the &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/bindings/bindings-overview/"&gt;Bindings building block&lt;/a&gt;, developers can use input, and output bindings, and either trigger their apps or invoke other resources without having to learn resource-specific SDKs for these resources. With &lt;a href="https://www.diagrid.io/blog/dapr-1-10-release-highlights"&gt;Dapr release 1.10&lt;/a&gt;, a &lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/cloudflare-queues/"&gt;new binding&lt;/a&gt; is provided that allows developers to publish messages to Cloudflare Queues. Because of the common set of APIs that Dapr offers, developers from any background can use the binding to publish messages to Cloudflare Queues without needing to know the Cloudflare SDKs or adding that dependency to their codebase.&lt;/p&gt;

&lt;p&gt;Let’s start building an event-driven app and see the binding in action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;The following is required to run this sample:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clone &lt;a href="https://github.com/diagrid-labs/dapr-cloudflare-queues"&gt;this repository&lt;/a&gt; to your local machine.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;a href="https://docs.dapr.io/getting-started/install-dapr-cli/"&gt;Dapr CLI&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Use the Dapr CLI to install the &lt;a href="https://docs.dapr.io/getting-started/install-dapr-selfhost/"&gt;Dapr runtime locally&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dapr init&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install &lt;a href="https://nodejs.org/en/download/"&gt;Node.js&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install &lt;a href="https://developers.cloudflare.com/workers/wrangler/install-and-update/"&gt;Cloudflare Wrangler&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure you're on a Cloudflare paid plan, since that is required to use Cloudflare queues.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Enable Queues in the Cloudflare dashboard.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard &amp;gt; Workers &amp;gt; Queues&lt;/li&gt;
&lt;li&gt;Enable Queues Beta&lt;/li&gt;
&lt;li&gt;You should see a confirmation in the dashboard that queues are enabled.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t7U2fY_3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gum1sg2hv31xt9xn1jc9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t7U2fY_3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gum1sg2hv31xt9xn1jc9.png" alt="Enabled Cloudflare Queues" width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the applications
&lt;/h2&gt;

&lt;p&gt;The solution consists of three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Cloudflare queue&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;consumer&lt;/em&gt; Cloudflare worker that reads messages from the queue.&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;producer&lt;/em&gt; Dapr app that will publish messages to the queue.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Create a Cloudflare queue
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open a terminal and use the wrangler CLI to login to Cloudflare:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wrangler login&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Follow the instructions in the browser to login to Cloudflare.&lt;/p&gt;

&lt;p&gt;The response in the terminal should end with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Successfully logged in.&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create the Cloudflare queue using the wrangler CLI:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wrangler queues create dapr-messages&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The response in the terminal should end with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Created queue dapr-messages.&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. Create a consumer Cloudflare worker
&lt;/h2&gt;

&lt;p&gt;You can either create a new &lt;em&gt;consumer&lt;/em&gt; worker by following steps 1-3, or use the &lt;a href="https://github.com/diagrid-labs/dapr-cloudflare-queues/tree/main/consumer"&gt;existing &lt;em&gt;consumer&lt;/em&gt; worker&lt;/a&gt; in this repository and continue from step 4.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;In the root folder, create a worker to consume messages:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wrangler init consumer&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create package.json: &lt;code&gt;Y&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use TypeScript: &lt;code&gt;Y&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create worker: &lt;code&gt;Fetch handler&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Write tests: &lt;code&gt;N&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A new folder named &lt;em&gt;consumer&lt;/em&gt; will be created which contains the worker.&lt;/p&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Update the &lt;em&gt;consumer/src/index.ts&lt;/em&gt; file to:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageBatch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Add the following lines to the &lt;em&gt;consumer/wrangler.toml&lt;/em&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[queues.consumers]]&lt;/span&gt;
&lt;span class="py"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"dapr-messages"&lt;/span&gt;
&lt;span class="py"&gt;max_batch_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Ensure that you're in the &lt;em&gt;consumer&lt;/em&gt; folder and install the dependencies:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cd consumer&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/p&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Publish the &lt;em&gt;consumer&lt;/em&gt; worker:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wrangler publish&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The response in the terminal should end with:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;Published consumer &lt;span class="o"&gt;(&lt;/span&gt;... sec&lt;span class="o"&gt;)&lt;/span&gt;
  https://consumer.&amp;lt;SUBDOMAIN&amp;gt;.workers.dev
  Consumer &lt;span class="k"&gt;for &lt;/span&gt;dapr-messages
Current Deployment ID: &amp;lt;DEPLOYMENT_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Start a tail to read the log of the consumer worker:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wrangler tail&lt;/code&gt;&lt;/p&gt;


&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Configure the producer Dapr app
&lt;/h2&gt;

&lt;p&gt;The Cloudflare Dapr binding uses a Cloudflare worker to publish messages, since only Cloudflare workers can access the queue.&lt;/p&gt;

&lt;p&gt;There are two options for this worker:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dapr provisions the worker.&lt;/li&gt;
&lt;li&gt;You use a pre-provisioned Cloudflare worker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sample uses option 1. Read the &lt;a href="https://v1-10.docs.dapr.io/reference/components-reference/supported-bindings/cloudflare-queues/#configuring-the-worker"&gt;Cloudflare Queues binding spec&lt;/a&gt; and choose &lt;em&gt;Manually provision the Worker script&lt;/em&gt; if you want to go for option 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a binding file
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Rename the &lt;code&gt;producer/resources/binding.yaml.template&lt;/code&gt; to &lt;code&gt;producer/resources/binding.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open the &lt;code&gt;binding.yaml&lt;/code&gt; file and inspect its content.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dapr.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Component&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare-queues&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bindings.cloudflare.queues&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="c1"&gt;# Increase the initTimeout if Dapr is managing the Worker for youinitTimeout: "120s"&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="c1"&gt;# Name of the existing Cloudflare Queue (required)- name: queueName&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dapr-messages"&lt;/span&gt;
&lt;span class="c1"&gt;# Name of the Worker (required)- name: workerName&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dapr-message-worker"&lt;/span&gt;
&lt;span class="c1"&gt;# PEM-encoded private Ed25519 key (required)- name: key&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;-----BEGIN PRIVATE KEY-----&lt;/span&gt;
        &lt;span class="s"&gt;MC4CAQ...&lt;/span&gt;
        &lt;span class="s"&gt;-----END PRIVATE KEY-----&lt;/span&gt;
&lt;span class="c1"&gt;# Cloudflare account ID (required to have Dapr manage the Worker)- name: cfAccountID&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# API token for Cloudflare (required to have Dapr manage the Worker)- name: cfAPIToken&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# URL of the Worker (required if the Worker has been pre-created outside of Dapr)- name: workerUrl&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The &lt;code&gt;metadata.name&lt;/code&gt;, &lt;code&gt;spec.metadata.queueName&lt;/code&gt; and &lt;code&gt;spec.metadata.workerName&lt;/code&gt; values have already been set. Ensure that the &lt;code&gt;queueName&lt;/code&gt; matches the &lt;code&gt;queue&lt;/code&gt; setting in the &lt;em&gt;consumer&lt;/em&gt; worker &lt;code&gt;wrangler.toml&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Values for &lt;code&gt;spec.metadata.key&lt;/code&gt;, &lt;code&gt;spec.metadata.cfAccountID&lt;/code&gt;, and &lt;code&gt;spec.metadata.cfAPIToken&lt;/code&gt; still need to be provided.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Follow &lt;a href="https://v1-10.docs.dapr.io/reference/components-reference/supported-bindings/cloudflare-queues/#generate-an-ed25519-key-pair"&gt;these instructions&lt;/a&gt; in the Dapr docs to set the value for &lt;code&gt;spec.metadata.key&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Cloudflare account ID should go in the &lt;code&gt;spec.metadata.cfAccountID&lt;/code&gt; field. You can find the account ID in the Cloudflare dashboard URL: &lt;code&gt;https://dash.cloudflare.com/&amp;lt;ACCOUNT_ID&amp;gt;/workers/overview&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A Cloudflare API token should go in the &lt;code&gt;spec.metadata.cfAPIToken&lt;/code&gt; field. It can be generated as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Cloudflare dashboard, go to the Workers page.&lt;/li&gt;
&lt;li&gt;Click the &lt;em&gt;API tokens&lt;/em&gt; link&lt;/li&gt;
&lt;li&gt;Click the &lt;em&gt;Create token&lt;/em&gt; button&lt;/li&gt;
&lt;li&gt;Click the &lt;em&gt;Use template&lt;/em&gt; button for Edit Cloudflare Workers&lt;/li&gt;
&lt;li&gt;Update the permissions to only contain:

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Account&lt;/em&gt; | &lt;em&gt;Worker Scripts&lt;/em&gt; | &lt;em&gt;Edit&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Update the Account Resources to only contain:

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Include&lt;/em&gt; | &lt;em&gt;&amp;lt;YOUR ACCOUNT&amp;gt;&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Set a time to live (TTL) for the token, the shorter, the better, if you're just testing.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now the binding file is complete. The file is gitignored, so the secrets won't be committed to the repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the Node app
&lt;/h3&gt;

&lt;p&gt;Let's have a look at the Dapr app that will send messages to the Cloudflare queue.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Inspect the &lt;code&gt;producer/index.ts&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DaprClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@dapr/dapr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Common settingsconst daprHost = "http://localhost";&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daprPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DAPR_HTTP_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Starting...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bindingName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cloudflare-queues&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bindingOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;DaprClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daprHost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;daprPort&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bindingName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bindingOperation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Completed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Note that the &lt;code&gt;bindingName&lt;/code&gt; is set to &lt;code&gt;cloudflare-queues&lt;/code&gt; and matches the value of the &lt;code&gt;metadata.name&lt;/code&gt; in the &lt;code&gt;binding.yaml&lt;/code&gt;. The &lt;code&gt;bindingOperation&lt;/code&gt; is set to &lt;code&gt;publish&lt;/code&gt; (&lt;code&gt;create&lt;/code&gt; could be used as an alias).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Run the producer app
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open a new terminal window and navigate to the &lt;code&gt;producer&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install the dependencies:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run the following command to start the producer app:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dapr run &lt;span class="nt"&gt;--app-id&lt;/span&gt; producer &lt;span class="nt"&gt;--resources-path&lt;/span&gt; ./resources &lt;span class="nt"&gt;--&lt;/span&gt; npm run start
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The terminal that logs the tail of the consumer app should show a log statement for each of the ten messages sent:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Unknown Event - Ok @ 17/02/2023, 11:22:50
  &lt;span class="o"&gt;(&lt;/span&gt;log&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"body"&lt;/span&gt;:&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Hello World 1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;,&lt;span class="s2"&gt;"timestamp"&lt;/span&gt;:&lt;span class="s2"&gt;"2023-02-17T10:22:50.556Z"&lt;/span&gt;,&lt;span class="s2"&gt;"id"&lt;/span&gt;:&lt;span class="s2"&gt;"8f6293d9d04001e3f2a12be5c47acde2"&lt;/span&gt;&lt;span class="o"&gt;}]&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jM08LsYc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0sjnrtrncscu92dmqc4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jM08LsYc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0sjnrtrncscu92dmqc4.gif" alt="wrangler log tail of the consumer" width="800" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations! You’ve now sent messages from a Dapr app to a Cloudflare Queue (&amp;amp; Worker) via the Cloudflare Queues binding! 🎉 Hungry for more Dapr bindings? Have a look at the long list of &lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/"&gt;supported bindings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Any questions or comments about this blog post or the code? Join the &lt;a href="https://aka.ms/dapr-discord"&gt;Dapr discord&lt;/a&gt; and post a message in the &lt;code&gt;#components-contrib&lt;/code&gt; channel. Have you made something with Cloudflare and Dapr? Post a message in the &lt;code&gt;#show-and-tell&lt;/code&gt; channel, we love to see your creations!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>cloud</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to upgrade your local Dapr runtime and CLI</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Wed, 22 Feb 2023 08:51:26 +0000</pubDate>
      <link>https://dev.to/marcduiker/how-to-upgrade-your-local-dapr-runtime-and-cli-4c02</link>
      <guid>https://dev.to/marcduiker/how-to-upgrade-your-local-dapr-runtime-and-cli-4c02</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;EDIT: updated for the Dapr 1.15 release :)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've recently upgraded my local Dapr runtime and the Dapr CLI due to the &lt;a href="https://blog.dapr.io/posts/2025/02/27/dapr-v1.15-is-now-available/" rel="noopener noreferrer"&gt;new 1.15 release being available&lt;/a&gt;. Since I'll be upgrading to new releases on a regular basis, and use both Mac and a Windows machines, I might as well help my future self and write this down since I forget these things after a week 😅.&lt;/p&gt;

&lt;p&gt;Upgrading the self-hosted environment (which I call the runtime) is described in detail in the &lt;a href="https://docs.dapr.io/operations/hosting/self-hosted/self-hosted-upgrade/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;, but here I'm writing a slimmed down version, useful if you're either running Windows with winget, or macOS with Homebrew.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Uninstall the old Dapr runtime:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dapr uninstall
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;If you also want to remove the Redis and Zipkin images run:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dapr uninstall &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install the new Dapr runtime:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dapr init
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Upgrade the Dapr CLI:&lt;/p&gt;

&lt;p&gt;Using Windows and winget:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;winget upgrade Dapr.CLI
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Using macOS and brew:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew upgrade dapr-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify that everything is upgraded:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dapr &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The response should list the new versions:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CLI version: 1.15.0
Runtime version: 1.15.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🚨 Bleeding edge 🚨
&lt;/h3&gt;

&lt;p&gt;If you need the very latest, (bleeding) edge bits of Dapr, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pull the runtime images from the &lt;a href="https://github.com/dapr/dapr/pkgs/container/dapr" rel="noopener noreferrer"&gt;Dapr GitHub container registry&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Download the Dapr CLI release candidates from the &lt;a href="https://github.com/dapr/cli/releases" rel="noopener noreferrer"&gt;Dapr CLI GitHub release page&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enjoy the latest Dapr release!&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqolqp3a6wulpbw5udgp9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqolqp3a6wulpbw5udgp9.gif" alt="Dappy" width="960" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Dapr 1.10 release highlights</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Tue, 21 Feb 2023 08:23:41 +0000</pubDate>
      <link>https://dev.to/diagrid/dapr-110-release-highlights-2429</link>
      <guid>https://dev.to/diagrid/dapr-110-release-highlights-2429</guid>
      <description>&lt;p&gt;The Dapr maintainers released a new version of &lt;a href="https://dapr.io/"&gt;Dapr, the distributed application runtime&lt;/a&gt; 🎉. This post highlights the major new features and changes in release 1.10, the first one in 2023.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflows
&lt;/h2&gt;

&lt;p&gt;My favorite new feature is the &lt;a href="https://v1-10.docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/"&gt;workflow engine and building block&lt;/a&gt;. This feature enables developers to orchestrate business logic for messaging and state management across various microservices. Workflows are suitable for multistep processes such as order processing, HR onboarding, and coordinating software updates across many devices because workflows are stateful and support long-running services. Workflow patterns such as &lt;em&gt;chaining&lt;/em&gt; and &lt;em&gt;fan-out/fan-in&lt;/em&gt; are written in code to execute tasks reliably. The built-in workflow engine is durable and resilient and can either be used via HTTP/gRPC, as well as the .NET SDK (other languages will follow).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--N7Uvai89--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sx7514gazytzhced1yhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--N7Uvai89--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sx7514gazytzhced1yhr.png" alt="Dapr Workflow" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read more about the Workflow building block in the &lt;a href="https://v1-10.docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/"&gt;Dapr docs&lt;/a&gt; or try out the &lt;a href="https://docs.dapr.io/getting-started/quickstarts/workflow-quickstart/"&gt;Workflow Quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pluggable Components SDKs
&lt;/h2&gt;

&lt;p&gt;Dapr provides a long list of &lt;a href="https://docs.dapr.io/concepts/components-concept/"&gt;built-in components&lt;/a&gt; to work with. But if this isn’t enough, Pluggable Components allows developers to create their own components for their specific integration requirements. Pluggable Components can be written in any language that supports the gRPC protocol.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--49ANZixh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nk6p2pftq9zyns1f05hr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--49ANZixh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nk6p2pftq9zyns1f05hr.png" alt="Pluggable Components" width="548" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With release 1.10, developers can create components using the Pluggable Components SDKs in .NET, Java, and Go instead of using the gRPC protocol directly. This will enable many developers to create new components, both public and private ones.&lt;/p&gt;

&lt;p&gt;Read more about Pluggable Components in the &lt;a href="https://docs.dapr.io/operations/components/pluggable-components/pluggable-components-overview/"&gt;Dapr docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running multiple Dapr apps
&lt;/h2&gt;

&lt;p&gt;Running a microservices system locally can be challenging, since multiple apps need to be up and running. Developers would need to call the &lt;code&gt;dapr run&lt;/code&gt; command for each microservice separately, which gets a bit tedious.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bQ28Ub3B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sxsvyznyk1amq7oh4wty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bQ28Ub3B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sxsvyznyk1amq7oh4wty.png" alt="Multi-app run" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With release 1.10, another big developer productivity improvement has been achieved. Developers can now run multiple apps with just one command and a multi-app template file.  The &lt;code&gt;dapr run -f &amp;lt;path&amp;gt;&lt;/code&gt;  command is used to run Dapr locally, where &lt;em&gt;&lt;/em&gt; points to a directory that contains a &lt;em&gt;dapr.yaml&lt;/em&gt; file, the multi-app template, that contains the properties to run the apps. More information on how to structure the multi-app template can be found in the &lt;a href="https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/multi-app-template/"&gt;Dapr docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components
&lt;/h2&gt;

&lt;p&gt;Dapr uses a modular design where functionality is delivered as a component.  Release 1.10 contains both new components as improvements to existing components.&lt;/p&gt;

&lt;h3&gt;
  
  
  New Components
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-sqlite/"&gt;SQLite&lt;/a&gt;: Enables state store functionality using &lt;a href="https://www.sqlite.org/index.html"&gt;SQL Lite 3&lt;/a&gt;. SQLite is a small, fast and self-contained SQL database engine, great for local development and running on small devices.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-cloudflare-workerskv/"&gt;Cloudflare KV&lt;/a&gt;: Enables state store functionality using &lt;a href="https://developers.cloudflare.com/workers/runtime-apis/kv"&gt;Cloudflare Workers KV&lt;/a&gt;. Workers KV is a global, low-latency, key-value data store that is ideal for high read volumes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/cloudflare-queues/"&gt;Cloudflare Queues binding&lt;/a&gt;: Enables publishing message to &lt;a href="https://developers.cloudflare.com/queues/"&gt;Cloudflare queues&lt;/a&gt;. Cloudflare queues allow developers to send and receive messages with guaranteed delivery. It integrates with Cloudflare Workers and offers at-least once delivery and message batching.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/kubemq/"&gt;KubeMQ binding&lt;/a&gt;: Supports input and output bindings for &lt;a href="https://kubemq.io/"&gt;KubeMQ&lt;/a&gt;, an enterprise-grade Kubernetes-based message broker and message queue that is scalable, highly available, and secure&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-kubemq/"&gt;KubeMQ&lt;/a&gt;: Enables pub/sub messaging using &lt;a href="https://kubemq.io/"&gt;KubeMQ&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-solace-amqp/"&gt;Solace AMQP&lt;/a&gt;: Enables pub/sub messaging using &lt;a href="https://solace.com/products/event-broker/software/"&gt;Solace PubSub+ Event Broker&lt;/a&gt;. The Solace Event Broker is a container-based message broker that enables real-time data distribution between applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Component Improvements
&lt;/h3&gt;

&lt;p&gt;With 1.10 many components have graduated to the &lt;em&gt;Stable&lt;/em&gt; certification level. All components have a certification level as mentioned in the &lt;a href="https://docs.dapr.io/operations/components/certification-lifecycle/"&gt;Certification Lifecycle&lt;/a&gt;. The &lt;em&gt;Stable&lt;/em&gt; level means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The component must have component &lt;a href="https://docs.dapr.io/operations/components/certification-lifecycle/#certification-tests"&gt;certification tests&lt;/a&gt; validating functionality and resiliency.&lt;/li&gt;
&lt;li&gt;The component is maintained by Dapr maintainers and supported by the community.&lt;/li&gt;
&lt;li&gt;The component is well documented and tested.&lt;/li&gt;
&lt;li&gt;The component has been available as Alpha or Beta for at least 1 minor version release of Dapr runtime prior.&lt;/li&gt;
&lt;li&gt;A maintainer will address component security, core functionality and test issues according to the Dapr support policy and issue a patch release that includes the patched stable component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following components have progressed to &lt;em&gt;Stable&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-aws-snssqs/"&gt;AWS SQS/SNS&lt;/a&gt;: Enables pub/sub messaging using AWS &lt;a href="https://aws.amazon.com/sqs/"&gt;SQS&lt;/a&gt; or &lt;a href="https://aws.amazon.com/sns/"&gt;SNS&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-dynamodb/"&gt;AWS DynamoDB&lt;/a&gt;: Enables state store functionality using &lt;a href="https://aws.amazon.com/dynamodb/"&gt;AWS DynamoDB&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-cockroachdb/"&gt;CockroachDB&lt;/a&gt;: Enables state store functionality using &lt;a href="https://www.cockroachlabs.com/product/"&gt;CockroachDB&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-mysql/"&gt;MySQL&lt;/a&gt;: Enables state store functionality using &lt;a href="https://www.mysql.com/"&gt;MySQL&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-secret-stores/hashicorp-vault/"&gt;HashiCorp Vault&lt;/a&gt;: Enables secret store functionality using &lt;a href="https://www.hashicorp.com/products/vault"&gt;HashiCorp Vault&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-pulsar/"&gt;Pulsar&lt;/a&gt;: Enables pub/sub messaging using &lt;a href="https://pulsar.apache.org/"&gt;Apache Pulsar&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/servicebusqueues/"&gt;Azure Service Bus Queues binding&lt;/a&gt;: supports input and output binding to &lt;a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-queues-topics-subscriptions"&gt;Azure Service Bus Queues&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/cron/"&gt;Cron binding&lt;/a&gt;: Enables scheduled task execution via &lt;a href="https://en.wikipedia.org/wiki/Cron"&gt;cron expressions&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re using any of these components and have a demo to show, drop a link in the &lt;em&gt;#show-and-tell&lt;/em&gt; channel on the &lt;a href="https://discord.com/invite/ptHhX6jc34"&gt;Dapr Discord&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is next?
&lt;/h2&gt;

&lt;p&gt;This post is not a complete list of features and changes released in version 1.10. Read the &lt;a href="https://blog.dapr.io/posts/2023/02/16/dapr-v1.10-is-now-available/"&gt;official Dapr release notes&lt;/a&gt; for more information. The release notes also contain information on how to upgrade to this latest version.&lt;/p&gt;

&lt;p&gt;Excited about these features and want to learn more? I’ll cover the new features in more detail in future posts. Until then, join the &lt;a href="https://discord.com/invite/ptHhX6jc34"&gt;Dapr Discord&lt;/a&gt; to connect with thousands of Dapr users.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cloudnative</category>
      <category>architecture</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Be ready for failure on stage: introducing the Speaker Buddy System</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Thu, 24 Nov 2022 14:45:38 +0000</pubDate>
      <link>https://dev.to/marcduiker/be-ready-for-failure-on-stage-introducing-the-speaker-buddy-system-f89</link>
      <guid>https://dev.to/marcduiker/be-ready-for-failure-on-stage-introducing-the-speaker-buddy-system-f89</guid>
      <description>&lt;h2&gt;
  
  
  Conferences
&lt;/h2&gt;

&lt;p&gt;I love ❤️ going to in-person conferences, especially when I'm invited to speak there. Although I've been speaking at dozens of conferences and meetups over the past years, I still have impostor syndrome when it comes to speaking. Some people really seem to have a gift for public speaking, and sometimes I feel way out of my league. I do realize those experienced speakers have been practicing for years, and perhaps I'll get there as well, practice makes &lt;del&gt;perfect&lt;/del&gt; improvement! 💪&lt;/p&gt;

&lt;h2&gt;
  
  
  Failing on stage
&lt;/h2&gt;

&lt;p&gt;One thing that can throw me off during my speaking sessions is technical issues. And I don't mean failing demos (I'm used to those 🫠), but audio/video issues that require minutes (feels like hours 😭) to solve.&lt;/p&gt;

&lt;p&gt;A couple of weeks ago, I had such an issue where my laptop didn't mirror the screen to the AV system. I was using a MacBook that I had used a dozen times before, but I'm not very familiar with macOS, and even when I got some help (and tried multiple adapters), the issue wasn't solved. It was a few minutes past the official start time of the talk, and I was now getting nervous (and I'm quite a chill person otherwise). Luckily the speaker before me (&lt;a href="https://github.com/devlead"&gt;Mattias Karlsson&lt;/a&gt;) had a brilliant idea: he asked me to install TeamViewer on my laptop, connect to the TeamViewer session on his laptop, and he would connect his machine to the AV system. This worked like a charm (WiFi was reasonable), and I was able to start my talk about 5 minutes late. 🎉 The stress I built up during those minutes was not beneficial for my delivery though. My breathing was shallow, and my pace was too fast.&lt;/p&gt;

&lt;p&gt;Could I have prevented the technical issue? No, but I could have been better prepared.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TLDR: Have a look at the &lt;a href="https://github.com/marcduiker/speaker-buddy"&gt;Speaker Buddy System&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Preparing for failure
&lt;/h2&gt;

&lt;p&gt;There are several ways of preparing for technical issues. I'll share some of my ideas, and close with an idea initially proposed by Mattias, called the Speaker Buddy System.&lt;/p&gt;

&lt;p&gt;I won't cover 'my demo doesn't work' failures here, since these things can be solved in different ways, such as; showing a solution that has been prepared beforehand, or recording the solution before the conference and playing it back during the session.&lt;/p&gt;

&lt;p&gt;The general idea to overcome a catastrophic (laptop) failure is to ensure that your presentation, code, and demos are available on other environments you can quickly access.&lt;/p&gt;

&lt;p&gt;Let's go through some solutions:&lt;/p&gt;

&lt;h3&gt;
  
  
  Bring a second laptop
&lt;/h3&gt;

&lt;p&gt;Yes, it can be that 'easy'. But one, you need to &lt;em&gt;have&lt;/em&gt; a second laptop (💸), and two, you need to take it with you when traveling, which isn't always an option. I have a small Windows Surface Go that I usually bring with me next to my main laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud storage
&lt;/h3&gt;

&lt;p&gt;For things that don't require showing or running code, you can use a cloud storage solution like OneDrive, Google Drive, Dropbox, or even a git repository. I use OneDrive to store and access my presentations. If your laptop fails, you still need to use another machine, but you can access your presentation relatively quickly (if you have access to the internet).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud-hosted coding environments
&lt;/h3&gt;

&lt;p&gt;When you &lt;em&gt;do&lt;/em&gt; need to show, or run, code in your session, make sure you can run it in the cloud instead of your own machine! I have all my source code on GitHub, and I'm using &lt;a href="https://github.com/features/codespaces"&gt;GitHub Codespaces&lt;/a&gt; so I can run a demo completely in the cloud if necessary. There are many similar solutions out there such as &lt;a href="https://www.gitpod.io/"&gt;Gitpod&lt;/a&gt;, &lt;a href="https://www.jetbrains.com/space/"&gt;JetBrains Space&lt;/a&gt;, &lt;a href="https://codesandbox.io/"&gt;CodeSandbox&lt;/a&gt;.&lt;br&gt;
The downside of this solution is that it does require a good internet connection.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt;: If your source code is on GitHub, and you just need to show the source code, you can use &lt;a href="https://github.com/github/dev"&gt;github.dev&lt;/a&gt;, a lightweight, browser-based editor (based on VSCode) you can access by replacing &lt;code&gt;.com&lt;/code&gt; with &lt;code&gt;.dev&lt;/code&gt; when typing the repository URL. It even supports some extensions such as the brilliant &lt;a href="https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour"&gt;CodeTour&lt;/a&gt;, which is great for showing code in a guided way throughout your session.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Speaker Buddy System
&lt;/h2&gt;

&lt;p&gt;This is the idea that Mattias and I talked about after my failing laptop experience, and I love it! We named it &lt;strong&gt;Speaker Buddy&lt;/strong&gt; (not to be mistaken with &lt;a href="https://www.conferencebuddy.io/"&gt;Conference Buddy&lt;/a&gt;), but I'm open to better names. In the solutions mentioned above, you still need access to another laptop (and the internet), and the Speaker Buddy system solves this!&lt;/p&gt;

&lt;h3&gt;
  
  
  1. For conference speakers
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1.1. For speakers who want to volunteer as Speaker Buddy
&lt;/h4&gt;

&lt;p&gt;You can be a Speaker Buddy if you enjoy helping others to be successful during their talks.&lt;/p&gt;

&lt;p&gt;You can help out by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Being kind and respectful (e.g. following the Code of Conduct of the conference).&lt;/li&gt;
&lt;li&gt;Announcing that you're available to help out as a Speaker Buddy (preferably do this via the conference organizers and their communication channels).&lt;/li&gt;
&lt;li&gt;Agreeing up front how you can help (see 1.2).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  1.2. For speakers who want a Speaker Buddy
&lt;/h4&gt;

&lt;p&gt;If you are going to speak at a conference and you want another speaker to have your back in case you run into technical issues or just want a friendly and supportive face on the front row, then this is for you!&lt;/p&gt;

&lt;p&gt;Once the conference agenda is known and the Speaker Buddies are confirmed by the conference organizers, contact one of the Speaker Buddies (who does not speak at the same time as you!) to ask if they are willing to help you out in case of technical issues/laptop failure, or just being there as a friendly face in the audience. If they are, you can discuss the details such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supporting you by sitting in the front row (smiling and nodding).&lt;/li&gt;
&lt;li&gt;Have a copy of your presentation on their machine.&lt;/li&gt;
&lt;li&gt;Cloning your code repository, installing SDKs and tools on their machine, if you want to show/run code locally.&lt;/li&gt;
&lt;li&gt;Doing a (remote) dry run via their laptop to be completely prepared for a technical failure. (You also test your database backups right?)&lt;/li&gt;
&lt;li&gt;Is there any other way your buddy can help you out? Can they tell a funny story during the technical issue, ask a relevant question, help out in case you get difficult questions, or do something else to put you at ease?&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Even with the existence of cloud-based coding environments, I still suggest having a local environment available in case the internet connection is not good enough.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. For conference organizers
&lt;/h3&gt;

&lt;p&gt;I know organizers have a lot to worry about when organizing a conference, and most of the conferences I attended did a good job regarding the AV setup. I prefer when there are hired AV professionals to help out, but in case they are not, please ensure there is at least someone from the organization available in the room to assist when technical issues arise.&lt;/p&gt;

&lt;p&gt;Additionally, you can offer your speakers some peace of mind when they know someone has their back in case of technical difficulties (e.g. laptop failure).&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. CfP preparation
&lt;/h3&gt;

&lt;p&gt;When preparing the Call for Papers (CfP), add a field to whatever system you're using to have the speaker indicate if they are willing to be a Speaker Buddy. Include a link to &lt;a href="https://github.com/marcduiker/speaker-buddy"&gt;this repo&lt;/a&gt; so they can read more about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2. Speaker onboarding/confirmation
&lt;/h3&gt;

&lt;p&gt;When onboarding/confirming speakers to your conference, mention the Speaker Buddy System and provide the list of speakers who have volunteered to be a Speaker Buddy.&lt;/p&gt;

&lt;p&gt;Feel free to use this template in your communication:&lt;/p&gt;




&lt;p&gt;Hi {speaker},&lt;/p&gt;

&lt;p&gt;We highly encourage you to look for a Speaker Buddy before attending our conference.&lt;/p&gt;

&lt;p&gt;A Speaker Buddy is another speaker that can help you out in case of technical issues (e.g. laptop failure).&lt;/p&gt;

&lt;p&gt;These speakers are volunteering as Speaker Buddies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;{list of speakers}&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our conference agenda is published at {conference agenda url}, so look for a Speaker Buddy that is available during your session. &lt;/p&gt;

&lt;p&gt;It's best to prepare well in advance. For more information on how to be prepared, read the tips at &lt;a href="https://github.com/marcduiker/speaker-buddy"&gt;https://github.com/marcduiker/speaker-buddy&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Preparing the event location
&lt;/h3&gt;

&lt;p&gt;When you prepare the conference rooms, label a front-row seat for the Speaker Buddy. This way, they can be supportive and help out the speaker quickly in case of technical issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's use it!
&lt;/h2&gt;

&lt;p&gt;I've put together a &lt;a href="https://github.com/marcduiker/speaker-buddy"&gt;repository&lt;/a&gt; that contains most of the info in this blog post. I highly encourage conference organizers to refer to this repo in their communication to inform speakers about the Speaker Buddy System.&lt;/p&gt;

&lt;p&gt;If you see an opportunity for improvement, please submit an issue or pull request! ❤️&lt;/p&gt;

</description>
      <category>community</category>
      <category>devrel</category>
      <category>speaking</category>
    </item>
    <item>
      <title>One year at Ably as a Developer Advocate</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Fri, 11 Nov 2022 09:03:18 +0000</pubDate>
      <link>https://dev.to/marcduiker/one-year-at-ably-as-a-developer-advocate-3fa2</link>
      <guid>https://dev.to/marcduiker/one-year-at-ably-as-a-developer-advocate-3fa2</guid>
      <description>&lt;p&gt;It has been one year since I've joined the DevRel team at Ably as Sr Developer Advocate. In this post I'll highlight some things I've been working on and what I've learned in the past year. I'll cover content creation, developer tooling, events, roadmap &amp;amp; predictability, communication &amp;amp; relationships, and finally 'anything that could be improved'.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content creation
&lt;/h2&gt;

&lt;p&gt;Before I joined Ably, I mostly worked on event-driven architectures using .NET-based backends and Azure services (Azure Functions, CosmosDB, Service Bus). Ably really shines when real-time communication is needed between clients, or between servers and clients, so this meant I had to get up to speed with client-side programming, and WebSocket-based communication using the Ably SDK.&lt;/p&gt;

&lt;p&gt;During my onboarding, I created my first demo, &lt;a href="https://agileflush.ably.dev/"&gt;Agile Flush&lt;/a&gt;, a small and playful web app to do remote planning poker. This was my first introduction to VueJS and the Ably client SDK, and after a while I got to like working with Vue. It did take me some time to get used to the toolchain though.&lt;/p&gt;

&lt;p&gt;I've stuck to using Vue (TypeScript based, with Vite for the dev tooling) for most of my demos where I need a front-end. It pairs nicely with Azure Static Web Apps that I use for hosting the demos.&lt;/p&gt;

&lt;p&gt;Here's a list with all the demos I've created so far, including related content pieces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agile Flush
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gWxGqx8_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/no8nqh43ighu3iqllurg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gWxGqx8_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/no8nqh43ighu3iqllurg.png" alt="Agile Flush" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A playful web app for remote poker planning. The clients publish their votes and actions to Ably, and Ably pushes the updates to the subscribed clients.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ably.com/blog/tutorial-vuejs-nodejs-azure-static-web-apps"&gt;Blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=59BZCQuRRkM"&gt;Video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://agileflush.ably.dev/"&gt;Live demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ably-labs/agile-flush-vue-app"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/59BZCQuRRkM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  What is pub/sub and how to apply it in C# .NET to build a chat app
&lt;/h3&gt;

&lt;p&gt;The demo is a .NET 6 console application that can publish or subscribe to messages which get delivered through Ably. The blog post also covers the basics what pub/sub is, the typical use cases, and benefits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ably.com/blog/use-pub-sub-to-build-a-chat-app-with-csharp-net"&gt;Blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ably-labs/pubsub-demo-dotnet"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Serverless WebSockets Quest
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6Tb0TTum--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/coqd2345o34oi0qdziro.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6Tb0TTum--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/coqd2345o34oi0qdziro.gif" alt="Image description" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A turn-based mini game with with real-time aspects build on Azure Functions, Durable Functions, and Ably.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ably.com/blog/quest-for-serverless-websockets-azure-functions-adventure"&gt;Blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=KHzdc3USFU4"&gt;Video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://quest.ably.dev/"&gt;Live demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ably-labs/serverless-websockets-quest"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/KHzdc3USFU4"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Collaborative Pixelart Drawing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ekfsTTod--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/al506b8f79eh4doo7boy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ekfsTTod--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/al506b8f79eh4doo7boy.gif" alt="Collaborative pixelart" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A collaborative pixelart drawing canvas. Multiple users draw on a canvas, and their movement and actions are shared with the other connected users.&lt;/p&gt;

&lt;p&gt;I created two versions of this demo, one using Ably, and one using Azure Web PubSub, and did a write-up about difference in developer experience between the two services.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ably.com/blog/cloud-pubsub-services-compared-azure-web-pubsub-ably"&gt;Blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=sPgHwm3-yiM"&gt;Video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pixel-paint.ably.dev/"&gt;Live demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/sPgHwm3-yiM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Serverless Pizza Workflow Visualizer
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HGTEf9tC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ynszpdhr1eq6fkqomxbx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HGTEf9tC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ynszpdhr1eq6fkqomxbx.gif" alt="Image description" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A demo that visualizes the real-time progress of a serverless workflow that is built with Durable Functions. I really had a lot of fun creating the pizza-themed visuals for this demo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ably.com/blog/visualize-azure-serverless-workflow-progress-in-realtime-with-pubsub"&gt;Blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=y9-a_ewgWCQ"&gt;Video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pizza.ably.dev/"&gt;Live demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ably-labs/serverless-workflow-visualizer"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/y9-a_ewgWCQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer tooling
&lt;/h2&gt;

&lt;p&gt;I &lt;strong&gt;really&lt;/strong&gt; like improving the developer experience from a tooling perspective. Ably has a Control API, a RESTful interface that allows management of the Ably apps. I've used this API in several tools to speed up the creation of apps and keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ably Control API GitHub Action
&lt;/h3&gt;

&lt;p&gt;Since most of my demos require a new Ably App, I created a GitHub action that creates an app from a GitHub workflow. The action also enables the the creation of an API key with a configurable set of capabilities. This was the first time I've created a GitHub action, which was a great learning experience for me. The action has a limited feature set, but I intend to add more features in the future.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CjYLR61S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f5y18dlqtn4d3dh09ise.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CjYLR61S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f5y18dlqtn4d3dh09ise.png" alt="Control API GitHub Action" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ably.com/blog/infrastructure-as-code-ably-control-api-github-action"&gt;Blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=b7GE39JaM3M"&gt;Video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marketplace/actions/ably-control-api"&gt;Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ably-labs/ably-control-api-action"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ably VSCode extension
&lt;/h3&gt;

&lt;p&gt;During the first Ably Innovation Days, I proposed to create a VSCode extension that allows developers to manage their Ably apps directly from VSCode. I formed a team with several colleagues, and in two days we had a working prototype that lists all the Ably apps in the activity bar, and creates a new Ably app via the command palette. We won the &lt;em&gt;Ship It 🚢&lt;/em&gt; award with this prototype, which allowed me to continue working on the extension and release it to the VSCode extension marketplace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DDbF2mo1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ffpjjepl8kzfgzxb25u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DDbF2mo1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ffpjjepl8kzfgzxb25u.gif" alt="VSCode extension" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ably.com/blog/announcing-the-ably-vs-code-extension"&gt;Blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=3CPHb_kn1-o"&gt;Video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ably-labs.vscode-ably"&gt;Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ably-labs/vscode-ably"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/3CPHb_kn1-o"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Ably CLI
&lt;/h3&gt;

&lt;p&gt;During the second Ably Innovation Days, I started working on specifications for an Ably CLI. After the first day &lt;a href="https://twitter.com/leggetter"&gt;Phil&lt;/a&gt; and I started with a prototype based on &lt;a href="https://oclif.io/"&gt;oclif&lt;/a&gt;. We managed to create a working prototype in a day that lists Ably apps, and creates a new Ably app. This project is still Work In Progress. Once the CLI is in a releasable state, I'll create some content around this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RrirhO6C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ex35uqkg6bt40i4xi881.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RrirhO6C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ex35uqkg6bt40i4xi881.png" alt="Ably CLI" width="800" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ably-labs/ably-cli"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Events
&lt;/h2&gt;

&lt;p&gt;I had the opportunity to speak at several conferences and meetups this year, both online and in-person. It was great to attend in-person events again, networking with other people, seeing people respond to your talk, and people asking questions afterwards. I'm still a proponent of online (or hybrid) events as well though. Online events can be more accessible, and allow for a more diverse audience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6OaVS7E0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sbozqkliz7acykc57da9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6OaVS7E0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sbozqkliz7acykc57da9.jpg" alt="dotnetdays Romania" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Events I spoke at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://azurelowlands.com/"&gt;Azure Lowlands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ndcoslo.com/"&gt;NDC Oslo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dotnetdays.ro/"&gt;dotnetdays Romania&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=LpzJTJvH6go&amp;amp;t=3453s"&gt;CosmosDB Conference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/Y0YTtgn5KKo"&gt;ServerlessDays NYC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=FGklbFQrd44"&gt;Microsoft Reactor Toronto&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dotnetoxford.com/posts/2022-02-lightning-talks"&gt;Oxford .NET User Group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=R87-35-Aiw8"&gt;Welsh Azure User Group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sharepointeurope.com/webinars/start-building-serverless-applications-on-azure/"&gt;ESPC Community Webinar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Roadmap &amp;amp; Predictability
&lt;/h2&gt;

&lt;p&gt;Part of my role involves creating and updating a roadmap for the .NET &amp;amp; Azure community. This document refers to the company strategy, and how the DevRel team activities fit in that strategy. The roadmap contains the topics which I plan to create content for, estimations on their outcomes, and a timeline. The metrics &amp;amp; predictability of the outcomes is really one of the most difficult parts in DevRel. One blog post can go viral while the next one hardly gets any engagement. It's important to try understand why something works well or why it doesn't.&lt;/p&gt;

&lt;p&gt;What I've learned over the last year is that content creation is just a small part of the bigger picture when working in DevRel. Content distribution is just as important. If I write a great blog post but hardly anyone will read it, then writing it has been a waste of my time. I'm spending more time these days on the distribution part, to make sure the content gets the proper exposure across various channels. It's not a part I particularly like, but it needs to be done, and I do appreciate the new insights it gives me.&lt;/p&gt;

&lt;p&gt;Reflecting on the content, the engagement, and analyzing the performance (visits, session duration, sign-ups etc) is even more important. If a content piece is not resulting in (enough) sign-ups, then we investigate why, change the content, or use a different approach for future pieces. We're measuring several aspects for a couple of months now and it still feels like we're at the beginning of understanding the numbers. It's certainly an area that interests me, and where I want to improve the predictability of my work. Being in DevRel means that you're constantly learning, measuring, and tweaking. And that's what I love about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication &amp;amp; Relationships
&lt;/h2&gt;

&lt;p&gt;So far, it looks like this is all a one-person-show, but it's far from that.&lt;/p&gt;

&lt;p&gt;I depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My DevRel and content team colleagues, who help me review the content I create.&lt;/li&gt;
&lt;li&gt;Our community manager, who informs me of CfPs, sponsoring opportunities, and new questions on Discord and social media.&lt;/li&gt;
&lt;li&gt;Our content marketing team, to provide me with insights on keyword research and content performance.&lt;/li&gt;
&lt;li&gt;Our product team, to keep me up to date with the latest product features and changes.&lt;/li&gt;
&lt;li&gt;My managers, to provide me with valuable feedback about my work, so I can keep improving.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;em&gt;relations&lt;/em&gt; part of DevRel is really crucial. In order to have a good relation with your developer audience, you also need to have a good relation with your team members, Marketing, Product, Documentation, Engineering, and Customer Support. Establishing and growing these relations take time. New people join the company and others leave, it's an ever changing environment. It can be tiring and challenging at times, but once the relationship is there, it feels good to work together to achieve a common goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anything that could be improved?
&lt;/h2&gt;

&lt;p&gt;Always! 😅 I noticed that I spent less time on my open source projects than before I joined Ably. It's probably because I now create content for a living, I don't always feel like doing that in my spare time as well. I still have plenty of plans to continue working on these projects though, it just needs a bit more planning and prioritization.&lt;/p&gt;

&lt;p&gt;One of the areas I really want to expand on is collaborations with other people. These could be collaborations with other DevRel folks, or individual developers who share a common interest around serverless, real-time communication, pixelart, or something completely out of the box! If you think we should combine forces to work on a fun project together, please &lt;a href="https://twitter.com/marcduiker"&gt;reach out&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>devrel</category>
      <category>career</category>
    </item>
    <item>
      <title>Create realtime dashboards to show serverless workflow progress</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Tue, 18 Oct 2022 08:05:12 +0000</pubDate>
      <link>https://dev.to/ably/create-realtime-dashboards-to-show-serverless-workflow-progress-20ee</link>
      <guid>https://dev.to/ably/create-realtime-dashboards-to-show-serverless-workflow-progress-20ee</guid>
      <description>&lt;p&gt;In this post I'll explain how to use a cloud pubsub service such as Ably to visualize the progress of a serverless workflow in realtime.&lt;/p&gt;

&lt;p&gt;You'll learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to build serverless workflows with Azure Functions &amp;amp; Durable Functions.&lt;/li&gt;
&lt;li&gt;How to publish messages from activity functions in the back-end.&lt;/li&gt;
&lt;li&gt;How to subscribe to these messages in the front-end.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Go to the &lt;a href="https://pizza.ably.dev/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; or view the source on &lt;a href="https://github.com/ably-labs/serverless-workflow-visualizer" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/y9-a_ewgWCQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Organizations that have their business processes automated often need to show the progress of these processes to their users - either via internal dashboards, or via external client portals. Common use cases include applying for a reimbursement at your health insurance provider, or making an online purchase.&lt;/p&gt;

&lt;p&gt;Over the years, automation has resulted in the decoupling of front-ends and back-ends. Although decoupling is great for improving resiliency and effective scaling, it does make it more challenging to inform the front-end of status updates that occur in the back-end, especially with asynchronous communication, and serverless back-ends that are not always running.&lt;/p&gt;

&lt;p&gt;A suitable way to update a front-end from back-end processes is to use pubsub over WebSockets. In this post I'll show how to use Ably, a cloud based pubsub service, to visualize the progress of a serverless workflow implemented with Azure Functions and Durable Functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzn4ji9j1k2v9cz9t22f4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzn4ji9j1k2v9cz9t22f4.png" alt="High-level component view of the solution."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project uses the following components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/azure-functions/functions-overview" rel="noopener noreferrer"&gt;Azure Functions&lt;/a&gt;, the serverless compute service in Azure.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/azure-functions/durable/" rel="noopener noreferrer"&gt;Durable Functions&lt;/a&gt;, an extension for Azure Functions that allows writing workflows as code and enables stateful functions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vuejs.org/" rel="noopener noreferrer"&gt;Vue3&lt;/a&gt;, a popular front-end framework.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/static-web-apps/overview" rel="noopener noreferrer"&gt;Azure Static Web Apps&lt;/a&gt;, a hosting solution in the Azure cloud.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ably.com?utm_campaign=GLB-2210-serverless-workflow-visualizer&amp;amp;utm_content=blog-serverless-workflow-visualizer&amp;amp;utm_source=dev-to&amp;amp;utm_medium=blog&amp;amp;src=GLB-2210-serverless-workflow-visualizer-dev-to" rel="noopener noreferrer"&gt;Ably&lt;/a&gt;, a serverless pubsub service for realtime messaging at scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This diagram show the various functions and their interactions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9wbenyoafc3bs777h1c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9wbenyoafc3bs777h1c.png" alt="The Auth and PizzaWorkflow Apps showing the application flow."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The serverless workflow
&lt;/h2&gt;

&lt;p&gt;The serverless workflow is implemented with Durable Functions. This is an extension for Azure Functions that allows workflows to be written as code. It enables stateful orchestrator functions because an Azure storage account is used behind the scenes. The interactions with the storage account, the queues, and table storage, are abstracted away. The Durable Functions API is used when writing an orchestrator function that chains several activity functions in a sequence. The code below shows the &lt;code&gt;PizzaWorkflowOrchestrator&lt;/code&gt; function that is chaining six activity functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PizzaWorkflowOrchestrator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PizzaWorkflowOrchestrator&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OrchestrationTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;IDurableOrchestrationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetInput&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Instructions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReceiveOrder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SendInstructionsToKitchen&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;preparationTasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;instruction&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MenuItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MenuItemType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;preparationTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PreparePizza&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preparationTasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CollectOrder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeliverOrder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeliveredOrder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/api/PizzaWorkflow/Orchestrators/PizzaWorkflowOrchestrator.cs.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The responsibility of the orchestrator function is to initiate the execution of the activity functions in the right order. The orchestrator function will replay from the start several times and therefore the function must be deterministic to avoid side effects. Any non-deterministic behavior (e.g. communication with external APIs) should be put in activity functions.&lt;/p&gt;

&lt;p&gt;In this demo, the activity functions are merely placeholders and don't do anything meaningful. The functions only log information statements, simulate a randomized wait time, and publish the progress to Ably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing messages from the back-end
&lt;/h2&gt;

&lt;p&gt;Since each of the activity functions require their progress to be published, I created an abstract &lt;code&gt;MessagingBase&lt;/code&gt; class to wrap the publishing behavior for easy re-use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessagingBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IRestClient&lt;/span&gt; &lt;span class="n"&gt;_ablyClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;MessagingBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IRestClient&lt;/span&gt; &lt;span class="n"&gt;ablyClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_ablyClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ablyClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channelName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ABLY_CHANNEL_PREFIX"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ablyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/api/PizzaWorkflow/Activities/MessagingBase.cs.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The activity functions inherit from this abstract class and call the &lt;code&gt;PublishAsync&lt;/code&gt; method, as can be seen in the &lt;code&gt;CollectOrder&lt;/code&gt; activity function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CollectOrder&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MessagingBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CollectOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IRestClient&lt;/span&gt; &lt;span class="n"&gt;ablyClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ablyClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CollectOrder&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ActivityTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Collect menu items for order &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"collect-order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/api/PizzaWorkflow/Activities/CollectOrder.cs.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;WorkflowState&lt;/code&gt; object is used as the payload when publishing progress updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WorkflowState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;OrderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;MessageSentTimeStampUTC&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUnixTimeMilliseconds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"messageSentTimeStampUTC"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;MessageSentTimeStampUTC&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/api/PizzaWorkflow/Models/WorkflowState.cs.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the &lt;code&gt;WorkflowState&lt;/code&gt; doesn't contain information about the exact state of the workflow. This is because named events are used, and these names correspond with the activity function name. The front-end subscribes to these named events; therefore, no additional workflow information is required in the &lt;code&gt;WorkflowState&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;MessageBase&lt;/code&gt; class requires an &lt;code&gt;IRestClient&lt;/code&gt; object. This is the &lt;a href="https://ably.com/docs/rest?utm_campaign=GLB-2210-serverless-workflow-visualizer&amp;amp;utm_content=blog-serverless-workflow-visualizer&amp;amp;utm_source=dev-to&amp;amp;utm_medium=blog&amp;amp;src=GLB-2210-serverless-workflow-visualizer-dev-to" rel="noopener noreferrer"&gt;Ably REST client&lt;/a&gt;, and is configured in the &lt;code&gt;StartUp&lt;/code&gt; class, and injected in each of the activity functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;FunctionsStartup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StartUp&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;PizzaWorkflow&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StartUp&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FunctionsStartup&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IFunctionsHostBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IRestClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AblyRest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ABLY_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/api/PizzaWorkflow/StartUp.cs.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ABLY_API_KEY&lt;/code&gt; environment variable contains an Ably API key with &lt;code&gt;publish&lt;/code&gt; capabilities.&lt;/p&gt;

&lt;p&gt;Now that we've seen how the serverless workflow publishes the progress updates for each activity, let's have a look at the front-end where the messages are received.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receiving messages in the front-end
&lt;/h2&gt;

&lt;p&gt;The Vue front-end uses Pinia as the local state store. The store contains &lt;code&gt;WorkflowState&lt;/code&gt; definitions for each of the six workflow states, which are enabled - one by one - as the workflow progresses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PizzaWorkflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;RealtimeState&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isWorkflowComplete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;disableOrdering&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;orderReceivedState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;kitchenInstructionsState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;preparationState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;collectionState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;deliveryState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;deliveredState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isOrderPlaced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/src/types/PizzaWorkflow.ts.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WorkflowState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isDisabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isCurrentState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;messageSentTimeStampUTC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;messageReceivedTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;messageDeliveredTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/src/types/WorkflowState.ts.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;WorkflowState&lt;/code&gt; contains the following timestamps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;messageSentTimeStampUTC&lt;/code&gt;, the time set when the WorkflowState object was instantiated in the .NET back-end.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;messageReceivedTimestamp&lt;/code&gt;, the time that the message was received in the Ably platform.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;messageDeliveredTimestamp&lt;/code&gt;, the time set in the front-end when the message was received.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These three times should all be very close to each other. However some irregularities will be visible since these times are set in different machines, across various networks, meaning the clocks are not synced. Therefore it is possible that the &lt;code&gt;messageReceivedTimestamp&lt;/code&gt; is slightly earlier than the &lt;code&gt;messageSentTimeStampUTC&lt;/code&gt; etc. So, the times are not 100% accurate, but they are close enough to give a good indication of the speed of message delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instantiating the Ably realtime client
&lt;/h3&gt;

&lt;p&gt;A WebSocket-based connection is set up using the &lt;a href="https://ably.com/docs/realtime?utm_campaign=GLB-2210-serverless-workflow-visualizer&amp;amp;utm_content=blog-serverless-workflow-visualizer&amp;amp;utm_source=dev-to&amp;amp;utm_medium=blog&amp;amp;src=GLB-2210-serverless-workflow-visualizer-dev-to" rel="noopener noreferrer"&gt;Ably realtime client&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createRealtimeConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtimeClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Realtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/api/CreateTokenRequest/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;echoMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConnectionStateChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachToChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOrderPlaced&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
              &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOrderPlaced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disconnected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachToChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/src/stores/index.ts.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A call is made to the &lt;code&gt;CreateTokenRequest&lt;/code&gt; endpoint (hosted as a serverless function) which returns an authentication URL, including a token that is used to initiate a secure connection with Ably.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateTokenRequest&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"CreateTokenRequest/{clientId?}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tokenParams&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TokenParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ClientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tokenData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_ablyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequestTokenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenParams&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/api/Auth/CreateTokenRequest.cs.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Subscribing to named events
&lt;/h3&gt;

&lt;p&gt;Once a connection is established and the Ably channel is retrieved, the front-end is subscribing to the named events that correspond with the activity functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;subscribeToMessages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelInstance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;receive-order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleOrderReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelInstance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send-instructions-to-kitchen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleSendInstructions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelInstance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prepare-pizza&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handlePreparePizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelInstance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;collect-order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleCollectOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelInstance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deliver-order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleDeliverOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelInstance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivered-order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleDeliveredOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/src/stores/index.ts.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of the subscribe methods calls a handler function that updates the specific &lt;code&gt;WorkflowState&lt;/code&gt; in the Pinia store, see for example, the &lt;code&gt;handleOrderReceived&lt;/code&gt; handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;handleOrderReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$patch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;orderReceivedState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;messageSentTimeStampUTC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageSentTimeStampUTC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;messageReceivedTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;messageDeliveredTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="na"&gt;isDisabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;isCurrentState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;kitchenInstructionsState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="c1"&gt;/// For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/src/stores/index.ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating the UI
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;PizzaProcess.vue&lt;/code&gt; component contains the sequence of the six states in the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ProgressItem&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"animate"&lt;/span&gt;
    &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"(orderReceivedState as WorkflowState).isVisible"&lt;/span&gt;
    &lt;span class="na"&gt;:state=&lt;/span&gt;&lt;span class="s"&gt;"(orderReceivedState as WorkflowState)"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ProgressItem&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"animate"&lt;/span&gt;
    &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"(kitchenInstructionsState as WorkflowState).isVisible"&lt;/span&gt;
    &lt;span class="na"&gt;:state=&lt;/span&gt;&lt;span class="s"&gt;"(kitchenInstructionsState as WorkflowState)"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ProgressItem&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"animate"&lt;/span&gt;
    &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"(preparationState as WorkflowState).isVisible"&lt;/span&gt;
    &lt;span class="na"&gt;:state=&lt;/span&gt;&lt;span class="s"&gt;"(preparationState as WorkflowState)"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ProgressItem&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"animate"&lt;/span&gt;
    &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"(collectionState as WorkflowState).isVisible"&lt;/span&gt;
    &lt;span class="na"&gt;:state=&lt;/span&gt;&lt;span class="s"&gt;"(collectionState as WorkflowState)"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ProgressItem&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"animate"&lt;/span&gt;
    &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"(deliveryState as WorkflowState).isVisible"&lt;/span&gt;
    &lt;span class="na"&gt;:state=&lt;/span&gt;&lt;span class="s"&gt;"(deliveryState as WorkflowState)"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ProgressItem&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"animate"&lt;/span&gt;
    &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"(deliveredState as WorkflowState).isVisible"&lt;/span&gt;
    &lt;span class="na"&gt;:state=&lt;/span&gt;&lt;span class="s"&gt;"(deliveredState as WorkflowState)"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/src/components/PizzaProcess.vue. --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ProgressItem.vue&lt;/code&gt; component contains the definition of a single workflow state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"green-dot"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
        &lt;span class="na"&gt;v-bind:class=&lt;/span&gt;&lt;span class="s"&gt;"{
          disabled: props.state.isDisabled,
          transition: true,
        }"&lt;/span&gt;
        &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;"GreenDot"&lt;/span&gt;
        &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"32"&lt;/span&gt;
      &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"details"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
        &lt;span class="na"&gt;v-bind:alt=&lt;/span&gt;&lt;span class="s"&gt;"props.state.title"&lt;/span&gt;
        &lt;span class="na"&gt;v-bind:title=&lt;/span&gt;&lt;span class="s"&gt;"getImgTitle(props.state)"&lt;/span&gt;
        &lt;span class="na"&gt;v-bind:class=&lt;/span&gt;&lt;span class="s"&gt;"{
          disabled: props.state.isDisabled,
          transition: true,
        }"&lt;/span&gt;
        &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;"props.state.image"&lt;/span&gt;
      &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-bind:class=&lt;/span&gt;&lt;span class="s"&gt;"{ disabled: props.state.isDisabled }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        {{
          props.state.isDisabled
            ? "Waiting for your order..."
            : `${convertToTimeSeconds(
                props.state.messageReceivedTimestamp
              )} - ${props.state.title} (${props.state.orderId.split("-")[1]})`
        }}
      &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- For the full implementation see https://github.com/ably-labs/serverless-workflow-visualizer/blob/main/src/components/ProgressItem.vue.--&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running locally
&lt;/h2&gt;

&lt;p&gt;The following dependencies are required to run the solution locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dotnet.microsoft.com/download/dotnet/6.0" rel="noopener noreferrer"&gt;.NET 6 SDK&lt;/a&gt;. The .NET SDK required for the C# Azure Functions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en/" rel="noopener noreferrer"&gt;Node 16&lt;/a&gt;. The JavaScript runtime required for the Vue front-end and installing the Static Web Apps CLI.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/azure-functions/functions-run-local?tabs=v4%2Cwindows%2Ccsharp%2Cportal%2Cbash" rel="noopener noreferrer"&gt;Azure Functions Core Tools&lt;/a&gt;. This is part of the Azure Functions extensions for VSCode that should be recommended for automatic installation when this repo is opened in VSCode.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://marketplace.visualstudio.com/items?itemName=Azurite.azurite" rel="noopener noreferrer"&gt;Azurite&lt;/a&gt;. This is a local storage emulator that is required for Durable Functions. When this repo is opened in VSCode, a message will appear to install this extension.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Azure/static-web-apps-cli" rel="noopener noreferrer"&gt;Azure Static Web Apps CLI&lt;/a&gt;. Install this tool globally by running this command in the terminal: &lt;code&gt;npm install -g @azure/static-web-apps-cli&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A free Ably Account, &lt;a href="https://ably.com/signup?utm_campaign=GLB-2210-serverless-workflow-visualizer&amp;amp;utm_content=blog-serverless-workflow-visualizer&amp;amp;utm_source=dev-to&amp;amp;utm_medium=blog&amp;amp;src=GLB-2210-serverless-workflow-visualizer-dev-to" rel="noopener noreferrer"&gt;sign up&lt;/a&gt; or &lt;a href="https://ably.com/login?utm_campaign=GLB-2210-serverless-workflow-visualizer&amp;amp;utm_content=blog-serverless-workflow-visualizer&amp;amp;utm_source=dev-to&amp;amp;utm_medium=blog&amp;amp;src=GLB-2210-serverless-workflow-visualizer-dev-to" rel="noopener noreferrer"&gt;log in&lt;/a&gt; to ably.com, and &lt;a href="https://faqs.ably.com/setting-up-and-managing-api-keys?utm_campaign=GLB-2210-serverless-workflow-visualizer&amp;amp;utm_content=blog-serverless-workflow-visualizer&amp;amp;utm_source=dev-to&amp;amp;utm_medium=blog&amp;amp;src=GLB-2210-serverless-workflow-visualizer-dev-to" rel="noopener noreferrer"&gt;create a new app and copy the API key&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Optional: The &lt;a href="https://marketplace.visualstudio.com/items?itemName=ably-labs.vscode-ably" rel="noopener noreferrer"&gt;Ably VSCode extension&lt;/a&gt; to have easy access to the API keys of your Ably app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are two components in this solution that run independently from each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The back-end that runs the Durable Functions workflow (&lt;code&gt;PizzaWorkflow.csproj&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The Static Web App that contains the front-end (a Vue3 project) and a function app (&lt;code&gt;Auth.csproj&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In order to run and test the solution locally, first start the PizzaWorkflow function app, then the Static Web Apps project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps to run the PizzaWorkflow function app
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;dotnet restore&lt;/code&gt; in the &lt;code&gt;api/PizzaWorkflow&lt;/code&gt; folder to install the dependencies.&lt;/li&gt;
&lt;li&gt;Rename the &lt;code&gt;api/PizzaWorkflow/local.settings.json.example&lt;/code&gt; file to &lt;code&gt;api/PizzaWorkflow/local.settings.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Copy/paste the Ably API key in the &lt;code&gt;ABLY_API_KEY&lt;/code&gt; field in the &lt;code&gt;local.settings.json&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Start Azurite (VSCode: &lt;code&gt;CTRL+SHIFT+P -&amp;gt; Azurite: Start&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Start the PizzaWorkflow function app by either pressing &lt;code&gt;F5&lt;/code&gt; or running &lt;code&gt;func start&lt;/code&gt; in the &lt;code&gt;api/PizzaWorkflow/&lt;/code&gt; folder.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Steps to run the Static Web Apps locally
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;npm install&lt;/code&gt; in the root folder to install the dependencies.&lt;/li&gt;
&lt;li&gt;Rename the &lt;code&gt;api/Auth/local.settings.json.example&lt;/code&gt; file to &lt;code&gt;api/Auth/local.settings.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Copy/paste the Ably API key in the &lt;code&gt;ABLY_API_KEY&lt;/code&gt; field in the &lt;code&gt;local.settings.json&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;swa start&lt;/code&gt; in the root folder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, browse to &lt;code&gt;http://localhost:4280&lt;/code&gt; and click the &lt;em&gt;Place Order&lt;/em&gt; button to start the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We've seen how to build a serverless workflow using Azure Functions and Durable Functions, and how to publish the progress in realtime using Ably. This type of solution is great for all kinds of realtime dashboards, and serverless back-ends, regardless of the cloud service provider.&lt;/p&gt;

&lt;p&gt;When applied at a larger scale, with hundreds of workflows running simultaneously, the same approach can be used to drive a front-end summarizing progress using live charts. I'll explore this in a future post.&lt;/p&gt;

&lt;p&gt;I encourage you to clone/fork &lt;a href="https://github.com/ably-labs/serverless-workflow-visualizer" rel="noopener noreferrer"&gt;the repository&lt;/a&gt; and run the solution yourself. Try to add another activity call to the workflow and have the front-end respond to it. &lt;a href="http://go.ably.com/discord" rel="noopener noreferrer"&gt;Let me know on Discord&lt;/a&gt; what your experience is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/ably/quest-for-serverless-websockets-an-adventure-with-azure-functions-durable-entities-36oh"&gt;Combine realtime pub/sub messaging with Azure Functions &amp;amp; Durable Entities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ably/how-to-use-pubsub-in-c-net-6-to-build-a-chat-app-1ok7"&gt;What is pub/sub and how to apply it in C# .NET to build a chat app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ably/cloud-pubsub-services-compared-azure-web-pubsub-ably-3gil"&gt;Cloud pubsub services compared: Azure Web PubSub &amp;amp; Ably&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>azurefunctions</category>
      <category>architecture</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Cloud pubsub services compared: Azure Web PubSub &amp; Ably</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Thu, 18 Aug 2022 11:18:00 +0000</pubDate>
      <link>https://dev.to/ably/cloud-pubsub-services-compared-azure-web-pubsub-ably-3gil</link>
      <guid>https://dev.to/ably/cloud-pubsub-services-compared-azure-web-pubsub-ably-3gil</guid>
      <description>&lt;p&gt;In this post, I compare two cloud pubsub services: &lt;a href="https://docs.microsoft.com/azure/azure-web-pubsub/overview"&gt;Azure Web PubSub&lt;/a&gt; and &lt;a href="https://ably.com/"&gt;Ably&lt;/a&gt;, to determine which provides the best development experience. The context will be a multi-user pixelart drawing application that I've built with both services. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How easy is it to use their SDKs/APIs?&lt;/li&gt;
&lt;li&gt;How much code do you need to write to add realtime capabilities to your app?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---Jk74YiP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5qk3j8faurqy2dxl90rc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---Jk74YiP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5qk3j8faurqy2dxl90rc.gif" alt="Collaborative Pixelart drawing animation" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not a step-by-step instruction on how to build the entire application, I'll highlight some key features and differences. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt;: Draw on the &lt;a href="https://pixel-paint.ably.dev/"&gt;live canvas&lt;/a&gt;, or look at the source code in the &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing"&gt;GitHub repo&lt;/a&gt; which includes &lt;a href="https://code.visualstudio.com/learn/educators/codetour"&gt;CodeTours&lt;/a&gt; to guide you through the solution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/sPgHwm3-yiM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Watch the video to learn about the three largest differences between Web PubSub and Ably.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Realtime messaging is taking over the world. Our food delivery apps show us our meal is only 5 minutes away. We use chat applications every day for work and private use. We receive the latest news on sports, stocks and celebrity news on our mobile devices, and we collaborate on digital documents.&lt;/p&gt;

&lt;p&gt;As I mentioned in my recent article about &lt;a href="https://dev.to/ably/how-to-use-pubsub-in-c-net-6-to-build-a-chat-app-1ok7"&gt;pubsub with C#.NET&lt;/a&gt;, there are plenty of benefits of using pubsub in multi-user realtime applications. The number of cloud-based messaging services that include client-side pubsub has been growing significantly and for developers it can be difficult to make a decision which one to use. The services available are built using protocols such as WebSockets: some services offer the native WebSocket API directly while others have created abstractions on top of it, making it easier to use.&lt;/p&gt;

&lt;p&gt;Let's have a look at the technologies used in this application and expand on the cloud-based WebSockets providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;I've created two versions of a collaborative drawing application. Both versions use these components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://p5js.org/"&gt;p5js&lt;/a&gt;: A creative coding library, used for the drawing canvas.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/azure-functions/functions-overview"&gt;Azure Functions&lt;/a&gt;: A serverless compute service from Microsoft Azure, used for the authentication endpoint, and a function to change the color palette.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/en-us/azure/static-web-apps/overview"&gt;Azure Static Web Apps&lt;/a&gt;: An Azure service for hosting the static web app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Serverless WebSockets
&lt;/h3&gt;

&lt;p&gt;One version of the drawing application uses &lt;a href="https://ably.com/"&gt;Ably&lt;/a&gt;, the other uses &lt;a href="https://docs.microsoft.com/azure/azure-web-pubsub/overview"&gt;Azure Web PubSub&lt;/a&gt;. Both are cloud-based pubsub services (or Serverless WebSocket providers), used for realtime communication. Here's an overview of some features they offer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Ably&lt;/th&gt;
&lt;th&gt;Azure Web PubSub&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pubsub messaging&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;message queues&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;presence&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;connection state recovery&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;sup&gt;1&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;automatic transport selection&lt;sup&gt;2&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;guaranteed message ordering&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;message history&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webhooks&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;sup&gt;1&lt;/sup&gt; &lt;em&gt;Web PubSub reliable subprotocol is still in preview.&lt;/em&gt;&lt;br&gt;
&lt;sup&gt;2&lt;/sup&gt; Ability to use the &lt;a href="https://ably.com/docs/key-concepts#any-internet-device"&gt;best transport available&lt;/a&gt; depending on client capablities.&lt;/p&gt;

&lt;p&gt;The communication between clients and the cloud-based messaging service is shown in this diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tb1B8tFN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9uji3t7wyzwnxf2otkr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tb1B8tFN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9uji3t7wyzwnxf2otkr.png" alt="Communication flow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How I compare cloud pubsub services
&lt;/h2&gt;

&lt;p&gt;I compare the two services by considering the following criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating the cloud resource&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Creating a client-side connection&lt;/li&gt;
&lt;li&gt;Presence&lt;/li&gt;
&lt;li&gt;Publishing messages&lt;/li&gt;
&lt;li&gt;Subscribing to messages&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Naming
&lt;/h3&gt;

&lt;p&gt;Before I start comparing the developer experience, let's have a look at naming first, since both cloud services sometimes use a different terminology for certain concepts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ably&lt;/th&gt;
&lt;th&gt;Azure Web PubSub&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;Service Instance&lt;/td&gt;
&lt;td&gt;A cloud resource is usually created per application. This resource can contain multiple channels/hubs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection&lt;/td&gt;
&lt;td&gt;Connection&lt;/td&gt;
&lt;td&gt;An individual client connection (over WebSockets) that is connected to the cloud service.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel&lt;/td&gt;
&lt;td&gt;Hub&lt;/td&gt;
&lt;td&gt;Logical group of client connections, usually for one purpose (e.g. chat).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presence&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Awareness of the clients present in a channel. Clients can announce their presence by entering a channel, or remove their presence by leave a channel. Presence can be queried to retrieve a list of clients present in a channel.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Group&lt;/td&gt;
&lt;td&gt;A subset of client connections. Clients can join or leave a group. A Group can't be queried for their members. Messages can be sent to a group instead of an entire Hub.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrations&lt;/td&gt;
&lt;td&gt;Event Handlers&lt;/td&gt;
&lt;td&gt;A method of adding extra functionality (e.g. calling a webhook, relaying a message to an upstream server) once messages are received at the cloud service.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;A user (person) uses a client (device) to establish a connection to the cloud service. One user can have multiple connections when using several clients.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;A client device that uses one connection to communicate with the cloud service.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;Information that is published by a user, sent to the cloud service and distributed to the connected clients.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For more detailed info, see &lt;a href="https://docs.microsoft.comazure/azure-web-pubsub/concept-service-internals"&gt;Web PubSub Service Internals&lt;/a&gt; and &lt;a href="https://ably.com/docs/key-concepts"&gt;Ably Key Concepts&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating the cloud resource
&lt;/h2&gt;

&lt;p&gt;Before we can do anything with cloud-based messaging, we need to create the appropriate resources.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ably
&lt;/h3&gt;

&lt;p&gt;For Ably, the first thing that has to be done is to create a new Ably app and an API key. If you've just &lt;a href="https://ably.com/signup"&gt;signed up&lt;/a&gt;, a new default app and root API key is already created for you. I do suggest that you create a dedicated Ably app for each application you build. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j99AEpqb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62emaznaclxfd7ca6bhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j99AEpqb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62emaznaclxfd7ca6bhy.png" alt="Create Ably App" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same goes for the API key. The default root API key has all the capabilities that Ably offers and probably has too many rights. For this application, you only require &lt;em&gt;Publish&lt;/em&gt;, &lt;em&gt;Subscribe&lt;/em&gt; and &lt;em&gt;Presence&lt;/em&gt; capabilities. The API key is required when using the Ably SDK for authentication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PdI9j3av--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nsqr7af1ud5mdz3t2pkg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PdI9j3av--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nsqr7af1ud5mdz3t2pkg.png" alt="Ably Dashboard with API key capabilities" width="800" height="186"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Creating the Ably app and API key can be done via:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ably.com/tutorials/publish-subscribe#setup-ably-account"&gt;The Ably dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/marketplace/actions/ably-control-api"&gt;The Ably GitHub action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ably-labs.vscode-ably"&gt;The Ably VSCode extension&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Web PubSub
&lt;/h3&gt;

&lt;p&gt;For Web PubSub Service, you need to create a new Web PubSub service resource in Azure. You require an &lt;a href="https://azure.microsoft.com/free/"&gt;Azure subscription&lt;/a&gt;. When creating the cloud resource, you have to specify a resource group and a region to deploy the service. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lFw2elrL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xsshhc3qwx6s918fvivp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lFw2elrL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xsshhc3qwx6s918fvivp.png" alt="Create Web PubSub instace" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the service instance is created, you'll find the connection strings in the Keys blade in the portal. One of these connection strings is required when using the server-side Web PubSub SDK for authentication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FXSow4-v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/idm81slout7dby51sl58.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FXSow4-v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/idm81slout7dby51sl58.png" alt="Azure Portal Web PubSub connection string" width="800" height="242"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Creating the Azure resource can be done via:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/azure/azure-web-pubsub/howto-develop-create-instance"&gt;The Azure portal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/azure/azure-web-pubsub/quickstart-cli-create"&gt;The Azure CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/azure/azure-web-pubsub/quickstart-bicep-template"&gt;Bicep templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Third-party infrastructure tools such as Terraform and Pulumi.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Resource creation differences
&lt;/h3&gt;

&lt;p&gt;For the Azure resource, you need to define a region where the Web PubSub resource will be deployed. For Ably, you don't have to pick a region, it's multi-region by default (hosted on AWS). When a client connection is established, the Ably client will select a region that results in the lowest latency.&lt;/p&gt;

&lt;p&gt;For Ably, you need to create an API key with the required capabilities, unless you're using the root key which has all the capabilities. For Web PubSub, there are connection strings (primary and secondary) and the Client URL Generator that is intended for test and validation purposes only.&lt;/p&gt;
&lt;h2&gt;
  
  
  Authentication
&lt;/h2&gt;

&lt;p&gt;For a client to connect to the cloud service, it requires a client access token. Both Web PubSub and Ably provide SDKs to generate these tokens server-side, since it's not advised to have these tokens in client-side code. To generate the tokens, the SDKs require a connection string or API key to authenticate to the cloud service.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ably
&lt;/h3&gt;

&lt;p&gt;For Ably, an Azure Function is written in C# that uses the .NET &lt;a href="https://www.nuget.org/packages/ably.io"&gt;ably.io NuGet package&lt;/a&gt; to create a client access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateTokenRequest&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"CreateTokenRequest/{clientId}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tokenParams&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TokenParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ClientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tokenData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_ablyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequestTokenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenParams&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/f223d1d582fa7f67a591137115e8feaf6c3f303f/api/CreateTokenRequest.cs#L21"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;CreateTokenRequest&lt;/code&gt; function depends on an Ably &lt;code&gt;IRestClient&lt;/code&gt; object via constructor injection. The dependency is configured in the &lt;code&gt;StartUp&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IFunctionsHostBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ablyApiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ABLY_APIKEY"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ablyClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AblyRest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ablyApiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IRestClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ablyClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/8339a4b070037dfb0c4a8e10b1c7f99a6cd3711d/api/Startup.cs#L12"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;The Ably API key mentioned in the &lt;em&gt;Creating the cloud resource&lt;/em&gt; section is put in the App Settings of the Azure Function App, so it can be retrieved as an environment variable, and used when an instance of the &lt;code&gt;AblyRest&lt;/code&gt; client is created.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Web PubSub
&lt;/h3&gt;

&lt;p&gt;For Azure Web PubSub, a similar &lt;code&gt;CreateTokenRequest&lt;/code&gt; Azure Function is created that uses the &lt;a href="https://www.nuget.org/packages/Azure.Messaging.WebPubSub"&gt;Azure.Messaging.WebPubSub NuGet package&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateTokenRequest&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"CreateTokenRequest/{hubName}/{groupName}/{clientId}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;hubName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;webPubSubService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WebPubSubServiceClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WebPubSubConnectionString"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;hubName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clientUri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;webPubSubService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetClientAccessUriAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
            &lt;span class="s"&gt;$"webpubsub.joinLeaveGroup.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;$"webpubsub.sendToGroup.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StringContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientUri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/da83ca3099da76bb16d12cde5aa3eca1b1f39b53/api/CreateTokenRequest.cs#L14"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;The Web PubSub connection string key mentioned in the &lt;em&gt;Creating the cloud resource&lt;/em&gt; section is put in the App Settings of the Azure Function App, retrieved as an environment variable, and used when an instance of the &lt;code&gt;WebPubSubServiceClient&lt;/code&gt; is created.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication differences
&lt;/h3&gt;

&lt;p&gt;The Ably specific function depends on the &lt;code&gt;AblyRest&lt;/code&gt; client, which only requires the Ably API key. The API key has the configured capabilities to allow publishing, subscribing and presence management for the channel. This means that if several different access rights are required for various roles, multiple API keys need to be created.&lt;/p&gt;

&lt;p&gt;The Web PubSub specific function requires more parameters to create a client token:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The connection string of the service.&lt;/li&gt;
&lt;li&gt;The Hub name&lt;/li&gt;
&lt;li&gt;The Group name*&lt;/li&gt;
&lt;li&gt;The client ID*&lt;/li&gt;
&lt;li&gt;The roles for leaving and joining the group*&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;&lt;sup&gt;&lt;em&gt;&lt;/em&gt;&lt;/sup&gt; These are required since a specific subprotocol is used for the client connection. See the *Creating a client-side connection&lt;/em&gt; section.&lt;/p&gt;

&lt;p&gt;The Web PubSub connection string has all the rights to the service. When the client access token is generated, the group-specific access is defined.&lt;/p&gt;

&lt;p&gt;Although the result is more or less the same (a client access token) the level of detail required for creating the connection is different.&lt;/p&gt;

&lt;p&gt;Since the &lt;code&gt;WebPubSubServiceClient&lt;/code&gt; requires the Hub name next to the connection string, I decided not to use dependency injection for the service client. Otherwise, I had to put the Hub name in an environment variable as well. I believe that a Hub name should be truly dynamic, so I prefer it not be present in configuration or back-end code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a client-side connection
&lt;/h2&gt;

&lt;p&gt;The user (with a client device) need to establish a connection with the cloud service. The client-side code will call the &lt;code&gt;CreateTokenRequest&lt;/code&gt; Azure Function shown earlier, that will return a client access token to establish the WebSocket-based connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ably
&lt;/h3&gt;

&lt;p&gt;Ably has a client SDK to manage connections and messaging, instead of using the native WebSockets API. An instance of the &lt;code&gt;Ably.Realtime&lt;/code&gt; client is created that uses the &lt;code&gt;CreateTokenRequest&lt;/code&gt; endpoint to obtain the client access token that contains the user ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/api/CreateTokenRequest/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;echoMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;ably&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Ably&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Realtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the &lt;code&gt;ably&lt;/code&gt; client is connected, the &lt;code&gt;channel&lt;/code&gt; can be retrieved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ably&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rewind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note that the &lt;code&gt;rewind&lt;/code&gt; parameter is used to retrieve the last two minutes of messages. This is possible because messages are persisted by Ably.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Azure Web PubSub
&lt;/h3&gt;

&lt;p&gt;Azure Web PubSub uses the WebSocket API directly by specifying the URL with the client access token, and a subprotocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`/api/CreateTokenRequest/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hubName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;clientUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;webSocket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json.webpubsub.azure.v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/da83ca3099da76bb16d12cde5aa3eca1b1f39b53/src/connectAzureWebPubSub.js#L13"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client-side connection differences
&lt;/h3&gt;

&lt;p&gt;For Ably, the &lt;a href="https://github.com/ably/ably-js"&gt;JavaScript client library&lt;/a&gt; is used to create a WebSocket-based connection and interacting with the cloud service. The benefit of using this library over the native WebSocket API is that a lot of boilerplate WebSocket code and difficult to implement concepts, such as reliable connection management, and message ordering, are taken care of for you.&lt;/p&gt;

&lt;p&gt;For Web PubSub, the &lt;a href="https://docs.microsoft.com/azure/azure-web-pubsub/reference-json-webpubsub-subprotocol"&gt;&lt;code&gt;json.webpubsub.azure.v1&lt;/code&gt; WebSocket subprotocol&lt;/a&gt; is specified when creating the connection. This is required for doing pubsub without the need of &lt;a href="https://docs.microsoft.com/azure/azure-web-pubsub/concept-service-internals#server_protocol"&gt;using event handlers&lt;/a&gt; which push messages to an upstream server. The downside of using this subprotocol is that custom events can only be supported by using event handlers. In this application, I did not use any integrations/event handlers to reduce complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Presence
&lt;/h2&gt;

&lt;p&gt;When a user visits the web application and clicks the &lt;em&gt;Connect&lt;/em&gt; button, they connect to the messaging service and their (randomly generated) client ID is added to a &lt;code&gt;users&lt;/code&gt; collection in the client-side code. The total number of connected users (client devices, actually) is shown next to the button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JRDkz1n6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivr0lknn7glgwflw721o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JRDkz1n6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivr0lknn7glgwflw721o.png" alt="Users connected" width="800" height="80"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;How do you find out which (or how many) clients are connected to the channel? This concept is called &lt;a href="https://ably.com/docs/realtime/presence"&gt;presence&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ably
&lt;/h3&gt;

&lt;p&gt;With Ably, presence is a built-in feature that is exposed via the client SDKs.&lt;/p&gt;

&lt;p&gt;The following four actions are done when a user has established a connection to Ably:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Subscribe to presence &lt;em&gt;enter&lt;/em&gt; messages.&lt;/li&gt;
&lt;li&gt;Subscribe to presence &lt;em&gt;leave&lt;/em&gt; messages.&lt;/li&gt;
&lt;li&gt;Get the presence of the channel.&lt;/li&gt;
&lt;li&gt;Enter their own presence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Subscribing to &lt;em&gt;enter&lt;/em&gt; and &lt;em&gt;leave&lt;/em&gt; presence messages published by other clients is done via the &lt;code&gt;channel.presence&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;addUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;removeUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire channel presence can be retrieved as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;addUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retrieving the total presence of a channel is very useful since you don't have to store the connected users yourself. As soon as a client has joined a channel, they can retrieve the presence and know which users are present in the same channel.&lt;/p&gt;

&lt;p&gt;A client can announce their own presence to the channel as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note that you can submit a JSON payload when entering (or updating) presence information. This is ideal for storing client-specific data. In this case, the color of their pixel cursor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/8339a4b070037dfb0c4a8e10b1c7f99a6cd3711d/src/connectAbly.js#L23"&gt;GitHub&lt;/a&gt; for the full implementation of user presence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Web PubSub
&lt;/h3&gt;

&lt;p&gt;Although Web PubSub does have the &lt;em&gt;Group&lt;/em&gt; concept and allows adding/remove users, it does not provide a way to retrieve user information from a &lt;em&gt;Group&lt;/em&gt;. So, there is no presence awareness in Web PubSub.&lt;/p&gt;

&lt;p&gt;Once a connection to the Web PubSub service has been established, these steps are performed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send a &lt;em&gt;joinGroup&lt;/em&gt; message, so the user is part of this group, and can send and receive group messages.&lt;/li&gt;
&lt;li&gt;Add user information to the &lt;code&gt;users&lt;/code&gt; collection (to identify the user on the client-side).&lt;/li&gt;
&lt;li&gt;Send a &lt;em&gt;sendToGroup&lt;/em&gt; message to broadcast their user information to the rest of the group.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;webSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;joinGroup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;addUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;webSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sendToGroup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;noEcho&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;messageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;joinedMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/da83ca3099da76bb16d12cde5aa3eca1b1f39b53/src/connectAzureWebPubSub.js#L21"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;However, one part is not yet accounted for. Say there are two clients, A and B, who are connecting in sequence (first A, then B) to the same Hub and Group. When B connects to the Hub, a &lt;em&gt;sendToGroup&lt;/em&gt; message is published by B which is received by A to announce B has joined. But B has no idea that A is also connected, since B connected later to the Hub than A.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eMvJYkLg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmzo1fjaqqmj9o0h0gqe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eMvJYkLg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmzo1fjaqqmj9o0h0gqe.png" alt="Web PubSub lack of presence" width="800" height="450"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The only way for B to be aware of A is that A sends a &lt;em&gt;sendToGroup&lt;/em&gt; message as well. The solution I chose was to always include the client ID and pixel cursor color when publishing the &lt;em&gt;hoverPositionMessage&lt;/em&gt; (a user moves their mouse over the pixel canvas):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;webSocket&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sendToGroup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;noEcho&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;messageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hoverPositionMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strokeColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/da83ca3099da76bb16d12cde5aa3eca1b1f39b53/src/User.js#L20"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;I don't really like this approach, since the payload gets a bit bloated with redundant information. Ideally, I'd only send &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; coordinates here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Presence differences
&lt;/h3&gt;

&lt;p&gt;The difference between Ably and Azure Web PubSub is significant when it comes to user presence. It would be great if Web PubSub would offer a way to list the users in a  group. Currently, some workarounds are required to get the same functionality as is offered by Ably. On the other hand, the Group concept of Web PubSub has no equivalent in Ably. You could say that both Hubs and Groups map to Channels in Ably. So if you require multiple Groups, you can use multiple channels in Ably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing messages
&lt;/h2&gt;

&lt;p&gt;The application publishes messages when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user's mouse hovers over the canvas.&lt;/li&gt;
&lt;li&gt;The user clicks a pixel on the canvas.&lt;/li&gt;
&lt;li&gt;The user enters the 'r' key to reset the canvas.&lt;/li&gt;
&lt;li&gt;The user selects a color palette.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Scenarios 1 to 3 use client-side publishing. Scenario 4 uses server-side publishing (from the Azure Function). Scenario 4 could also have been implemented via client-side publishing, but I wanted to use the .NET SDK to see how it differs from JavaScript and what other options there are when using server-side code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client-side publishing
&lt;/h3&gt;

&lt;p&gt;Let's look at the &lt;code&gt;mouseClicked&lt;/code&gt; function as an example of client-side publishing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ably
&lt;/h3&gt;

&lt;p&gt;For Ably, the &lt;code&gt;publish&lt;/code&gt; method is used on the &lt;code&gt;channel&lt;/code&gt; object. The method requires an event name and a payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;mouseClicked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mouseX&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mouseX&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;resoX&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mouseY&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mouseY&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;resoY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;clickCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mouseX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mouseY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clickPositionMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mouseX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mouseY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/8339a4b070037dfb0c4a8e10b1c7f99a6cd3711d/src/sketch.js#L83"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Web PubSub
&lt;/h3&gt;

&lt;p&gt;For Web PubSub, the &lt;code&gt;send&lt;/code&gt; method is used on the &lt;code&gt;WebSocket&lt;/code&gt; object. The method takes a JSON string and requires a &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;groupName&lt;/code&gt;, and a payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;mouseClicked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mouseX&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mouseX&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;resoX&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mouseY&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mouseY&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;resoY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;clickCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mouseX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mouseY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;webSocket&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sendToGroup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;noEcho&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;messageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clickPositionMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mouseX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mouseY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/da83ca3099da76bb16d12cde5aa3eca1b1f39b53/src/sketch.js#L90"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;To distinguish between messages, I added the &lt;code&gt;messageType&lt;/code&gt; field to the payload. The &lt;code&gt;noEcho&lt;/code&gt; argument is optional, but since I don't want to echo the message back to the user, it is set to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client-side publishing differences
&lt;/h3&gt;

&lt;p&gt;The Azure Web PubSub code is a bit more verbose since Groups are used. The usage of Groups is required though since the &lt;code&gt;json.webpubsub.azure.v1&lt;/code&gt; subprotocol is used because there was no need for me to send messages to an upstream server before sending them to the clients. Ideally, I want to use custom events using this subprotocol without the need of going through an upstream server. This is what Ably does by default by using the event name as the first argument.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;noEcho&lt;/code&gt; can be configured for each &lt;code&gt;send&lt;/code&gt; method separately using Web PubSub. Although this is very flexible, I wonder how many use cases there are that require this flexibility. For Ably, the &lt;code&gt;echoMessages&lt;/code&gt; option is set when creating the &lt;code&gt;Ably.Realtime&lt;/code&gt; client and is applied across all &lt;code&gt;publish&lt;/code&gt; methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-side publishing
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ChangeColorPalette&lt;/code&gt; Azure Function selects a color palette based on a palette ID in the route, and publishes a message to the cloud service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ably
&lt;/h3&gt;

&lt;p&gt;For Ably, the publishing is done using the &lt;code&gt;AblyRest&lt;/code&gt; client that is injected via the constructor and assigned to the &lt;code&gt;_ablyClient&lt;/code&gt; parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChangeColorPalette&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ChangeColorPalette/{paletteId}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;paletteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paletteId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// removed the switch cases to shorten this code snippet.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HttpUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParseQueryString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestUri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_ablyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"color-palette"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;paletteId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paletteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;colors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/8339a4b070037dfb0c4a8e10b1c7f99a6cd3711d/api/ChangeColorPalette.cs#L21"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;A channel is obtained by name and the &lt;code&gt;PublishAsync&lt;/code&gt; method on that channel is called that takes an event name (&lt;em&gt;color-palette&lt;/em&gt;) and a payload containing the &lt;code&gt;paletteId&lt;/code&gt; and the &lt;code&gt;colors&lt;/code&gt; array.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Web PubSub
&lt;/h3&gt;

&lt;p&gt;For Web PubSub, the &lt;code&gt;WebPubSub&lt;/code&gt; output binding is used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChangeColorPalette&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ChangeColorPalette/{groupId}/{paletteId}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;WebPubSub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Hub&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"{query.hubName}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;IAsyncCollector&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WebPubSubAction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;paletteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paletteId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// removed the switch cases to shorten this code snippet.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BinaryData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromObjectAsJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;messageType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"color-palette"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;paletteId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paletteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;colors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebPubSubAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSendToGroupAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WebPubSubDataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/da83ca3099da76bb16d12cde5aa3eca1b1f39b53/api/ChangeColorPalette.cs#L16"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;In general, I really like input &amp;amp; output bindings in Azure Functions, since they offer a quick way to move data in and out of a function without writing much code in the function body. The &lt;code&gt;WebPubSub&lt;/code&gt; binding however still requires a few lines of code since the payload needs to be formatted, a &lt;code&gt;WebPubSubAction&lt;/code&gt; object has to be created, and this has to be added to the &lt;code&gt;IAsyncCollector&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An alternative would be to use the &lt;code&gt;WebPubSubServiceClient&lt;/code&gt; object with the &lt;code&gt;SendToGroup&lt;/code&gt;  method in the function body instead of the output binding. It won't be much different regarding the lines of code required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-side publishing differences
&lt;/h3&gt;

&lt;p&gt;On the server-side, the amount of code required to publish a message is similar between Ably and Azure Web PubSub. I expected it to be less code for Web PubSub when using an output binding, but this wasn't the case. When looking at the steps required to publish a message, I find the Ably API a bit more concise and readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscribing to messages
&lt;/h2&gt;

&lt;p&gt;The clients are subscribing to the following messages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;hoverPositionMessage&lt;/em&gt;: When the mouse is moved over the canvas.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;clickPositionMessage&lt;/em&gt;: When the mouse is clicked on the canvas.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;changeColorPaletteMessage&lt;/em&gt;: When the color palette is changed.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;resetMessage&lt;/em&gt;: When the canvas is reset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ably
&lt;/h3&gt;

&lt;p&gt;For Ably, subscribing to specific messages looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hoverPositionMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;setUserPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clickPositionMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;clickCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changeColorPaletteMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;handleChangeColorPalette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paletteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resetMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resetGrid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/8c726d879f19c3c634f62f1f6449a6058de06c70/src/connectAbly.js#L39"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;In addition, the client can subscribe to presence events, when a user joins or leaves a channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;addUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;removeUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/8c726d879f19c3c634f62f1f6449a6058de06c70/src/connectAbly.js#L23"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Web PubSub
&lt;/h3&gt;

&lt;p&gt;For Azure Web PubSub, since it's using the native WebSocket API, the &lt;code&gt;onmessage&lt;/code&gt; event is used, which captures all kinds of events. It up to the developer to filter the exact type and since I added a separate &lt;code&gt;messageType&lt;/code&gt; field to the payload, that is used to identify the exact message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;webSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;hoverPositionMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nx"&gt;setUserPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;
                &lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;clickPositionMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nx"&gt;clickCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;changeColorPaletteMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nx"&gt;handleChangeColorPalette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paletteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;
                &lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;resetMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nx"&gt;resetGrid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;joinedMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nx"&gt;addUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
               &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing/blob/da83ca3099da76bb16d12cde5aa3eca1b1f39b53/src/connectAzureWebPubSub.js#L45"&gt;GitHub&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscribing differences
&lt;/h3&gt;

&lt;p&gt;The largest difference is the presence events, which are supported by Ably but not by Azure Web PubSub. Next to that, Ably supports event names that make it very convenient to subscribe to specific events. For Web PubSub, the lack of specific event subscriptions results in a large switch statement inside the &lt;code&gt;onmessage&lt;/code&gt; handler. Azure Web PubSub does support custom events, but these have to be processed with event handlers and sent to an upstream server, which adds additional complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Both Azure Web PubSub and Ably offer a similar pubsub feature set. Ably provides some additional features not offered by Web PubSub, such as presence, message history, and transport selection. I missed presence the most when creating the application with Web PubSub.&lt;/p&gt;

&lt;p&gt;I found the Ably client-side API easier to develop with than the native WebSockets API. Using a higher level client SDK for realtime messaging removes a lot of complexity and makes the code more concise. I prefer writing less code and focusing on the (business) domain, so I'm happy with using client SDKs if they make my life easier.&lt;/p&gt;

&lt;p&gt;Being able to publish named events and subscribe to them (client-side) without the need of handling them via an upstream server and custom event handlers is a big plus for me.&lt;/p&gt;

&lt;p&gt;A potential negative aspect of using a 3rd party library could be the download size of the client app. If you need to keep that to a minimum, then that would be an argument to choose native WebSockets.&lt;/p&gt;

&lt;p&gt;I haven't touched on reliable message delivery, message ordering, and default multi-region support. The Ably SDK takes care of this for you completely, and there's no need to manage &lt;code&gt;ackIDs&lt;/code&gt;, &lt;code&gt;connectionIds&lt;/code&gt;, and &lt;code&gt;reconnectionTokens&lt;/code&gt; yourself. &lt;/p&gt;

&lt;p&gt;I do admit I'm biased, since I have used Ably more than I have used the native WebSockets API. But look at the code snippets in this post and decide for yourself.&lt;/p&gt;

&lt;p&gt;I encourage you to clone/fork the &lt;a href="https://github.com/ably-labs/collaborative-pixel-drawing"&gt;GitHub repository&lt;/a&gt; and run the collaborative pixel drawing application yourself (either using Ably or Web PubSub). Try to add some extra pubsub features to it and &lt;a href="https://twitter.com/marcduiker"&gt;let me know&lt;/a&gt; what your experience is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/ably/quest-for-serverless-websockets-an-adventure-with-azure-functions-durable-entities-36oh"&gt;Combine realtime pub/sub messaging with Azure Functions &amp;amp; Durable Entities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ably/how-to-use-pubsub-in-c-net-6-to-build-a-chat-app-1ok7"&gt;What is pubsub and how to apply it in C# .NET to build a chat app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>programming</category>
      <category>architecture</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Combine realtime pub/sub messaging with Azure Functions &amp; Durable Entities</title>
      <dc:creator>Marc Duiker</dc:creator>
      <pubDate>Thu, 30 Jun 2022 13:13:00 +0000</pubDate>
      <link>https://dev.to/ably/quest-for-serverless-websockets-an-adventure-with-azure-functions-durable-entities-36oh</link>
      <guid>https://dev.to/ably/quest-for-serverless-websockets-an-adventure-with-azure-functions-durable-entities-36oh</guid>
      <description>&lt;p&gt;In this post I'll explain how data is distributed in realtime using pub/sub over WebSockets in a serverless application running in Azure. The context I'll be using is a multiplayer Advanced Dungeons &amp;amp; Dragons (ADnD) style game that is turn-based with realtime state updates.&lt;/p&gt;

&lt;p&gt;You'll learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to use pub/sub in C# Azure Functions to publish messages.&lt;/li&gt;
&lt;li&gt;How to use pub/sub in a JavaScript front-end to receive messages.&lt;/li&gt;
&lt;li&gt;How to persist state in Azure Functions using Durable Entities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--O48Kcnvz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a310gkcrfxsphudq423q.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--O48Kcnvz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a310gkcrfxsphudq423q.gif" alt="Serverless WebSockets Quest game play" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt;: Play the &lt;a href="https://quest.ably.dev/"&gt;live game&lt;/a&gt;, or look at the source code in the &lt;a href="https://github.com/ably-labs/serverless-websockets-quest"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Distributing state in serverless applications is complex
&lt;/h2&gt;

&lt;p&gt;Working with state in serverless applications and across many client devices is a difficult thing. First, serverless functions are usually stateless, so they can scale out easily without side effects, such as inconsistent state between the serverless instances. Secondly, synchronizing state across devices in a reliable and scalable way is a challenge to build yourself. Clients can temporarily lose connection, messages can get lost or be out of order.&lt;/p&gt;

&lt;p&gt;This post focuses on using the right cloud services to manage the game logic and synchronization of data in a reliable and scalable way. Let's assume we have the following requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers should be up &amp;amp; running quickly to produce a prototype.&lt;/li&gt;
&lt;li&gt;Operational maintenance should be minimal.&lt;/li&gt;
&lt;li&gt;Game logic should be running in the cloud and exposed via an HTTP API.&lt;/li&gt;
&lt;li&gt;A small amount of game data should be persisted.&lt;/li&gt;
&lt;li&gt;Game data provided by the backend should be distributed in realtime across all players.&lt;/li&gt;
&lt;li&gt;The client devices should cope with temporary connection issues and should receive messages in order.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tech stack
&lt;/h2&gt;

&lt;p&gt;A good solution that fits the above requirements is to have a serverless application that is quick to build, low in maintenance, and affordable to get started with.&lt;/p&gt;

&lt;p&gt;Player actions trigger HTTP-based Azure Functions that handle the game logic. Ably handles the distribution of data between the Functions and the clients.&lt;/p&gt;

&lt;p&gt;These are the high-level components used for the game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/azure-functions/functions-overview"&gt;Azure Functions&lt;/a&gt;, a serverless compute offering in Azure. This is used to create the HTTP API that clients can interact with.

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-entities?tabs=csharp"&gt;Entity Functions&lt;/a&gt;, an extension of Azure Functions, used to persist small pieces of game &amp;amp; player state.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ably.com/"&gt;Ably&lt;/a&gt;, an &lt;a href="https://ably.com/blog/ably-launches-state-of-edge-messaging"&gt;edge messaging&lt;/a&gt; solution, that offers serverless pub/sub over &lt;a href="https://ably.com/resources/ebooks/websocket-handbook"&gt;WebSockets&lt;/a&gt; to distribute data in realtime.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vuejs.org/"&gt;VueJS&lt;/a&gt;, a well-known front-end framework.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/static-web-apps/overview"&gt;Azure Static Web Apps&lt;/a&gt;, a hosting solution that serves the static files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eI955IGY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udjpz4hm6tqf8z9bla61.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eI955IGY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udjpz4hm6tqf8z9bla61.png" alt="Communication between player devices and the serverless application" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram 1: Communication between player devices and the serverless application.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The game API
&lt;/h2&gt;

&lt;p&gt;The API of the game exposes several HTTP endpoints which are implemented in C# (.NET 6) Azure Functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CreateQuest&lt;/strong&gt;; triggered by the first player to start a new quest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GetQuestExists&lt;/strong&gt;;  triggered by players who would like to join a quest to determine if they provided a valid quest ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AddPlayer&lt;/strong&gt;; triggered by the player once they have selected their character and name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ExecuteTurn&lt;/strong&gt;; triggered by the player when they want to attack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CreateTokenRequest&lt;/strong&gt;; provides an authentication token and is triggered when a connection to Ably is made via the front-end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All Azure Functions are just a couple of lines of code. The majority of the game logic is implemented in the &lt;code&gt;GameEngine&lt;/code&gt;, &lt;code&gt;GameState&lt;/code&gt;, and &lt;code&gt;Player&lt;/code&gt; classes. All functions related to game interaction only call methods in the &lt;code&gt;GameEngine&lt;/code&gt; class. The &lt;code&gt;GameEngine&lt;/code&gt; class is responsible for the game flow, and updating the state of the game and player objects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sUlUhmgM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zkm4od3i80itxm2dw8dk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sUlUhmgM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zkm4od3i80itxm2dw8dk.png" alt="Communication flow within the HTTP Azure Functions" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram 2: Communication flow within the HTTP Azure Functions.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a new quest
&lt;/h2&gt;

&lt;p&gt;To illustrate how Azure Functions and the &lt;code&gt;GameEngine&lt;/code&gt;, &lt;code&gt;GameState&lt;/code&gt;, and &lt;code&gt;Player&lt;/code&gt; classes work together, I'll show the &lt;code&gt;CreateQuest&lt;/code&gt; functionality starting at the Azure Function, and ending with publishing messages using Ably.&lt;/p&gt;

&lt;h3&gt;
  
  
  CreateQuest function
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;CreateQuest&lt;/code&gt; HTTP function is triggered when a player clicks the &lt;em&gt;Start Quest&lt;/em&gt; button. A new quest ID is generated client-side and provided in the request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateQuest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only showing the class members relevant for this blog section.&lt;/span&gt;
    &lt;span class="c1"&gt;// For the full implementation see https://github.com/ably-labs/serverless-websockets-quest/blob/main/api/Functions/CreateQuest.cs&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateQuest&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthorizationLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DurableClient&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;IDurableClient&lt;/span&gt; &lt;span class="n"&gt;durableClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;questId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsStringAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_realtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;questId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gameEngine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;durableClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;questId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gamePhase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;gameEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateQuestAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gamePhase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BadRequestObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"QuestId is required"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request is valid, this function will call the &lt;code&gt;CreateQuestAsync()&lt;/code&gt; method in the &lt;code&gt;GameEngine&lt;/code&gt; class.&lt;/p&gt;

&lt;h3&gt;
  
  
  GameEngine - CreateQuestAsync
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;GameEngine&lt;/code&gt; is responsible for the majority of the game flow and orchestrates actions using the &lt;code&gt;GameState&lt;/code&gt; and &lt;code&gt;Player&lt;/code&gt; classes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameEngine&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only showing the class members relevant for this blog section.&lt;/span&gt;
    &lt;span class="c1"&gt;// For the full implementation see https://github.com/ably-labs/serverless-websockets-quest/blob/main/api/Models/GameEngine.cs&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateQuestAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nextGamePhase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GamePhases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Character&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;InitializeGameStateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextGamePhase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nextGamePhase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeGameStateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;monsterEntityId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EntityId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEntityId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_questId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_durableClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SignalEntityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IPlayer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;monsterEntityId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_questId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitialHealth&lt;/span&gt;
            &lt;span class="p"&gt;}));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gameStateEntityId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EntityId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GameState&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;_questId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_durableClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SignalEntityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IGameState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;gameStateEntityId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitGameState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_questId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_durableClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SignalEntityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IGameState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;gameStateEntityId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPlayerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;InitializeGameStateAsync&lt;/code&gt; method is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating the monster, a &lt;code&gt;Player&lt;/code&gt; (a non-player character, NPC).&lt;/li&gt;
&lt;li&gt;Creating the initial &lt;code&gt;GameState&lt;/code&gt; that contains the quest ID and the name of the game phase.&lt;/li&gt;
&lt;li&gt;Adding the monster to the list of players in the &lt;code&gt;GameState&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stateful Entity Functions
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;GameState&lt;/code&gt; and &lt;code&gt;Player&lt;/code&gt; classes are so-called &lt;em&gt;Entity Functions&lt;/em&gt;, functions that are stateful. Their state is persisted in an Azure Storage Account that is abstracted away. You can interact with entities in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signaling (&lt;code&gt;SignalEntityAsync&lt;/code&gt;), which involves one-way (fire and forget) communication.&lt;/li&gt;
&lt;li&gt;Reading the state (&lt;code&gt;ReadEntityStateAsync&lt;/code&gt;), which involves two-way communication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more information about Entity Functions, please see the &lt;a href="https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-entities?tabs=csharp"&gt;Azure docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I chose Entity Functions since they're quick to get started with and ideal for storing small objects. Entity Functions require no additional Azure services, since a regular function app already comes with a storage account. Ideal for a demo like this.&lt;/p&gt;

&lt;p&gt;There are downsides to Entity Functions though. First, entities prioritize durability over latency. This means that it's not the fastest way to store data. This is because signaling tasks are sent to a storage queue which is being polled at a certain (configurable) frequency. The default configuration also enforces batch processing of signaling tasks which, in this game context, is not desirable. I changed the &lt;code&gt;maxQueuePollingInterval&lt;/code&gt; and &lt;code&gt;maxEntityOperationBatchSize&lt;/code&gt; settings in the host.json file to have an acceptable latency and consistency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"durableTask"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"storageProvider"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"maxQueuePollingInterval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00:00:01"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"maxEntityOperationBatchSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;a href="https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-perf-and-scale"&gt;Azure docs&lt;/a&gt; to learn more about performance and scale related to Entity Functions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Player entity function
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Player&lt;/code&gt; entity function is responsible for maintaining the state of a player in the game. The game has four players: the monster (NPC), and 3 real players. Each one has their own &lt;code&gt;Player&lt;/code&gt; entity function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MemberSerialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OptIn&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Player&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPlayer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only showing the class members relevant for this blog section.&lt;/span&gt;
    &lt;span class="c1"&gt;// For the full implementation see https://github.com/ably-labs/serverless-websockets-quest/blob/main/api/Models/Player.cs&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"questId"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;QuestId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"playerName"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;PlayerName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"characterClass"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CharacterClass&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"health"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Health&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;playerFields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;QuestId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;playerFields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;PlayerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;playerFields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;CharacterClass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;playerFields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;Health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToInt32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerFields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_publisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAddPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QuestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PlayerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once a &lt;code&gt;Player&lt;/code&gt; entity is initialized, a message will be published to an Ably channel. The players who have joined the quest are subscribed to this channel and will receive a message that a new player has joined.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Operations on Entity Functions are recommended to be implemented via interfaces to ensure type checking. There are however some restrictions on entity interfaces, as described in the &lt;a href="https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-dotnet-entities#restrictions-on-entity-interfaces"&gt;Azure docs&lt;/a&gt;. One limitation is that interface methods must not have more than one parameter. Since I want to initialize a &lt;code&gt;Player&lt;/code&gt; entity and set multiple parameters in one go instead of setting each one individually, I decided to provide an object array as a parameter. This then requires casting/converting the array elements to their correct type. Not ideal, but it works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  GameState entity function
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;GameState&lt;/code&gt; entity function is responsible for maintaining the state of the game, such as the quest ID, the player names, and the game phase (start, character selection, play, end).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MemberSerialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OptIn&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GameState&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IGameState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only showing the class members relevant for this blog section.&lt;/span&gt;
    &lt;span class="c1"&gt;// For the full implementation see https://github.com/ably-labs/serverless-websockets-quest/blob/main/api/Models/GameState.cs&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"questId"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;QuestId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"phase"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitGameState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;gameStateFields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;QuestId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gameStateFields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;Phase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gameStateFields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_publisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishUpdatePhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QuestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"players"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PlayerNames&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddPlayerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlayerNames&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PlayerNames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;playerName&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PlayerNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsPartyComplete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;UpdatePhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GamePhases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Play&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;AttackByMonster&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;UpdatePhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Phase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_publisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishUpdatePhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QuestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AttackByMonster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playerAttacking&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playerUnderAttack&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetRandomPlayerName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDamageFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_publisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishPlayerAttacking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QuestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerAttacking&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerUnderAttack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;playerEntityId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EntityId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEntityId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QuestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;playerUnderAttack&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SignalEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IPlayer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;playerEntityId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nextPlayerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetNextPlayerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CharacterClassDefinitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_publisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishPlayerTurnAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QuestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"Next turn: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nextPlayerName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextPlayerName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;GameState&lt;/code&gt; also performs actions such as signaling &lt;code&gt;Player&lt;/code&gt; entities and publishing messages, as can be seen in the &lt;code&gt;AttackByMonster&lt;/code&gt; method.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that the &lt;code&gt;AttackByMonster&lt;/code&gt; method contains two calls to &lt;code&gt;Task.Delay()&lt;/code&gt;. This is to add a bit of delay between publishing the messages about the automated monster attack. Without the delay, the action would be much quicker compared with what real players do. Since this is a turn-based game, with realtime state updates, this seemed more natural to me.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Publishing messages
&lt;/h3&gt;

&lt;p&gt;The final step of game logic functionality in the API is publishing messages to the players that have joined the quest. Since several classes need access to this functionality, I've wrapped it in a &lt;code&gt;Publisher&lt;/code&gt; class for ease of use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Publisher&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only showing the class members relevant for this blog section.&lt;/span&gt;
    &lt;span class="c1"&gt;// For the full implementation see https://github.com/ably-labs/serverless-websockets-quest/blob/main/api/Models/Publisher.cs&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;PublishAddPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;questId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;characterClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_ablyClient&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ablyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;questId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"add-player"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;characterClass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;characterClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;health&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;PublishUpdatePhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;questId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;teamHasWon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_ablyClient&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ablyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;questId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"update-phase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;teamHasWon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;teamHasWon&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publishing messages is the easiest part of the API. The &lt;code&gt;_ablyClient&lt;/code&gt; in the code sample above is an instance of the Ably REST client and responsible for publishing messages to a &lt;a href="https://ably.com/docs/core-features/channels"&gt;channel&lt;/a&gt;. The REST client is used since code is running in a short-lived Azure Function and doesn't require a bidirectional WebSocket connection. The client retrieves the channel, the quest ID in this case, and &lt;code&gt;PublishAsync&lt;/code&gt; is called that accepts an event name and a payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receiving messages client-side
&lt;/h2&gt;

&lt;p&gt;The client-side is subscribed to the messages published via the API using the realtime Ably client that is based on WebSockets. Based on the type of message received, the game progresses to the next phase, and local player state is updated. So, even though this is a turn-based game, updates in the API result in realtime communication with the players to update their local player state.&lt;/p&gt;

&lt;p&gt;The clients require a connection to Ably to receive messages in realtime. The &lt;code&gt;createRealtimeConnection&lt;/code&gt; function is called when players start a new quest or join a quest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;createRealtimeConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;questId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;realtimeClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Realtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/api/CreateTokenRequest/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;echoMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtimeClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConnectionStateChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Ably connection status: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messageText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attachToChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;questId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;disconnected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Ably connection status: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messageText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;attachToChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channelInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtimeClient&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rewind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;channelInstance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribeToMessages&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a new instance of the &lt;code&gt;Realtime&lt;/code&gt; object is created, the &lt;code&gt;authUrl&lt;/code&gt; is set by calling the &lt;code&gt;CreateTokenRequest&lt;/code&gt; endpoint in the API. This returns a JWT token that is used to &lt;a href="https://ably.com/docs/api/realtime-sdk/authentication"&gt;authenticate with Ably&lt;/a&gt;. This approach prevents any API keys from being present in the front-end and potentially ending up in source control.&lt;/p&gt;

&lt;p&gt;Once a connection is made, the client attaches to the channel (named after the &lt;code&gt;questId&lt;/code&gt;) and subscribes to messages published by Ably.&lt;/p&gt;

&lt;p&gt;As an example of how the front-end updates when a player attacks, let's have a look how the &lt;code&gt;player-under-attack&lt;/code&gt; message is handled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;subscribeToMessages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Only showing the functions relevant for this blog section.&lt;/span&gt;
    &lt;span class="c1"&gt;// For the full implementation see https://github.com/ably-labs/serverless-websockets-quest/blob/main/src/stores/index.ts&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelInstance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;player-under-attack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handlePlayerIsUnderAttack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="nx"&gt;handlePlayerIsUnderAttack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;teamHasWon&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;characterClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CharacterClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;characterClass&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDefeated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDefeated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; received &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; damage`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messageText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatePlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;characterClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDefeated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="nx"&gt;updatePlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;characterClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDefeated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isUnderAttack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characterClass&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Fighter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$patch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;fighter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;playerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;isUnderAttack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isUnderAttack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;isDefeated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isDefeated&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fighter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;damage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characterClass&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Ranger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Update ranger object&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characterClass&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Mage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Update mage object&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characterClass&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;CharacterClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Monster&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Update monster object&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local Vue store (&lt;a href="https://pinia.vuejs.org/"&gt;Pinia&lt;/a&gt;) contains definitions for each player. The store is updated with data coming from the messages pushed by Ably.&lt;/p&gt;

&lt;p&gt;Vue components such as the &lt;code&gt;PlayerUnit&lt;/code&gt; use the local data store to display the character information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- See https://github.com/ably-labs/serverless-websockets-quest/blob/main/src/components/PlayerUnit.vue for the full implementation. --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"!props.isPlayerSelect"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"props.useHealth"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"health"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ targetPlayer.health }} HP&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"damage"&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"showDamage()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-{{ targetPlayer.damage }}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"stats"&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"props.showStats"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Damage caused:&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ targetPlayer.totalDamageApplied }}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;v-bind:class=&lt;/span&gt;&lt;span class="s"&gt;"{ isActive: isActive(), isDefeated: targetPlayer.isDefeated }"&lt;/span&gt; &lt;span class="na"&gt;:alt=&lt;/span&gt;&lt;span class="s"&gt;"targetPlayer.characterClass"&lt;/span&gt; &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;"getAsset()"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;figcaption&amp;gt;&lt;/span&gt;{{ targetPlayer.name }}&lt;span class="nt"&gt;&amp;lt;/figcaption&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"props.isPlayerSelect"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;:id=&lt;/span&gt;&lt;span class="s"&gt;"targetPlayer.characterClass"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"character"&lt;/span&gt; &lt;span class="na"&gt;:value=&lt;/span&gt;&lt;span class="s"&gt;"targetPlayer.characterClass"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"store.characterClass"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"store.playerName=getName()"&lt;/span&gt; &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"isDisabled()"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;:for=&lt;/span&gt;&lt;span class="s"&gt;"targetPlayer.characterClass"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;:alt=&lt;/span&gt;&lt;span class="s"&gt;"targetPlayer.characterClass"&lt;/span&gt; &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;"getAsset()"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;figcaption&amp;gt;&lt;/span&gt;{{ getName() }}&lt;span class="nt"&gt;&amp;lt;/figcaption&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running locally
&lt;/h2&gt;

&lt;p&gt;You require the following dependencies to run the solution locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dotnet.microsoft.com/download/dotnet/6.0"&gt;.NET 6&lt;/a&gt;. The .NET runtime required for the C# Azure Functions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en/"&gt;Node 16&lt;/a&gt;. The JavaScript runtime required for the Vue front-end.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/azure/azure-functions/functions-run-local?tabs=v4%2Cwindows%2Ccsharp%2Cportal%2Cbash"&gt;Azure Functions Core Tools&lt;/a&gt;. This is part of the Azure Functions extensions for VSCode that should be recommended for installation when this repo is opened in VSCode.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://marketplace.visualstudio.com/items?itemName=Azurite.azurite"&gt;Azurite&lt;/a&gt;. This is a local storage emulator that is required for Entity Functions. When this repo is opened in VSCode, a message will appear to install this extension.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Azure/static-web-apps-cli"&gt;Azure Static Web Apps CLI&lt;/a&gt;. This is the command line interface to develop and deploy Azure Static Web Apps. Install this tool globally by running this command in the terminal: &lt;code&gt;npm install -g @azure/static-web-apps-cli&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="(https://ably.com/signup)"&gt;Sign up&lt;/a&gt; for a free Ably Account, &lt;a href="https://faqs.ably.com/setting-up-and-managing-api-keys"&gt;create a new app, and copy the API key&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Clone or fork the Serverless WebSockets Quest &lt;a href="https://github.com/ably-labs/serverless-websockets-quest"&gt;GitHub repo&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm install&lt;/code&gt; in the root folder.&lt;/li&gt;
&lt;li&gt;Rename the &lt;code&gt;api\local.settings.json.example&lt;/code&gt; file to &lt;code&gt;api\local.settings.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Copy/paste the Ably API key in the &lt;code&gt;ABLY_APIKEY&lt;/code&gt; field in the &lt;code&gt;local.settings.json&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Start Azurite (VSCode: &lt;code&gt;CTRL+SHIFT+P -&amp;gt; Azurite: Start&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;swa start&lt;/code&gt; in the root folder.&lt;/p&gt;

&lt;p&gt;You'll see this error message, which is a warning really, you can ignore this when running the solution locally:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Function app contains non-HTTP triggered functions. Azure Static Web Apps managed functions only support HTTP functions. To use this function app with Static Web Apps, see 'Bring your own function app'.
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The terminal will eventually output this message that indicates the emulated Static Web App is running:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Azure Static Web Apps emulator started at http://localhost:4280. Press CTRL+C to exit.
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the browser and navigate to &lt;code&gt;http://localhost:4280/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Deploying to the cloud
&lt;/h2&gt;

&lt;p&gt;To have the entire solution running in the Azure cloud, you'll need an &lt;a href="https://azure.microsoft.com/free/"&gt;Azure account&lt;/a&gt;, and a &lt;a href="https://github.com/"&gt;GitHub account&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The free Azure Static Web Apps (SWA) tier comes with &lt;em&gt;Managed Azure Functions&lt;/em&gt;. These are functions that are included in the Static Web App service. The downside of these managed functions is that only HTTP trigger functions are supported. Our game API uses Entity Functions as well. In order for Static Web Apps to work with our API we need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy the API separately to a dedicated Function App.&lt;/li&gt;
&lt;li&gt;Use the Standard (non-free) tier of Azure Static Web Apps.&lt;/li&gt;
&lt;li&gt;Update the configuration of SWA to indicate that a dedicated Function App is being used.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Deployment steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;I've created a &lt;a href="https://github.com/ably-labs/serverless-websockets-quest/blob/main/.github/workflows/functionapp.yml"&gt;GitHub workflow&lt;/a&gt; to:

&lt;ul&gt;
&lt;li&gt; Create the Azure resources.&lt;/li&gt;
&lt;li&gt; Build the C# API.&lt;/li&gt;
&lt;li&gt; Deploy the API to the Function App.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;This approach uses the &lt;em&gt;Azure Login&lt;/em&gt; action and requires the creation of an Azure Service Principal, as is explained in more detail in &lt;a href="https://github.com/Azure/login#configure-a-service-principal-with-a-secret"&gt;this README&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;The SWA resource was created in the Azure portal using &lt;a href="https://docs.microsoft.com/azure/static-web-apps/getting-started?tabs=vue"&gt;this quick start&lt;/a&gt;. This results in a generated GitHub workflow that will be included in the GitHub repository.&lt;/li&gt;
&lt;li&gt;Follow &lt;a href="https://docs.microsoft.com/azure/static-web-apps/functions-bring-your-own"&gt;these instructions&lt;/a&gt; to configure SWA to use a dedicated Function App.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Using serverless technology is a great way to get up and running in the cloud quickly and not worry about server maintenance. By combining serverless functions (Azure Functions) with serverless WebSockets (Ably) you can build a realtime solution that is cost-efficient and can scale automatically.&lt;/p&gt;

&lt;p&gt;Although this demo is built around a game concept, other live and collaborative experiences are also a good fit for this tech stack. These include chat apps, location tracking apps, and realtime monitoring dashboards.&lt;/p&gt;

&lt;p&gt;You've learned how to publish messages from the Azure Functions back-end and receive messages in the VueJS front-end to create a realtime experience for the players.&lt;/p&gt;

&lt;p&gt;I encourage you to fork the repository available on &lt;a href="https://github.com/ably-labs/serverless-websockets-quest"&gt;GitHub&lt;/a&gt; and see if you can extend it (add a cleric who can heal other players?). Please don't hesitate to &lt;a href="https://twitter.com/marcduiker"&gt;contact me on Twitter&lt;/a&gt; or &lt;a href="https://discord.gg/q89gDHZcBK"&gt;join our Discord server&lt;/a&gt; in case you have any questions or suggestions related to this project.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>azure</category>
      <category>dotnet</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
