<?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: Manikandan V</title>
    <description>The latest articles on DEV Community by Manikandan V (@manikandan_v_549bdba6219b).</description>
    <link>https://dev.to/manikandan_v_549bdba6219b</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%2F3602306%2F00497ede-00a9-4e20-b0d1-8c5ed4e55493.png</url>
      <title>DEV Community: Manikandan V</title>
      <link>https://dev.to/manikandan_v_549bdba6219b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manikandan_v_549bdba6219b"/>
    <language>en</language>
    <item>
      <title>I wrestled AppSync WebSockets for 4 hours so you don’t have to (React + GraphQL Subscriptions)</title>
      <dc:creator>Manikandan V</dc:creator>
      <pubDate>Sat, 08 Nov 2025 09:06:35 +0000</pubDate>
      <link>https://dev.to/manikandan_v_549bdba6219b/i-wrestled-appsync-websockets-for-4-hours-so-you-dont-have-to-react-graphql-subscriptions-obi</link>
      <guid>https://dev.to/manikandan_v_549bdba6219b/i-wrestled-appsync-websockets-for-4-hours-so-you-dont-have-to-react-graphql-subscriptions-obi</guid>
      <description>&lt;p&gt;You know that moment when “it should be simple” puts on a clown wig and honks a tiny horn? That was me, trying to wire a React client to AWS AppSync for real-time GraphQL updates. Four hours, three coffees, and one thousand &lt;code&gt;console.log&lt;/code&gt;s later… it works. Here’s the story, the fixes, and enough code breadcrumbs to save Future You from my timeline.&lt;/p&gt;

&lt;p&gt;Spoiler for the curious: I tried Cursor and a couple other AI copilots mid-chaos—useful for rubber-ducking, but they didn’t splice the wires for me. I also started with &lt;code&gt;subscription-transport-ws&lt;/code&gt; (RIP, old friend), then eventually ditched libs and went full native &lt;code&gt;WebSocket&lt;/code&gt;. That’s when the fog lifted.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; React client subscribes to &lt;code&gt;onPublish&lt;/code&gt; and renders messages in real time.&lt;br&gt;
&lt;strong&gt;Reality:&lt;/strong&gt; Handshake rituals, headers, base64, keep-alives, reconnects, and a protocol that likes its messages &lt;em&gt;just so&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The working component is intentionally verbose with logs, state, and safety rails, because “silent failure” is so last season.&lt;/p&gt;




&lt;h2&gt;
  
  
  What finally clicked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Build the &lt;strong&gt;right&lt;/strong&gt; AppSync Real-Time URL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Start with your &lt;strong&gt;HTTP&lt;/strong&gt; endpoint (env: &lt;code&gt;VITE_APPSYNC_HTTP_ENDPOINT&lt;/code&gt; or &lt;code&gt;VITE_APPSYNC_API_URL&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Swap &lt;code&gt;appsync-api&lt;/code&gt; → &lt;code&gt;appsync-realtime-api&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Append &lt;code&gt;?header=&amp;lt;base64(authHeader)&amp;gt;&amp;amp;payload=&amp;lt;base64({})&amp;gt;&lt;/code&gt; where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;authHeader = { host, "x-api-key": &amp;lt;your-api-key&amp;gt; }&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Yes, &lt;strong&gt;standard&lt;/strong&gt; base64—&lt;strong&gt;not&lt;/strong&gt; base64url. That one detail ate 20 minutes of my life.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Speak the protocol’s love language
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;graphql-ws&lt;/code&gt; subprotocol. The dance goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;connection_init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Wait for &lt;code&gt;connection_ack&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start&lt;/code&gt; with:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; (unique per subscription),&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload.data&lt;/code&gt; as a &lt;strong&gt;stringified JSON&lt;/strong&gt; of your GraphQL query,&lt;/li&gt;
&lt;li&gt;Optional &lt;code&gt;extensions.authorization&lt;/code&gt; with &lt;code&gt;{ host, "x-api-key" }&lt;/code&gt; (handy with API key auth).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Miss any of those and AppSync just stares into the middle distance.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Parse &lt;code&gt;data&lt;/code&gt; like it’s nested Russian dolls
&lt;/h3&gt;

&lt;p&gt;AppSync will send &lt;code&gt;type: "data"&lt;/code&gt; and the payload’s &lt;code&gt;data&lt;/code&gt; might itself be a JSON string. Parse it, then read &lt;code&gt;onPublish&lt;/code&gt;. Defensive parsing here means fewer 3AM mysteries.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Respect the keep-alives
&lt;/h3&gt;

&lt;p&gt;You’ll get &lt;code&gt;ka&lt;/code&gt; / &lt;code&gt;connection_keep_alive&lt;/code&gt;. Flip a heartbeat flag and update your status UI so you know the line is alive. Future-you will send you a thank-you pizza.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Be nice to your users (and yourself) with reconnection
&lt;/h3&gt;

&lt;p&gt;Exponential backoff up to a limit. Clear timeouts on unmount. Send a &lt;code&gt;stop&lt;/code&gt; when you’re done. Leave the room tidier than you found it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The subscription query (tiny but mighty)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;subscription&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OnPublish&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="n"&gt;onPublish&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;body&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wired straight into the &lt;code&gt;start&lt;/code&gt; message as stringified JSON under &lt;code&gt;payload.data&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Code crumbs you’ll likely reuse
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build the Real-Time URL (HTTP → WSS with auth headers)
&lt;/h3&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;buildWebSocketUrl&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;httpEndpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&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;VITE_APPSYNC_HTTP_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&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;VITE_APPSYNC_API_URL&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;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&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;VITE_APPSYNC_API_KEY&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;host&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;httpEndpoint&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;host&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;realtimeHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appsync-api&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;appsync-realtime-api&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;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;btoa&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;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;btoa&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`wss://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;realtimeHost&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/graphql?header=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;payload=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;btoa(JSON.stringify({}))&lt;/code&gt; producing &lt;code&gt;e30=&lt;/code&gt; is correct. Trust the emptiness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opening handshake + starting the subscription
&lt;/h3&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;ws&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;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildWebSocketUrl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;graphql-ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&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;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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="nf"&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;connection_init&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`sub_&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="s2"&gt;`&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;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&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;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="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ON_PUBLISH_SUBSCRIPTION_QUERY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&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;ws&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;msg&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&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;connection_ack&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;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startMessage&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;extensions.authorization&lt;/code&gt; block was the missing puzzle piece for me with API key auth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Receiving data (defensive parsing)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&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;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPublish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;onPublish&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;parsed&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;onPublish&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;onPublish&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onPublish&lt;/span&gt;&lt;span class="p"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handles both the “stringified JSON” and “already-an-object” variants you might see.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I tried before it worked (a small gallery of almosts)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Libraries first:&lt;/strong&gt; Reached for &lt;code&gt;subscription-transport-ws&lt;/code&gt; out of habit. It’s deprecated and doesn’t map cleanly to AppSync’s expectations. After a few protocol mismatches, I bailed and went native &lt;code&gt;WebSocket&lt;/code&gt;. Painful? A bit. Clarifying? Absolutely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI copilots (Cursor + others):&lt;/strong&gt; Great at reshaping code and suggesting patterns, but the devil here lives in undocumented quirks: vanilla base64, exact &lt;code&gt;payload.data&lt;/code&gt; shape, and when to send &lt;code&gt;start&lt;/code&gt;. The final mile needed eyeballs on raw frames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base64url headers:&lt;/strong&gt; AppSync wants plain base64. Using base64url gave me a very polite nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting before &lt;code&gt;connection_ack&lt;/code&gt;:&lt;/strong&gt; It’s tempting. Resist. The order matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping UI telemetry:&lt;/strong&gt; A tiny panel for connection state, heartbeat, subscription ID, and last error pays for itself immediately.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The full working component
&lt;/h2&gt;

&lt;p&gt;I left the logs noisy and the UI honest: connection state, subscription status, heartbeat, reconnection attempts, and a scrollable message list. Copy it, prune it, or keep it loud until prod—your call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick checklist (pin this)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wss://...appsync-realtime-api.../graphql?header=&amp;lt;b64&amp;gt;&amp;amp;payload=&amp;lt;b64&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt; Subprotocol: &lt;code&gt;"graphql-ws"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; &lt;code&gt;connection_init&lt;/code&gt; → wait for &lt;code&gt;connection_ack&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; &lt;code&gt;start&lt;/code&gt; with &lt;code&gt;payload.data = JSON.stringify({ query })&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Optional &lt;code&gt;extensions.authorization&lt;/code&gt; (&lt;code&gt;host&lt;/code&gt;, &lt;code&gt;x-api-key&lt;/code&gt;) for API key flows&lt;/li&gt;
&lt;li&gt; Parse &lt;code&gt;payload.data&lt;/code&gt; defensively&lt;/li&gt;
&lt;li&gt; Handle &lt;code&gt;ka&lt;/code&gt;/keep-alive&lt;/li&gt;
&lt;li&gt; Exponential reconnect with limits&lt;/li&gt;
&lt;li&gt; Clean up: &lt;code&gt;stop&lt;/code&gt; + &lt;code&gt;close&lt;/code&gt; on unmount&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Real-time isn’t hard; it’s picky. Once you respect the handshake and payload shapes, AppSync hums along nicely. The “native WS first principles” pass taught me more than a stack of blog posts, and the extra status UI + chatty logs turned a cryptic failure into a solvable puzzle.&lt;/p&gt;

&lt;p&gt;If you’re wiring this up with your own mutations and resolvers, drop a note about your stack and any quirks you hit. There’s always one more header, one more “tiny” detail, and one more developer who could use breadcrumbs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Yeah Yeah!!, AI helped me rewrite this experience. but, the app, the debugging, and the code are very real.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>graphql</category>
      <category>devbugsmash</category>
      <category>react</category>
    </item>
  </channel>
</rss>
