<?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: Virender Chauhan</title>
    <description>The latest articles on DEV Community by Virender Chauhan (@hey-virender).</description>
    <link>https://dev.to/hey-virender</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4056450%2Fcfebdde6-c4f6-47ce-a601-33b0ba63164b.jpg</url>
      <title>DEV Community: Virender Chauhan</title>
      <link>https://dev.to/hey-virender</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hey-virender"/>
    <language>en</language>
    <item>
      <title>Building Real-Time Messaging in a Next.js SaaS with Server-Sent Events</title>
      <dc:creator>Virender Chauhan</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:45:51 +0000</pubDate>
      <link>https://dev.to/hey-virender/building-real-time-messaging-in-a-nextjs-saas-with-server-sent-events-1l8</link>
      <guid>https://dev.to/hey-virender/building-real-time-messaging-in-a-nextjs-saas-with-server-sent-events-1l8</guid>
      <description>&lt;p&gt;I recently shipped &lt;a href="https://do-rent.vercel.app" rel="noopener noreferrer"&gt;Do Rent&lt;/a&gt;, a property management SaaS with landlord, tenant, and admin roles. One requirement stood out early: landlords and tenants needed to message each other in real time, without a full chat infrastructure or a third-party service.&lt;/p&gt;

&lt;p&gt;Here's how I built it with Server-Sent Events (SSE) instead of WebSockets, and what I'd do differently next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SSE over WebSockets
&lt;/h2&gt;

&lt;p&gt;For Do Rent, messages only flow one direction that matters for "real-time" — server to client. The client still sends messages via a normal POST request; the server just needs to push new messages down to whoever's conversation is open. That's exactly the shape SSE is built for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No extra library, no socket server to manage — it's a long-lived HTTP response.&lt;/li&gt;
&lt;li&gt;Auto-reconnect is built into the browser's &lt;code&gt;EventSource&lt;/code&gt; API.&lt;/li&gt;
&lt;li&gt;Works cleanly with Next.js API routes and Vercel's serverless model, where a persistent WebSocket server is awkward to run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebSockets would have been overkill for a one-directional push, and added a whole reconnection/heartbeat protocol I'd have had to write myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;On the server, an API route stays open and streams events as new messages are saved:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&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;send&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="nx"&gt;unknown&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;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &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="nf"&gt;stringify&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="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="c1"&gt;// subscribe to new messages for this conversation&lt;/span&gt;
      &lt;span class="c1"&gt;// and call send() whenever one arrives&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="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keep-alive&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the client, &lt;code&gt;EventSource&lt;/code&gt; subscribes and appends incoming messages to the conversation state:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/conversations/&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;/stream`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;es&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="nf"&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="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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;prev&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Data lives in MongoDB via Prisma, and every write (a new message, a status change) is what triggers the stream to push. Auth and role checks (landlord vs. tenant vs. admin) happen before the stream ever opens, so a user only ever gets events for conversations they're actually part of. Request/response bodies going into the REST APIs are validated with Zod, so a malformed payload never reaches the database layer in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;SSE is a one-way street, which was fine here but won't be if Do Rent ever needs typing indicators or read receipts — those need the client to push small events too, which means falling back to regular POST calls alongside the SSE stream rather than a single connection. It works, but it's not as clean as a bidirectional protocol would be.&lt;/p&gt;

&lt;p&gt;The other thing I underestimated: serverless functions and long-lived connections don't always mix well. On Vercel specifically, you have to be deliberate about timeouts and about closing the stream when a client disconnects, or you end up with dangling connections. Worth reading your host's docs on this before committing to SSE for anything at scale.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/hey-virender/do-rent" rel="noopener noreferrer"&gt;github.com/hey-virender/do-rent&lt;/a&gt;&lt;br&gt;
Live: &lt;a href="https://do-rent.vercel.app" rel="noopener noreferrer"&gt;do-rent.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions if you're weighing SSE vs. WebSockets vs. polling for something similar.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>mongodb</category>
    </item>
  </channel>
</rss>
