<?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: Ninjobu</title>
    <description>The latest articles on DEV Community by Ninjobu (@ninjobu).</description>
    <link>https://dev.to/ninjobu</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%2F613247%2F9985bc9a-ef19-41b8-97d1-79e5b95e7dc7.png</url>
      <title>DEV Community: Ninjobu</title>
      <link>https://dev.to/ninjobu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ninjobu"/>
    <language>en</language>
    <item>
      <title>You have unread messages</title>
      <dc:creator>Ninjobu</dc:creator>
      <pubDate>Fri, 16 Apr 2021 16:30:34 +0000</pubDate>
      <link>https://dev.to/ninjobu/you-have-unread-messages-43p4</link>
      <guid>https://dev.to/ninjobu/you-have-unread-messages-43p4</guid>
      <description>&lt;p&gt;Imagine creating a profile on this job searching site that promises to connect you to industry insiders. A recruiter sees it and messages you with a dream opportunity. But, you never see the message because you signed up to this site for fun and haven't logged back in to check your messages for weeks. That was  &lt;a href="https://ninjobu.com"&gt;Ninjobu&lt;/a&gt;, at least up until a few days ago.&lt;/p&gt;

&lt;p&gt;In the  &lt;a href="http://blog.ninjobu.com/building-a-chat-with-firebase"&gt;last post&lt;/a&gt;, I wrote a bit about how I set up the chat system so recruiters could communicate with candidates. One significant omission was new message notifications. Since we have a web app here, it's unlikely people stay active on the platform for long, the way you might do on social media or IM. And, arguably, user communication is a crucial part of asynchronous job searching. It made sense for the next feature to be a way to let users know when someone contacts them.&lt;/p&gt;

&lt;p&gt;My goal was to write a simple solution that would notify users when they have new unread messages while not being too naggy. The resulting structure ended up straightforward, implemented with just two Firebase functions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ohH4Xrof--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618530344158/IRpF4uBj-.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ohH4Xrof--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618530344158/IRpF4uBj-.png" alt="chat_format.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As mentioned in the previous post, our database consists of chat documents that store the last message timestamp and when each chat participant last viewed that message. With this information, we can easily decide which users we should notify.&lt;/p&gt;

&lt;p&gt;First, I added a new Firebase function that triggers on writes for each chat document. The &lt;em&gt;onWrite&lt;/em&gt; trigger executes for the creation, updates, and deletions of documents. I ignore the deletion case, but I know the last message timestamp will have updated during the creation and update events.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--d_mJLn5v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618530940582/tConhnLsu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--d_mJLn5v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618530940582/tConhnLsu.png" alt="on_write_func.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a chat is updated, it will have a &lt;code&gt;lastMessageTime&lt;/code&gt; timestamp and an array &lt;code&gt;lastSeenTime&lt;/code&gt; with two entries: one timestamp for each of the two chat participants, representing when they last saw the chat. If any of the &lt;code&gt;lastSeenTime&lt;/code&gt; timestamps are older than the message, we record the chat id in a document &lt;code&gt;misc/chats_to_notify&lt;/code&gt; for later. Firebase's &lt;em&gt;FieldValue.arrayUnion&lt;/em&gt; utility lets us atomically add unique entries to an array.&lt;/p&gt;

&lt;p&gt;With the above function running every time a chat document is updated, we will end up with a list of chat ids in our &lt;code&gt;misc/chats_to_notify&lt;/code&gt; document. The next step is to go through these chats and send emails to the participants, as required. We do this with a second function that we schedule at a set interval.&lt;/p&gt;

&lt;p&gt;Being tightly integrated with GCP, Firebase offers many features from Google Cloud wrapped in simple-to-use interfaces. One example is scheduling functions that run at predefined intervals, using Google Cloud Pub/Sub and Scheduler behind the scenes. The function that handles our email notifications is a bit chunkier, so let's split it into smaller parts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--P2FuSsuz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618531160273/3kFHCct86.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P2FuSsuz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618531160273/3kFHCct86.png" alt="notification_func_0.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I set up the function to run once every two hours, quickly done with the  &lt;a href="https://cloud.google.com/appengine/docs/standard/python/config/cronref"&gt;App Engine cron.yaml&lt;/a&gt; syntax.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6zSN9EiI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618531218701/Crbj8GPK2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6zSN9EiI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618531218701/Crbj8GPK2.png" alt="notification_func_1.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start by retrieving the &lt;code&gt;misc/chats_to_notify&lt;/code&gt; document where we have the ids for our updated chats, and early out if the document is invalid or the list of chats is empty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--f-O-QrpO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618531247343/fMhvVcTdj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--f-O-QrpO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618531247343/fMhvVcTdj.png" alt="notification_func_2.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we have our chats, I chose to process only up to 100 of them on each invocation. Firebase functions have a running time limit of 60 seconds, and I don't want the function to time out and not send any emails if the list of updated chats is too long. However, my choice is a bit premature and speculative. The 100 chats limit was chosen arbitrarily, and Firebase allows the function timeout to be configured to up to 9 minutes. I'm also unsure how the function itself will scale with a significantly larger array of chat ids. I will likely have to tweak this once I have more data. But for now, processing 100 chats every 2 hours seems reasonable based on the website's current activity.&lt;/p&gt;

&lt;p&gt;While looping over each chat, we add its id to the &lt;code&gt;processed&lt;/code&gt; array and later use it to remove the entry from the &lt;code&gt;misc/chats_to_notify&lt;/code&gt; document. Next, we retrieve the chat document data and validate the required fields. We need &lt;code&gt;lastMessageTime&lt;/code&gt; to exist and the &lt;code&gt;members&lt;/code&gt; array to contain the two chat participants' UIDs. We then save each member's UID if their &lt;code&gt;lastSeenTime&lt;/code&gt; is older than the &lt;code&gt;lastMessageTime&lt;/code&gt;. This check is important because, between the time the chat id was recorded in the &lt;code&gt;misc/chats_to_notify&lt;/code&gt; document and the time this function runs, each member of the chat may have already seen the last message. We don't want to send an email notification for a read message. Our &lt;em&gt;onWrite&lt;/em&gt; function for the chat document only adds chat ids to the notification list and doesn't remove them.&lt;/p&gt;

&lt;p&gt;A thing to keep in mind here is, if we have 100 chats where the participants have both seen the messages, this function will process those chat entries and not send any emails until the following invocation 2 hours later. I'm not entirely happy with this, but I consider this part of the code temporary until I have more data on how many emails one run of the function can process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bK1UKyWZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618532091941/DuCqToPyJ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bK1UKyWZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618532091941/DuCqToPyJ.png" alt="notification_func_3.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We now have a list of UIDs for users that need to receive a notification email. For each UID, we get the user's email from the Firebase Auth module and create a personalization entry.  &lt;a href="https://sendgrid.com/docs/for-developers/sending-email/personalizations/"&gt;Personalizations&lt;/a&gt; are a Sendgrid feature that lets us send the same email to multiple recipients with a single API call. It also ensures that each recipient will only see their email address in the &lt;em&gt;to&lt;/em&gt; field and avoids catastrophic invasion of privacy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eoV8Kuj6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618532201418/rV6SMtXFo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eoV8Kuj6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1618532201418/rV6SMtXFo.png" alt="notification_func_4.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we remove all the chat ids we have processed from the &lt;code&gt;misc/chats_to_notify&lt;/code&gt; document to avoid sending multiple emails to the same folks. The &lt;em&gt;FieldValue.arrayRemove&lt;/em&gt; feature allows us to do this quickly with a single call. &lt;/p&gt;

&lt;p&gt;Speaking of the array with chat ids, it may be worth noting that at some point, this may become a bottleneck. Firestore document sizes have a limit of 1 MiB. If we assume the auto-generated chat ids continue to be 20 bytes each like they are at the moment, we have room for a bit over 50,000 entries in the array before we blow the size limit. Additionally, Firebase has a soft limit of one write per second to the same document that they don't recommend you go over to avoid contention errors. It's a soft limit in the sense that it shouldn't cause issues in short bursts, but something to keep in mind. If there's loads of chat activity on the site, the &lt;code&gt;misc/chats_to_notify&lt;/code&gt; document will be hammered and potentially go over the one write per second limit. Once we hit these problems, however, congratulations are in order, most likely.&lt;/p&gt;

&lt;p&gt;Thank you for reading this far. If you are a software engineer open to new opportunities but not actively looking for work, try out &lt;a href="https://ninjobu.com"&gt;Ninjobu&lt;/a&gt;! Create a profile, let recruiters know what job and salary you'd like, and who knows what might happen.&lt;/p&gt;

&lt;p&gt;Until next time.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>firebase</category>
    </item>
    <item>
      <title>Building a chat with Firebase</title>
      <dc:creator>Ninjobu</dc:creator>
      <pubDate>Thu, 15 Apr 2021 01:25:26 +0000</pubDate>
      <link>https://dev.to/ninjobu/building-a-chat-with-firebase-2jlo</link>
      <guid>https://dev.to/ninjobu/building-a-chat-with-firebase-2jlo</guid>
      <description>&lt;h1&gt;
  
  
  How can users chat in my web app?
&lt;/h1&gt;

&lt;p&gt;I recently launched  &lt;a href="https://ninjobu.com" rel="noopener noreferrer"&gt;Ninjobu&lt;/a&gt;, an anonymous job search platform for software engineers. The premise is, as a programmer, you create a profile with your previous experience, programming languages used, domain knowledge, etc., as well as what role you are looking for, preferred location (or remote), and desired salary. Next, you sit back and wait while recruiters search the list of profiles to find matching candidates for their open roles. Once a recruiter has found a potentially suitable candidate, they make contact and start the hiring process, which brings us to the chat system.&lt;/p&gt;

&lt;p&gt;Ninjobu only needs a way for recruiters to initiate a 1-on-1 chat with candidates. There's no need for groups, file sharing, emojis, gifs, or similar features. Although you can send emojis just fine, there's no UI for selecting them, and it's more of a happy accident due to how Firestore supports Unicode rather than something I specifically implemented. The lack of these additional features simplifies the chat system's requirements somewhat, but we code what we need to code and nothing else, right?&lt;/p&gt;



&lt;h1&gt;
  
  
  Data design
&lt;/h1&gt;

&lt;p&gt;I went with a design loosely inspired by the  &lt;a href="https://firebase.google.com/docs/firestore/billing-example" rel="noopener noreferrer"&gt;Firebase pricing example&lt;/a&gt;. The primary data consists of a collection of chats, where each chat has a subcollection of messages.&lt;/p&gt;

&lt;p&gt;There are two members in every chat because Ninjobu only needs 1-on-1 communication. It's trivial to extend this to groups with additional members. Still, since I don't anticipate the need for multi-member discussions in this project, parts of the code occasionally assume only two chat members exist.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618157735551%2Fz4C4EJtnh.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618157735551%2Fz4C4EJtnh.png" alt="chat_json.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;members&lt;/code&gt; array then contains two entries: a unique user id for each chat participant. With these UIDs, we can find the chats for a specific user and block everyone else from accessing the data. Ninjobu uses Firebase Auth to authenticate users, and I use the UID from the Auth module to identify each user in the chat data.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;names&lt;/code&gt; array holds the name of the chat members, and the &lt;code&gt;desc&lt;/code&gt; array holds a secondary string for each user. For the recruiter, this is the company name, and for the candidate, it's their preferred job title. Storing these strings in the chat document allows us to give each chat a bit of context in the user's inbox without querying any additional data.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618157822077%2FgiD3pfNYU.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618157822077%2FgiD3pfNYU.png" alt="inbox_window.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ninjobu aims to keep its candidate users anonymous. Candidates aren't even asked for their names when they sign up. When communicating with a candidate, their name shows up as &lt;em&gt;candidate#xyz&lt;/em&gt; while the recruiter's name and company are visible as expected.&lt;/p&gt;

&lt;p&gt;Lastly, we have a string with the last message sent to this chat and the timestamp when this happened. With this data in the document, we can query a list of chats and display them similar to how an email application might show the user a list of emails. When a user selects a chat, we pull the subcollection of messages as needed.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618157871913%2Ffl68gnNn4.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618157871913%2Ffl68gnNn4.png" alt="message_window.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The message data is straightforward. Each document stores the author UID, the timestamp for the message, and the string itself. The UID is used to check the author's index in the parent chat's &lt;code&gt;members&lt;/code&gt; array, and that index maps to the author name and secondary string in the &lt;code&gt;names&lt;/code&gt; and &lt;code&gt;desc&lt;/code&gt; arrays, respectively. You could store the member's index instead of the UID here, but using the UID simplified some of the front-end code in my case.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618157939889%2F8vgIwmLhm.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618157939889%2F8vgIwmLhm.png" alt="message_json.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Firebase calculates the cost based on the number of documents read, deleted, and updated, in addition to network bandwidth usage and storage. With this setup, we duplicate some data, such as the last message sent, but it allows us to request fewer documents overall. The flipside to this is that when we post a new chat message, we need to create a new document in the message subcollection and update the parent chat document with the last message string and timestamp. I still think this is acceptable. Presumably, there will be much more document reads from users logging in and viewing their inbox than document writes from users sending new messages.&lt;/p&gt;



&lt;h1&gt;
  
  
  Keeping data private
&lt;/h1&gt;

&lt;p&gt;Firestore data is encrypted when saved to the hard drives, and Ninjobu redirects any unencrypted requests to go over HTTPS, which means our data should stay safe in transit and at rest. But we still need to make sure chats are only visible to their participants. We do this with Firebase's excellent security rule system.&lt;/p&gt;

&lt;p&gt;Firestore integrates with Firebase Auth to make our life easy. Since the chat documents have a &lt;code&gt;members&lt;/code&gt; array with each chat participant's UID, we can write a security rule that gives access only to chat participants.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618183526113%2F-MJgfbPo2.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618183526113%2F-MJgfbPo2.png" alt="rule0.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For unauthenticated requests, accessing the &lt;code&gt;request.auth.uid&lt;/code&gt; variable will be invalid, and the rule will fail. Otherwise, we allow access to chat documents containing the requester's UID in the &lt;code&gt;members&lt;/code&gt; array.&lt;/p&gt;

&lt;p&gt;Unfortunately, there's a tiny glitch with this setup. When we first create a document, the resource doesn't exist, and we have no members array to check, so the rule fails. We need to treat the creation of the document as a special case, which we can do.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618183534701%2FHVQ9BrHI3.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618183534701%2FHVQ9BrHI3.png" alt="rule1.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the updated rule, we allow reads and updates to a chat document when the requester's UID exists in the &lt;code&gt;members&lt;/code&gt; array already recorded in the database. However, when the document gets created, we expect the request to contain a two-entry array with the requester's UID present.&lt;/p&gt;

&lt;p&gt;For nested documents, we need independent rules because each rule applies only to the specified path. The chat rule will have no effect on the subcollection for messages. However, we can still use the &lt;code&gt;members&lt;/code&gt; array in the parent chat document when securing message documents.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618183963207%2FbjCUL7fOq.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618183963207%2FbjCUL7fOq.png" alt="rule2.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;get()&lt;/code&gt; function allows us to read any document in the database. In the above rule, we access the parent chat and verify that the user is a participant - keeping in mind that document access in a security rule counts towards billing as a regular document read made manually.&lt;/p&gt;



&lt;h1&gt;
  
  
  Real-time chat
&lt;/h1&gt;

&lt;p&gt;You can fetch data from the database at any time you like, but since we're trying to build a chat system, we want to see updates happen as soon as possible. If you hang out on your Inbox page, and someone sends you a message, you want to see it right away without having to do a page reload.&lt;/p&gt;

&lt;p&gt;Firestore provides a way to register a listener on a particular query and then calls it with snapshots every time the data affected by that query is modified. It's incredibly convenient as data updates happen in real-time without any additional work to synchronize state between users. This way, the chat behaves more like a chat and less like a laggy email.&lt;/p&gt;

&lt;p&gt;There's one caveat I'd like to mention so that you don't have to spend the time debugging as I did if you were to use a similar implementation as mine. If you build your UI around snapshot listeners, one drawback tends to be the high latency caused by the roundtrip to the database server. When you send a message, it gets sent to the server, the database gets updated, and only then does the snapshot callback trigger with the update. This update time is adequate for a chat experience, but it will be way too sluggish as far as UI responsiveness goes. Luckily, the folks at Firebase provide an excellent latency compensation solution.&lt;/p&gt;

&lt;p&gt;With Firestore, a database change will first be applied locally, then sent to the server. The snapshot listener will trigger before the server has recorded the change, allowing the UI to update quickly. You can check for this in your callback, if you'd like, by looking at the snapshot metadata for the  &lt;a href="https://firebase.google.com/docs/firestore/query-data/listen" rel="noopener noreferrer"&gt;hasPendingWrites&lt;/a&gt; flag. The critical thing to realize is that the local change doesn't communicate with the server and has no way of running the security rules. This isn't a problem if you design the code well, but I didn't, so I had a problem.&lt;/p&gt;

&lt;p&gt;When a recruiter on Ninjobu first sent a message to a candidate, it would create a new chat document and a subcollection with that first message. Then, in a snapshot listener for chat documents, it detected the new chat and immediately read its subcollection of messages to display on the page. If you remember our security rules earlier, we first look at the parent chat document and verify that the user is a member of that chat. In this case, the rule would fail because we queried the collection of messages for a chat document that had only been created locally and wasn't yet recorded in the database server. The solution was simple: display the message locally immediately, but don't query the subcollection from the database until the chat document write finished successfully.&lt;/p&gt;



&lt;h1&gt;
  
  
  But wait, there's more
&lt;/h1&gt;

&lt;p&gt;I wanted to add a visual cue for when the user has new messages. I decided to go with a tiny blip next to the Inbox header to indicate unread messages and mark each chat the user hasn't yet read. For this to work, I needed to add some additional data to the chat document.&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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618158286043%2F_4u8q8cYU.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%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1618158286043%2F_4u8q8cYU.png" alt="blip_window.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I added a new two-entry array, &lt;code&gt;lastSeenTime&lt;/code&gt;, with a timestamp for each chat participant that indicates when they last read the chat. To detect a new message now is simple: we compare the last message timestamp with the new last seen timestamp for a member. When a user opens a chat and retrieves all messages, or a new message is received while the chat is open, we update the last seen timestamp. We can also use these fields to show when the recipient has seen a message, but I'm not too fond of that aspect of chat software, so I didn't add it to Ninjobu.&lt;/p&gt;



&lt;h1&gt;
  
  
  In conclusion
&lt;/h1&gt;

&lt;p&gt;Firebase makes it incredibly easy to build a chat component for your web app. The design in this post may not be the best way to implement such a system, and perhaps it's not even very efficient. Nevertheless, it has worked well for Ninjobu, and I believe it will scale well enough for this platform's needs. If I'm wrong, there will probably be another post with the improvements made.&lt;/p&gt;

&lt;p&gt;There is one thing missing from this chat system that I'd like to build next. When a user receives a message, they have to log in to find out about it. Ideally, we send an email notification to let them know there's a new message waiting for them. Once I build this, perhaps that will be the subject of the next blog post.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>firebase</category>
    </item>
  </channel>
</rss>
