Mar 02, 2023 · 11 minutes read

Sending realtime push notifications to your users

Usually a users browser sends a request to a server and the server returns a response. That's how HTTP works. But sometimes we need a little more. What if we have to notify our user about something even after he left our page? Or we need his immediate attention even if he's in another program?

Usually a users browser sends a request to a server and the server returns a response. That's how HTTP works. But sometimes we need a little more. What if we have to notify our user about something even after he left our page? Or we need his immediate attention even if he's in another program?

In this blog post, I'ld like to show you how you can easily send realtime push notifications to your users browser, even further, to a dedicated mobile app on your users phone or tablet. We're going to archive that trough the use of native push notifications:

Disclaimer: I hate push notifications 🙂 Ok that's a hard start, let's say I don't like them when a website asks for permissions without having a good reason. Use them wise and not too much.

My tech-stack

So of course my application is a Laravel 8 app with Jetstream installed, but it also works with older versions without Jetstream. Your app should only have a user management.

To actually send the notifications I've chosen Pusher Beams. I've already worked with Pusher Channels once and it was really easy, so I had a good first impression on their products. With the free plan you can have up to 1.000 concurrent subscribers and for up to 10.000 subscribers you would pay $29 - seems a fair pricing.

For Frontend I've chosen normal Laravel blade views with some Alpine.js magic.

Creating our Beam instance

Before we can start the actual coding we have to create a new Beam instance first. Head over to Pusher Beams and register for a free account if you not already have one. Then create a new Beam instance and give it a name. It will show you a guide on how to install, but you can just ignore and close that.

Preparing the Frontend

To make the push notifications work even when your user has closed your website they use so called service workers. That are javascript files that are registered in their browser and handle the hard part for us.

Beams expect your service worker to be under your-domain.com/service-worker.js. To do that create a new file called service-worker.js in your public folder of your Laravel app with the following contents:

1importScripts("https://js.pusher.com/beams/service-worker.js");

Now that our service provider is there we can move on to the next step:

Asking for permissions and starting the listener

As mentioned above, I don't like it when sites automatically requesting push notification permissions and so I'ld like to have a button the user can click to enable them.

1<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg px-4 py-3" x-data="beamsData()">
2 <x-jet-button @click="enableNotifications">
3 Enable push notifications
4 </x-jet-button>
5</div>

If you are familiar to Alpine.js you already see that I've added a click listener to my button which refers to the enableNotifications method of my passed x-data="beamsData()" data.

So let's take a look at my beamsData method:

1<script type="text/javascript">
2 function beamsData() {
3 return {
4 beamsClient: new PusherPushNotifications.Client({
5 instanceId: 'YOUR_INSTANCE_ID',
6 }),
7 enableNotifications() {
8 this.beamsClient.start()
9 .then(() => console.log('Enabled push notifications 🎉'))
10 .catch(console.error)
11 },
12 }
13 }
14</script>

That isn't much code is it?

First of all replace YOUR_INSTANCE_ID with your instance id (who haven't thought that?). You can find it in your Beams dashboard under Keys in the left navigation. If you click on the button the browser will ask you for permissions to send push notifications.

Understanding how Beams work

Before we can continue you have to understand a little how Pusher Beams are working. We already have registered the users browser to Pusher but now we have to tell Pusher which notifications the user would like to get.

That are the interests. Let's say we have two different notification "channels". blog-posts which should notify the user about new blog posts and blog-comments that should notify the user about new comments for the blog posts.

If our user got registered for both of them, he would get notifications for new blog posts as well as new comments. If he unregisters for blog-comments he will no longer get new notifications for new comments, but he will keep receiving those for new posts.

Registering our interests

I'm using the laravel-notification-channels/pusher-push-notifications Push notification channel and it names the interests after your notifiable models. The interest name for user 1 would be App.Models.User.1, where 1 would be the user id and the rest the path to your model.

Let's modify our enableNotifications method so, that it will automatically registers our users interest:

1enableNotifications() {
2 this.beamsClient.start()
3 .then(() => console.log('Enabled push notifications 🎉'))
4 .then(() => this.beamsClient.addDeviceInterest('App.Models.User.{{ auth()->id() }}'))
5 .catch(console.error)
6}

That will register our user to our interest and we're now ready to send our first notification.

Backend

Sanding notifications is really easy, all we basically have to do is install laravel-notification-channels/pusher-push-notifications and configure our notification:

1composer require laravel-notification-channels/pusher-push-notifications

Head over to your config/services.php file and add the following code:

1'pusher' => [
2 'beams_instance_id' => env('BEAMS_INSTANCE_ID'),
3 'beams_secret_key' => env('BEAMS_SECRET_KEY'),
4],

Then grab your instance id and secret key, again from the Keys entry of your Pusher dashboard and add the equivalent .env file entries:

1BEAMS_INSTANCE_ID=
2BEAMS_SECRET_KEY=

Now let's create a notification:

1php artisan make:notification BlogPostPublishedNotification

Open your newly created notification and add or replace the via methods after your needs:

1public function via($notifiable): array
2{
3 return [PusherChannel::class];
4}

Add a method toPushNotification:

1public function toPushNotification($notifiable): PusherMessage
2{
3 return PusherMessage::create()
4 ->web()
5 ->link('https://felix-schmid.de/blog')
6 ->title('New blog post 🎉')
7 ->body('Felix Schmid has just published a new blog post. Go check it out!');
8}

To learn more about the available options head over to the package's documentation.

Lets try it out:

Since service workers only work on secured connections and localhost make sure to use the localhost domain or secure your site, for example with Valet and valet secure

Open your browser and click the enable push notifications button when not already done. In your console you should see the output

1Enabled push notifications 🎉

I'm using Tinkerwell (affiliate link) for testing but you could create a controller or simply create a tinker session by typing php artisan tinker.

So let's get our first user:

1$user = User::first();

and send the notification:

1$user->notify(new \App\Notifications\BlogPostPublishedNotification());

And voila, you should got an notification like this:

Awesome! 🎉 But sadly we're not finished yet.

Securing our interest-channel

So currently our user could simply open the browser console and subscribe to another users channel by calling something like this:

1this.beamsClient.addDeviceInterest('App.Models.User.4')

Since the App.Models.User.* channel is intended for notifications only to this one user, that isn't good. Sure we're only notifying them about a new blog post currently, but image there would be sensitive data in there.

But luckily Pusher got us covered there as well.

Backend

What we are doing now, is associating the users browser with the user id passed from our application. So we do not need the interests anymore for this use-case since we're sending the notification directly to the user.

To get started, create a new controller. I've called mine BeamsAuthenticationController.

1class BeamsAuthenticationController extends Controller
2{
3 public function __invoke(Request $request)
4 {
5 $beamsClient = new PushNotifications([
6 'instanceId' => config('services.pusher.beams_instance_id'),
7 'secretKey' => config('services.pusher.beams_secret_key'),
8 ]);
9 
10 // Abort unless the logged in user id is the same
11 // as the channel user id he wants to access to
12 abort_unless(auth()->id() == $request->get('user_id'), 401);
13 
14 $beamsToken = $beamsClient->generateToken((string) auth()->id());
15 
16 return response()->json($beamsToken);
17 }
18}

First we create a new instance of our beams client. Then we check if the authenticated user has the same ID as the channel he want's to access and if that is true, we generate a Token and return it as json.

Don't forget to register our new controller and make it only available for authenticated users. I've done that in my routes/web.php file, secured with Sanctum's middleware since I'm using Jetstream which comes with Sanctum build in.

1Route::get('/beams-auth', BeamsAuthenticationController::class)->middleware('auth:sanctum')->name('beams-auth');

Frontend

Now that our backend is ready for authenticating the user, we still have to register our user to his channel when logging in, and unregistering him when logging out.

To do so, we'll use a TokenProvider from Beams API. I'm going to add the token provider as object to my previous defined beamsData:

1beamsClient: new PusherPushNotifications.Client({
2 instanceId: 'YOUR_INSTANCE_ID',
3}),
4beamsTokenProvider: new PusherPushNotifications.TokenProvider({
5 url: '{{ route('beams-auth') }}',
6}),
7// ...

Now with our TokenProvider created, we still have to use it when registering the notifications.

Let's modify our enableNotifications method a little:

1enableNotifications() {
2 this.beamsClient.start()
3 .then(() => this.beamsClient.setUserId('{{ auth()->id() }}', this.beamsTokenProvider))
4 .then(() => console.log('Enabled push notifications 🎉'))
5 .catch(console.error)
6},

Since we do not need to register our user interest any more, I've removed that line of code and replaced it with our authentication process.

When a user now clicks on our button, he will get asked for granting push notification permissions. Then we're setting the user id and trying to authenticate the user via our authentication controller. Beams will handle storing our provided Token for us.

Disassociating the users browser when logging out

To stop sending notifications to the user after logging out we simply have to run:

1this.beamsClient
2 .stop()
3 .catch(console.error);

This will stop sending notifications and will unlink the user id as well.

Stopping the client when user got logged out from outside

There are some situations, where a user gets logged out from your website, without doing that manually. For example with Jetstream the user can logout from all other sessions he's connected with. When doing so, our .stop() call will not get performed. So we somehow need to check if the user associated with beams, it still the logged in user.

Since my application only consists of one view, I'm going to add another method to our beamsData which will get ran when initialising the component.

You should do this is when you initialize the Beams SDK when your web app loads, so not only on a button click or specific page.

1init() {
2 this.beamsClient.getUserId()
3 .then(id => {
4 if(id !== '{{ auth()->id() }}') {
5 return this.beamsClient.stop();
6 }
7 })
8}

Add this method to the HTML element to make sure it will get ran every time you open this page:

1<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg px-4 py-3"
2 x-data="beamsData()"
3 x-init="init">
4 <!-- ... -->
5</div>

So we now have registered our user successful and can send a notification to him.

Sending the notification

First of all, open your user model and add the following line of code:

1public $pushNotificationType = 'users';

Since the package we're using automatically targets notifications after their notifiable class name, we have to modify that logic a little as well:

1public function routeNotificationFor($channel)
2{
3 if($channel === 'PusherPushNotifications'){
4 return (string)$this->id;
5 }
6 
7 $class = str_replace('\\', '.', get_class($this));
8 
9 return $class.'.'.$this->getKey();
10}

If the notification will get send trough the Pusher channel, it will only return the users id, so that Beams know who this notification belongs to.

Make sure to cast the user id to a string every time you pass it to Pusher, otherwise the notification wont get delivered correctly.

And when you now resend your notification as usual, your should get a desktop notification after your needs.

In closing

I know push notifications aren't a need that often, but sometimes they would be really nice. Sending them isn't that hard as expected and I hope I could give you a good impression on how to send them with this long article.

Are they something for your service or website or what do you think about them in general? Let me know on Twitter

Did you enjoy what you’ve read?

Once in a while I send an email with some project updates, article drafts or other cool stuff. I won’t share your email and I won’t let your inbox overflow. Promised.