<?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: Danny Festor</title>
    <description>The latest articles on DEV Community by Danny Festor (@danakin).</description>
    <link>https://dev.to/danakin</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%2F381137%2Fa9701a8c-ce53-48fd-8c1b-d1bc5b78e6e3.jpeg</url>
      <title>DEV Community: Danny Festor</title>
      <link>https://dev.to/danakin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danakin"/>
    <language>en</language>
    <item>
      <title>Laravel's most underrated security feature - What actually is a signed URL, and how do they work?</title>
      <dc:creator>Danny Festor</dc:creator>
      <pubDate>Mon, 20 May 2024 07:47:43 +0000</pubDate>
      <link>https://dev.to/danakin/laravels-most-underrated-security-feature-what-actually-is-a-signed-url-and-how-do-they-work-3768</link>
      <guid>https://dev.to/danakin/laravels-most-underrated-security-feature-what-actually-is-a-signed-url-and-how-do-they-work-3768</guid>
      <description>&lt;p&gt;&lt;a href="https://festor.info/blog/20240520-laravels-most-underrated-security-feature" rel="noopener noreferrer"&gt;This post was first released on my personal blog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Laravel comes with many fantastic security features out of the box and also has many 1st party packages that add security functionality to your application. We have Cross Site Scripting (XSS) protection, Cross Site Request Forgery (CSRF) protection, Roles and Permissions via Gates, SQL Injection protection, but also whole authentication scaffolding (Breeze) including 2 factor authorization (Jetstream) &lt;/p&gt;

&lt;p&gt;One feature is very much overlooked in my opinion. Laravel comes with the ability to create signed urls. In this article I will not only show how to use signed URLs (Laravel makes this trivial), but also look on how this works under the hood so that you can implement the same functionality in other languages (or your own PHP framework).&lt;/p&gt;

&lt;h2&gt;
  
  
  What are signed URLs?
&lt;/h2&gt;

&lt;p&gt;Signed URLs are unguessable URLs you can share with other people without the requirement of being logged in into the system. They are like normal URLs to your app, but have a cryptographically signed hash at the end of the URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# normal url&lt;/span&gt;
http://signed-url.test/file/29

&lt;span class="c"&gt;# signed url&lt;/span&gt;
http://signed-url.test/file/29?signature&lt;span class="o"&gt;=&lt;/span&gt;78e68c73c03586a5705442e86116c5169c8b64e4720b16766eece296ada49d5a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the URLs temper-proof; Change one character in the URL and the hash will not match any longer. AWS S3 uses signed URLs to retrieve private images.&lt;/p&gt;

&lt;h3&gt;
  
  
  What can I actually use these for?
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://laravel.com/docs/11.x/urls#signed-urls" rel="noopener noreferrer"&gt;Laravel Documentation&lt;/a&gt; actually has a fantastic use case for signed URLs: Unsubscribing a user from a newsletter. You get an email, click the unsubscribe button, and you can unsubscribe from the newsletter without logging in or send another confirmation email, because the email address cannot be changed. If you changed the email address the hash would not match any longer.&lt;/p&gt;

&lt;p&gt;Another example would be a file sharing application. Upload the file, and get a signed url you can use to share the file with your friends or on the internet without anyone to register an account.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are signed URLs not?
&lt;/h2&gt;

&lt;p&gt;They are not one time use URLs, at least not by default. There is not default way to invalidate the signed URL, except for changing the application key, which would invalidate every single signed URL. Do not use signed URLs for security critical endpoints, if you don't have a strategy to invalidate the URLs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reddit.com/r/laravel/comments/1blzi84/comment/kw92scd/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button" rel="noopener noreferrer"&gt;There was a discussion on Reddit on this earlier this year.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating Signed URLs
&lt;/h2&gt;

&lt;p&gt;Generating a signed URL in Laravel is super easy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Import the URL facade&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NewsletterController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="nv"&gt;$unsubscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NewsletterUnsubscriptions&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$signedRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;signedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'newsletter.unsubscribe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'unsubscription'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$unsubscription&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="c1"&gt;// http://signed-url.test/newsletter/unsubscriptions/29?signature=78e68c73c03586a5705442e86116c5169c8b64e4720b16766eece296ada49d5a&lt;/span&gt;
        &lt;span class="c1"&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;h2&gt;
  
  
  Invalidating a Signed URL
&lt;/h2&gt;

&lt;p&gt;There is one way to invalidate a signed URL build into Laravel: The ability to add a time limit to a signed URL, making it temporary. When the current timestamp (on the server, of course) is after the timestamp specified in the signed URL, Laravel actually invalidates the URL automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Import the URL facade&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NewsletterController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="c1"&gt;// To add a timestamp to a signed URL, just use the temporarySignedRoute method instead of signedRoute and add a timestamp&lt;/span&gt;
        &lt;span class="nv"&gt;$unsubscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NewsletterUnsubscriptions&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$signedRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;temporarySignedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'newsletter.unsubscribe'&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'unsubscription'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$unsubscription&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="c1"&gt;// http://signed-url.test/file/30?expires=1716185539&amp;amp;signature=fa1fcfa2f47270ef2ef55b20aa5990eacbf6fb11d45984828d7be3f87258cae9&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 the expires parameter? Laravel add that automatically when using temporarySignedRoute. How cool is that?&lt;/p&gt;

&lt;h2&gt;
  
  
  Actually checking the signed URL
&lt;/h2&gt;

&lt;p&gt;Just adding the signature is of course not enough. At no point we are actually confirming that the hash is a legit one. Again, since Laravel supports signed URL out of the box, confirming the hash is super easy as well. It's just changing one line in your route file &lt;code&gt;routes/web.php&lt;/code&gt;, by adding the &lt;code&gt;signed&lt;/code&gt; middleware to your route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/newsletter/unsubscriptions/{unsubscription}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;\App\Http\Controllers\NewsletterController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'unsubscribe'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'newsletter.unsubscribe'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'signed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// add the signed middleware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not get easier than this!&lt;/p&gt;

&lt;h2&gt;
  
  
  So how does it work under the hood?
&lt;/h2&gt;

&lt;p&gt;I firmly believe that just knowing that a feature exists is not enough. I want to know how these things work under the hood. Let's try to recreate the functionality in a basic way&lt;/p&gt;

&lt;h3&gt;
  
  
  Manually generating a signed URL
&lt;/h3&gt;

&lt;p&gt;Manually generating a signed URL requires leveraging PHP's built-in hash function. If you are trying to port this functionality to another language, look up how to hash a string in your language of choice. For instance, &lt;a href="https://pkg.go.dev/crypto/sha256#Sum256" rel="noopener noreferrer"&gt;I know Go does have this built in into the crypto package&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// put the timestamp into a variable so we can reuse it here&lt;/span&gt;
&lt;span class="nv"&gt;$expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$signedRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;temporarySignedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'newsletter.unsubscribe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'unsubscription'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$unsubscription&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;  

&lt;span class="nv"&gt;$route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'newsletter.unsubscribe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'unsubscription'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$unsubscription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'expires'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$expires&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;  
&lt;span class="nv"&gt;$manualRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$route&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;signature='&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.key'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  

&lt;span class="nf"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
    &lt;span class="s1"&gt;'Result from $signedRoute:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$signedRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="s1"&gt;'Result from $manualRoute:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$manualRoute&lt;/span&gt;  
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// "Result from $signedRoute: http://signed-url.test/file/32?expires=1716187120&amp;amp;signature=b9aa919a8f4ce54e89e4270dbadc2bcab5e203304aacd1c4c95c9889f101e3f1"&lt;/span&gt;
&lt;span class="c1"&gt;// "Result from $manualRoute: http://signed-url.test/file/32?expires=1716187120&amp;amp;signature=b9aa919a8f4ce54e89e4270dbadc2bcab5e203304aacd1c4c95c9889f101e3f1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How easy was that? Notice how we signed the hash with our &lt;code&gt;app.key&lt;/code&gt; here. Signing urls with a different app key will result in a different hash&lt;/p&gt;

&lt;h3&gt;
  
  
  Manually Validating the Signature
&lt;/h3&gt;

&lt;p&gt;By default the &lt;code&gt;signed&lt;/code&gt; middleware sends the user to a &lt;code&gt;403&lt;/code&gt; error page. To manually handle URL validation, you can make use of a nice little request helper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;NewsletterUnsubscriptions&lt;/span&gt; &lt;span class="nv"&gt;$unsubscription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="nf"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasValidSignature&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;This helper function will check if the signature has not been tampered with, as well as if it has not yet expired.&lt;/p&gt;

&lt;p&gt;Under the hood of the hasValidSignature function there is actually happening a lot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// get the url without query string&lt;/span&gt;
&lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// get all query parameters except signature in [$key =&amp;gt; $value] form&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;except&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'signature'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// join each array entry to a &amp;amp;key=&amp;amp;value string&lt;/span&gt;
&lt;span class="nv"&gt;$params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// get the signature &lt;/span&gt;
&lt;span class="nv"&gt;$urlsignature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'signature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 

&lt;span class="c1"&gt;// reconstruct the original url without the signature&lt;/span&gt;
&lt;span class="nv"&gt;$original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'?'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;amp;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 

&lt;span class="c1"&gt;// generate a new signature based on the url without the signature&lt;/span&gt;
&lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$original&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.key'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  

&lt;span class="c1"&gt;// compare the signature in the url with the new signature&lt;/span&gt;
&lt;span class="nv"&gt;$equals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'signature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// handle what happens when the hash is not equal&lt;/span&gt;
&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$equals&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// check if the timestamp is expired&lt;/span&gt;
&lt;span class="nv"&gt;$expired&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'expires'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="nv"&gt;$expireTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createFromTimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'expires'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;  
    &lt;span class="nv"&gt;$expired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expireTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="c1"&gt;// handle an expired timestamp&lt;/span&gt;
&lt;span class="nf"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expired&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use hash_equals instead of normal string comparison here, because &lt;a href="https://www.php.net/manual/en/function.hash-equals.php" rel="noopener noreferrer"&gt;it is timing attack secure&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why would you manually handling this?
&lt;/h4&gt;

&lt;p&gt;The other day I had a client with a peculiar request. They wanted password reset to handle a completely invalid URL differently from an expired URL. On an invalid URL the program would throw an error, on an expired URL the app would redirect to the send password reminder page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Homework
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Making signed URLs actually one time usage
&lt;/h3&gt;

&lt;p&gt;So one question remains: How to actually make a signed URL a one time thing? In the example above we took the example from the Laravel documentation. Imagine you want to handle newsletter unsubscriptions. There are several ways you could go about this.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Just add a flag to the NewsletterUnsubscription model; it could be a boolean &lt;code&gt;is_used&lt;/code&gt;, an integer &lt;code&gt;used_count&lt;/code&gt; or a timestamp &lt;code&gt;used_at&lt;/code&gt;. After checking the validity of the signed URL (be it manually, or using Laravel's middleware) just check if the used flag was raised. In case of the integer, you could even restrict file downloads to an arbitrary number.&lt;/li&gt;
&lt;li&gt;Add a flag to the cache of your choice on NewsletterUnsubscription creation. If the cache has an entry, the request is valid, show the page and delete the cache entry. If the cache does not have an entry, the request is invalid.&lt;/li&gt;
&lt;li&gt;... even different solutions, I'm sure you can think of more strategies.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Acknoledgements
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.laravel.com" rel="noopener noreferrer"&gt;Laravel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gemini.google.com/" rel="noopener noreferrer"&gt;Google Gemini&lt;/a&gt;, which I actually used to generate the banner...&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Websockets with Laravel, Soketi and Laravel Echo</title>
      <dc:creator>Danny Festor</dc:creator>
      <pubDate>Sat, 09 Mar 2024 02:32:59 +0000</pubDate>
      <link>https://dev.to/danakin/websockets-with-laravel-soketi-and-laravel-echo-2di3</link>
      <guid>https://dev.to/danakin/websockets-with-laravel-soketi-and-laravel-echo-2di3</guid>
      <description>&lt;p&gt;&lt;a href="https://festor.info/blog/20240309-websockets-with-laravel-soketi-and-laravel-echo" rel="noopener noreferrer"&gt;This article was first released on my portfolio homepage&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everyone knows these pesky notification bell icons, which tell you in real-time whenever an event is triggered that is meant to... well... notify you.&lt;/p&gt;

&lt;p&gt;There are many different ways to implement such a system.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Javascript pulling via &lt;code&gt;setInterval&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;(Laravel Only) Livewire pulling via wire:poll&lt;/li&gt;
&lt;li&gt;Server Sent Events&lt;/li&gt;
&lt;li&gt;Websockets&lt;/li&gt;
&lt;li&gt;Probably more?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With &lt;a href="https://reverb.laravel.com/" rel="noopener noreferrer"&gt;Laravel Reverb&lt;/a&gt; on the radar, releasing on March 12 2024, (as in 4 days, at the time of this article), I wanted to explore how to actually use Websockets; My projects so far have never needed them.&lt;/p&gt;

&lt;p&gt;Up until now, &lt;a href="https://laravel.com/docs/10.x/broadcasting" rel="noopener noreferrer"&gt;Laravel recommends&lt;/a&gt; &lt;a href="https://pusher.com/" rel="noopener noreferrer"&gt;Pusher&lt;/a&gt;, &lt;a href="https://ably.com/" rel="noopener noreferrer"&gt;Ably&lt;/a&gt;, or the self hosted Pusher alternative &lt;a href="https://soketi.app/" rel="noopener noreferrer"&gt;Soketi&lt;/a&gt; in the official documentation. Soketi on the other hand has Laravel baked into their documentation as well. So I did the only sane thing, and explored how to use Websockets with Soketi. I expect I can easily migrate to Reverb, once released.&lt;/p&gt;

&lt;p&gt;In this article I will omit setting up the project, the models, and views. You can find the source code for this article in my &lt;a href="https://github.com/DannyFestor/-Youtube--Notification-Laravel-Soketi/" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt;. Follow the commit history to see changes to the various files. I used Laravel Breeze with the Alpine.js template to get Alpine and Tailwind.css installed and set up for me.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating Laravel Sail
&lt;/h3&gt;

&lt;p&gt;Since Soketi is a self hosted solution, you will have to install it somewhere. The easiest way is by using Docker, so I went with Laravel Sail for this tutorial. Laravel Sail is not meant for production, but it is good enough for Localhost development and testing.&lt;/p&gt;

&lt;h4&gt;
  
  
  Add Soketi to Docker
&lt;/h4&gt;

&lt;p&gt;First, you need to add the Soketi image to your docker-compose-file. Obviously skip this step when not using docker. As per &lt;a href="https://docs.soketi.app/getting-started/installation/laravel-sail-docker" rel="noopener noreferrer"&gt;their documentation&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# docker-compose.yml&lt;/span&gt;
services:
    &lt;span class="c"&gt;# ...&lt;/span&gt;
    soketi:  
        image: &lt;span class="s1"&gt;'quay.io/soketi/soketi:latest-16-alpine'&lt;/span&gt;  
        environment:  
            SOKETI_DEBUG: &lt;span class="s1"&gt;'1'&lt;/span&gt;  
            SOKETI_METRICS_SERVER_PORT: &lt;span class="s1"&gt;'9601'&lt;/span&gt;  
        ports:  
            - &lt;span class="s1"&gt;'${SOKETI_PORT:-6001}:6001'&lt;/span&gt;  
            - &lt;span class="s1"&gt;'${SOKETI_METRICS_SERVER_PORT:-9601}:9601'&lt;/span&gt;  
        networks:  
            - sail
networks:
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also need to update your environment file. Soketi uses the Pusher protocol, so you need to update relevant Pusher parts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BROADCAST_DRIVER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pusher &lt;span class="c"&gt;# was log&lt;/span&gt;

&lt;span class="c"&gt;# As set in soketi&lt;/span&gt;
&lt;span class="nv"&gt;PUSHER_APP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app-id
&lt;span class="nv"&gt;PUSHER_APP_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app-key  
&lt;span class="nv"&gt;PUSHER_APP_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app-secret 
&lt;span class="c"&gt;# Host is only soketi in docker. If you installed soketi locally, use 127.0.0.1, or the IP of your server&lt;/span&gt;
&lt;span class="nv"&gt;PUSHER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;soketi  
&lt;span class="nv"&gt;PUSHER_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6001  
&lt;span class="nv"&gt;PUSHER_SCHEME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http

&lt;span class="c"&gt;# Browser does not run in docker, so using the value set in PUSHER_HOST won't work&lt;/span&gt;
&lt;span class="nv"&gt;VITE_PUSHER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt; &lt;span class="c"&gt;# was "${PUSHER_HOST}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installing the needed packages
&lt;/h3&gt;

&lt;h4&gt;
  
  
  PHP side
&lt;/h4&gt;

&lt;p&gt;You will have to install the Pusher Channels SDK for Laravel to be able to use the pusher driver&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require pusher/pusher-php-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the driver, you need to tweak the configuration to enable broadcasting by uncommenting the BroadcastServiceProvider in &lt;code&gt;config/app.php&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/app.php&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="s1"&gt;'providers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ServiceProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;defaultProviders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nc"&gt;App\Providers\BroadcastServiceProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Enable the Service Provider, was commented out&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Frontend (Javascript) Side
&lt;/h4&gt;

&lt;p&gt;You also need the Javascript for you client side code&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; laravel-echo pusher-js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing the packages you also have to uncomment the broadcasting related code in &lt;code&gt;resources/js/bootstrap.js&lt;/code&gt;. Laravel makes it really easy, the code is already there!&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;import&lt;/span&gt; &lt;span class="nx"&gt;Echo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;laravel-echo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Pusher&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pusher-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Pusher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Pusher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Echo&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;Echo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;  
    &lt;span class="na"&gt;broadcaster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pusher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&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_PUSHER_APP_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="p"&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_PUSHER_APP_CLUSTER&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mt1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="na"&gt;wsHost&lt;/span&gt;&lt;span class="p"&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_PUSHER_HOST&lt;/span&gt; &lt;span class="p"&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_PUSHER_HOST&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ws-&lt;/span&gt;&lt;span class="p"&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_PUSHER_APP_CLUSTER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pusher.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="na"&gt;wsPort&lt;/span&gt;&lt;span class="p"&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_PUSHER_PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="na"&gt;wssPort&lt;/span&gt;&lt;span class="p"&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_PUSHER_PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="c1"&gt;// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',  &lt;/span&gt;
    &lt;span class="na"&gt;forceTLS&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="c1"&gt;// I deactivated TLS for local testing&lt;/span&gt;
    &lt;span class="na"&gt;enabledTransports&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="s1"&gt;ws&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="s1"&gt;wss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  
    &lt;span class="na"&gt;encrypted&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="c1"&gt;// Soketi documentation adds this line&lt;/span&gt;
    &lt;span class="na"&gt;disableStats&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="c1"&gt;// as well as this line&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Broadcasting an event to your users
&lt;/h3&gt;

&lt;p&gt;Broadcasting channels for Websockets live in their own routing file. You are probably used to adding routes to &lt;code&gt;routes/web.php&lt;/code&gt; or &lt;code&gt;routes/api.php&lt;/code&gt; and have wondered what the other files were used for. Well, it's time to actually activate &lt;code&gt;route/channels.php&lt;/code&gt; and add our Websocket Channel! The channel will actually be a private channel, which is scoped to your current user. You can actually add authorization to channels this way!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/channels.php&lt;/span&gt;
&lt;span class="c1"&gt;// The channel always receives the current user, as well as optional arguments. We will connect to notifications.[user_id], which the callback receives as an argument as well&lt;/span&gt;
&lt;span class="nc"&gt;Broadcast&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'notifications.{userId}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$userId&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="nv"&gt;$userId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// users can only connect to this channel if true is returned&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Laravel, information is broadcasted over events, so let us create one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:event UserNotificationEvent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event will receive a user id, because we scoped the channel to a user in the snippet above, as well as a notification (or whatever data you want to send).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Events/UserNotificationEvent.php&lt;/span&gt;

&lt;span class="c1"&gt;// do not forget to implement the `Illuminate\Contracts\Broadcasting\ShouldBroadcast` interface, otherwise your event will not be broadcasted to your websocket server&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserNotificationEvent&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldBroadcast&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithSockets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="c1"&gt;// receive the data needed for the event. all public properties of the class will be sent on the event&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&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="nv"&gt;$userId&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;Notification&lt;/span&gt; &lt;span class="nv"&gt;$notification&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;function&lt;/span&gt; &lt;span class="n"&gt;broadcastOn&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Channel&lt;/span&gt; &lt;span class="c1"&gt;// update return type from array to Channel  &lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="c1"&gt;// broadcast over a private channel to the user!&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;PrivateChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'notifications.'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;userId&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;For test purposed I also added a console command to quickly create notifications. This command will also dispatch the event. Usually, you would to everything in the command in your admin panel, but for a small test, a console command has to be sufficient.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:command MakeNotificationCommand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MakeNotificationCommand&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="c1"&gt;// run with php artisan make:notification, which accepts a number&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'make:notification {num=1}'&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;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="c1"&gt;// always prevent shenanigans by your users, even in a test application. I restricted input to 1-100 notifications... &lt;/span&gt;
        &lt;span class="nv"&gt;$num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'num'&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="c1"&gt;// get all users&lt;/span&gt;
        &lt;span class="c1"&gt;// typically, you would select users in your admin panel&lt;/span&gt;
        &lt;span class="nv"&gt;$userIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// generate notifications. I'm using a factory here to create random data, but usually you would receive title, content etc through your admin panel&lt;/span&gt;
        &lt;span class="nv"&gt;$notifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  

        &lt;span class="nv"&gt;$notifications&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Notification&lt;/span&gt; &lt;span class="nv"&gt;$notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
            &lt;span class="k"&gt;foreach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// add an entry into my notification_user pivot table for each user&lt;/span&gt;
                &lt;span class="nv"&gt;$insert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$notification&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="p"&gt;[&lt;/span&gt;  
                        &lt;span class="s1"&gt;'notification_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$notification&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
                        &lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$userId&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="nv"&gt;$chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
                &lt;span class="nc"&gt;NotificationUser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$insert&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

                &lt;span class="k"&gt;foreach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunk&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// finally, dispatch the event to every user, passing the notification information&lt;/span&gt;
                    &lt;span class="nc"&gt;UserNotificationEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$notification&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything went correctly (did you remember to implement the ShouldBroadcast interface? I was trying to figure that one out the longest time 😅)&lt;/p&gt;

&lt;h3&gt;
  
  
  Receiving events on the client side
&lt;/h3&gt;

&lt;p&gt;The sending is good and well, but we want to actually see the notifications on the client side as well. I have the views for the notification bell, the unread count, and a notification list all ready. They are available in the git repository if you need inspiration. To receive notifications, you just have to connect to your channel route, and then handle the event!&lt;/p&gt;

&lt;p&gt;First, let us get all existing notifications, and pass them to the user. Usually you would do this in a controller, or even a view service provider (since you probably want to show the notification bell on every page), but the &lt;code&gt;routes/web.php&lt;/code&gt; file will do just fine for this demo&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/web.php&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// get all new notifications for the current user which (as in, the seen_at column on the notification_user pivot table is null)&lt;/span&gt;
    &lt;span class="nv"&gt;$notifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'seen_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;  
        &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="s1"&gt;'notifications'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$notifications&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'verified'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/notifications'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// mark all notifications as seen&lt;/span&gt;
    &lt;span class="nc"&gt;\App\Models\NotificationUser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="o"&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="s1"&gt;'seen_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;  

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'ok'&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="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'notifications'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// resources/views/dashboard.blade.php&lt;/span&gt;
&lt;span class="c1"&gt;// add the notification component, if you have created one, and pass the data&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$notifications&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I am using &lt;a href="https://alpinejs.dev" rel="noopener noreferrer"&gt;Alpine.js&lt;/a&gt; for displaying the content. I expect the code can be easily ported to any Javascript framework and even vanilla JS. Just update the view logic, and bring the content from the init() method to your frameworks mounted hook (onMounted on Vue3, useEffect on React etc).&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;!-- resources/views/components/notification.blade.php --&amp;gt;&lt;/span&gt;
@props(['user', 'notifications', 'eventName'])  

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"notification"&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="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"markRead"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative w-6 h-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;  
        &lt;span class="nt"&gt;&amp;lt;x-bell&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;  
        &lt;span class="nt"&gt;&amp;lt;x-count&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;x-notification-dropdown&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;  

@push('scripts')  
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;  
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alpine:init&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="nx"&gt;Alpine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notification&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="c1"&gt;// this object will hold all notifications received at page load&lt;/span&gt;
            &lt;span class="na"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$notifications&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  

            &lt;span class="c1"&gt;// a computed property to get the current notification count&lt;/span&gt;
            &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;count&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
            &lt;span class="p"&gt;},&lt;/span&gt;


            &lt;span class="c1"&gt;// on click of the bell, make a post request to mark all notifications as read. you probably want a better implementation, because you probably want the notifications to show in a popup. I leave the implementation to you&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;markRead&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{{ route(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;) }}&lt;/span&gt;&lt;span class="dl"&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;data&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="s1"&gt;ok&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notifications&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="p"&gt;},&lt;/span&gt;

            &lt;span class="c1"&gt;// this is where we connect to the websocket. Do this on your mounted hook, be it document.addEvent('DOMContentLoaded') in vanilla JS, onMounted in Vue3, or useEffect() in React... &lt;/span&gt;
            &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
                &lt;span class="c1"&gt;// connect to our socket. notice that we pass the user id here, just as we expected in routes/channels.php&lt;/span&gt;
                &lt;span class="nx"&gt;Echo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;private&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications.{{ $user-&amp;gt;id }}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="c1"&gt;// the event name we are listening to must exactly match the short/unqualified class name of our event (so, without namespace or the ::class suffix)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UserNotificationEvent&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;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="k"&gt;if &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;notification&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="p"&gt;{&lt;/span&gt;  
                            &lt;span class="c1"&gt;// if the event has a notification property, push the notification on the notifications object&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;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;notification&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;span class="p"&gt;});&lt;/span&gt;  
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;  
@endpush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My demo implementation is just an ordered list of all notifications. You probably want to pop it up on click, or something similar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Laravel makes broadcasting to webcasts as well as receiving the events really easy. Most of the functionality comes out of the box and only a few files have to be adjusted to get started.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bonus: Possible improvements as homework
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The frontend part is really bare bones. Show it some love (and update us how you did it!)&lt;/li&gt;
&lt;li&gt;Show a notification bell throughout your application. Try adding a &lt;a href="https://laravel.com/docs/10.x/views#sharing-data-with-all-views" rel="noopener noreferrer"&gt;View Composer&lt;/a&gt; to accomplish this&lt;/li&gt;
&lt;li&gt;Try playing with public and presence channels as well. Laravels Broadcasting documentation explains what these are&lt;/li&gt;
&lt;li&gt;Maybe build a small chat app that allows guest users to communicate &lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Bonus 2: Getting really cheeky using Reflection
&lt;/h4&gt;

&lt;p&gt;Since we have to pass the short class name to the event, it might make sense to actually let PHP handle the event name. This way you can rename the event (using your IDE/language servers rename function (Intelephense etc), to automatically update references), without digging into the Javascript side again&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/web.php&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
    &lt;span class="nv"&gt;$notifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'seen_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
    &lt;span class="nv"&gt;$eventName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\ReflectionClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\App\Events\UserNotificationEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getShortName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// get the short class name. This will automatically update, should you rename the event via IDE or language server &lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;  
        &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="s1"&gt;'notifications'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="s1"&gt;'eventName'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// pass the eventName to your view. Don't forget to update the views to use the variable, instead of hardcoding the event name3&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="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'verified'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://laravel.com" rel="noopener noreferrer"&gt;Laravel&lt;/a&gt;, &lt;a href="https://laravel.com/docs/10.x/starter-kits" rel="noopener noreferrer"&gt;Laravel Breeze&lt;/a&gt;, &lt;a href="https://reverb.laravel.com/" rel="noopener noreferrer"&gt;Laravel Reverb&lt;/a&gt;, &lt;a href="https://laravel.com/docs/10.x/broadcasting#client-side-installation" rel="noopener noreferrer"&gt;Laravel Echo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://soketi.app/" rel="noopener noreferrer"&gt;Soketi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pusher.com/" rel="noopener noreferrer"&gt;Pusher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heroicons.com/" rel="noopener noreferrer"&gt;Heroicons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://alpinejs.dev/" rel="noopener noreferrer"&gt;Alpine.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;tailwindcss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/DannyFestor/-Youtube--Notification-Laravel-Soketi" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>websockets</category>
      <category>webdev</category>
      <category>php</category>
      <category>laravel</category>
    </item>
  </channel>
</rss>
