---
title: "iOS & iPadOS PWA Notifications"
url: https://monogram.io/blog/notifications-from-ios-and-ipados-pwas
description: "How to add notifications to your iOS & iPadOS progressive web apps (PWA)."
date: 2024-05-01
authors: ["Julien"]
---

# Add Notifications to your iOS PWA

**Published:** May 01, 2024 | **Authors:** Julien, Full-Stack Developer | **Categories:** Engineering

---

How to add notifications to your iOS & iPadOS progressive web apps (PWA).

Thanks to a mix of anti-monopoly legislation and a commitment to open standards, the popularity of Progressive Web Apps (PWAs) has been steadily climbing. In a significant leap forward, Apple has recently enabled the delivery of native iOS notifications for PWAs. This development is a game-changer, removing one of the key barriers that often push developers towards publishing apps via the Apple App Store rather than the web.

This article will guide you through the process of integrating iOS push notifications into your PWA. We're diving into this using Typescript, Next.js + Server Actions, and Vercel for hosting & db provider, but the concepts are easily transferable to any javascript framework.

[Demo](https://ios-pwa.vercel.app/) | [GitHub](https://github.com/monogramdesign/ios-pwa-demo)

![Image](https://cdn.sanity.io/images/zep746qw/production/33c66490807dbc6540a002d498de27232e8cf07f-3635x2401.png)

## Setup

> For the purpose of this demo, we’ll be starting from a blank next.js template.

First, install dependencies. The `web-push` npm library comes in handy for enabling server-side web push notifications.

```bash
pnpm i web-push
```

For those of you coding in TypeScript, don't forget to grab the type definitions as well:

```bash
pnpm i -D @types/web-push
```

Next on our agenda is generating a [VAPID](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) key pair, crucial for authenticating our push notifications. The `web-push` package simplifies this with a handy script:

```bash
pnpx web-push generate-vapid-keys
```

## Getting Started

Time to introduce some environment variables. Feel free to share your public key and contact email with the client side, but keep your private key under wraps. The contact email is there to provide a direct line in case the push service needs to reach out about any issues.

```
NEXT_PUBLIC_VAPID_PUBLIC_KEY="YOUR_PUBLIC_KEY_HERE"
VAPID_PRIVATE_KEY="YOUR_PRIVATE_KEY_HERE"
VAPID_MAILTO="your-email@example.com"
```

A manifest file is a must-have for any Progressive Web App (PWA). Here's a basic one to start with, located at `public/site.webmanifest`. Feel free to jazz it up with icons and theme colors as you like. More on what you can include can be found [here](https://developer.mozilla.org/en-US/docs/Web/Manifest)

```json
{
  "name": "My Cool PWA",
  "short_name": "My Cool PWA",
  "icons": [],
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "display": "standalone"
}
```

Don't forget to link to your manifest file in the <head> of your HTML. If you're using Next.js like we are, you can leverage its built-in metadata features by adding this to your root layout in `app/layout.tsx`:

```
export const metadata: Metadata = {
  title: "My Cool PWA",
  appleWebApp: { capable: true },
  manifest: "/site.webmanifest",
};
```

## Registering Service Worker and Notifications

For simplicity, we'll use short-lived browser cookies to store our subscription info client-side. Imagine this data flowing from your user database instead.

Let's set up our service worker in `public/service-worker.js` to listen for push notifications:

```
self.addEventListener("push", async (event) => {
  const { title, body } = await event.data.json();
  self.registration.showNotification(title, { body });
});
```

Our app features a page for subscribing to notifications and another to trigger them. Here's how our home server component looks:

```
import Register from "@/components/register";
import { cookies } from "next/headers";
import Registered from "@/components/registered";

export default async function Home() {
  const subId = cookies().get("subId")?.value;

  return (
    <main>
      {subId ? <Registered subId={subId} /> : <Register />}
    </main>
  );
}
```

Now, let's dive into creating the `<Register>` component, which will be a client-side component to interact with the browser APIs.

First, we register our service worker on page load and keep it in our state:

```
const [swRegistration, setSwRegistration] =
    useState<ServiceWorkerRegistration | null>(null);

  useEffect(() => {
    (async function registerServiceWorker() {
      if (!("serviceWorker" in navigator)) {
        return;
      }

      const sw = await navigator.serviceWorker.register("/service-worker.js");
      setSwRegistration(sw);
    })();
  }, []);
```

In the same client component, we’ll wire up a button that creates a subscription when it receives an `onClick` event. We’ll then call a Next.js server action to save that subscription server-side (replace with an API call if needed).

```
const onClick = async () => {
    const result = await window.Notification.requestPermission();
    console.log(result);

    if (result !== "granted" || swRegistration === null) {
      return alert("You must grant permission to send notifications.");
    }

    const subscription = await swRegistration.pushManager.subscribe(
      pushSubscriptionOpts
    );

    await saveSubscription(subscription);
  };

  return (
    <button onClick={onClick}>
      Subscribe
    </button>
  );
```

`pushSubscriptionOpts` helper is declared in our `utils.ts` file:

```
function urlBase64ToUint8Array(b64String: string) {
  const base64 = b64String
    .padEnd(b64String.length + ((4 - (b64String.length % 4)) % 4), "=")
    .replace(/-/g, "+")
    .replace(/_/g, "/");
  const rawData = Buffer.from(base64, "base64").toString("binary");
  return Uint8Array.from(rawData, (char) => char.charCodeAt(0));
}

if (!process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY) {
  throw new Error("NEXT_PUBLIC_VAPID_PUBLIC_KEY not set");
}

export const pushSubscriptionOpts: PushSubscriptionOptionsInit = {
  applicationServerKey: urlBase64ToUint8Array(
    process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY
  ),
  userVisibleOnly: true,
};
```

## Server actions

We’re using a single `actions.ts` file. We’ll start by adding this to the start of the file. We’re using Vercel KV to store subscription info but feel free to swap this part out with any database provider.

```
"use server";

import { cookies } from "next/headers";
import { kv } from "@vercel/kv";
import webpush from "web-push";

if (
  !process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY ||
  !process.env.VAPID_PRIVATE_KEY ||
  !process.env.VAPID_MAILTO
) {
  throw new Error("VAPID keys not set");
}

webpush.setVapidDetails(
  `mailto:${process.env.VAPID_MAILTO}`,
  process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
);
```

Now we can add our `saveSubscription` action that we called from the UI earlier. This simply saves the subscription info as JSON and sets the response cookies for the client to use.

```
export async function saveSubscription(subscription: PushSubscription) {
  const subId = crypto.randomUUID();

  await kv.hset("subscriptions", { [subId]: subscription });

  cookies().set("subId", subId);
}
```

At this point, `<Register>` should be rendering since we have subId from the response cookie. We can now trigger notifications for our subscription. Add the following code for your `<Register>` server component.

```
import { sendNotification } from "@/actions";

type Props = { subId: string };

export default async function Registered({ subId }: Props) {
  return (
    <>
      <form action={sendNotification.bind(null, subId)}>
        <button type="submit">
          Send Notification
        </button>
      </form>
      <code>{subId}</code>
    </>
  );
}
```

Our `sendNotification` server action looks like this:

```
export async function sendNotification(subId: string) {
  const subscription = await kv.hget("subscriptions", subId);
  if (!subscription) return;

  const { statusCode, body, headers } = await webpush.sendNotification(
    subscription as webpush.PushSubscription,
    JSON.stringify({
      title: "Hello, from Monogram!",
      body: "👋 Lorem ipsum dolor sit amet, consectetur adipiscing elit",
    })
  );

  console.log({ statusCode, headers, body });
}
```

## Device testing

If you’ve been following on the same tech stack, you should be able to push to Vercel and add the app as a PWA on your iOS device. Remember to create your KV database (if using it) and set your environment variables in your hosting environment.

Since iOS PWA notifications are still in beta, notifications are turned off in Safari by default. Enable them by going to Settings > Safari > Advanced > Experimental Features. Make sure “Notifications” is toggled on.

## Resources

[https://devtails.xyz/@adam/how-to-setup-web-push-notifications-in-ios-safari](https://devtails.xyz/@adam/how-to-setup-web-push-notifications-in-ios-safari)

[https://www.npmjs.com/package/web-push](https://www.npmjs.com/package/web-push)

next.js docs [https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)

## Contact Us

[Build what comes next](https://monogram.io/#contact) | Email: [hello+llm@monogram.io](mailto:hello+llm@monogram.io)

Provide your contact details and claim a time to meet. We'll help you do the rest.

Monogram is an Applied AI Studio based in Atlanta that designs and deploys production AI systems — connecting models, data, workflows, and enterprise software to automate real work.

The contact form collects: name, email, company, estimated budget (USD), a project description, and how you heard about Monogram.

Not sure where to start? Tell us what you're trying to automate or build — we'll help you figure out where AI fits.


---

**More content:** [Home](/.md) | [About](/about.md) | [Work](/work.md) | [Engineering](/engineering.md)

Monogram helps companies, organizations, and firms adopt AI by designing and building proof of concepts (POCs), MVPs, intelligent workflows, and production-ready AI applications.

Website designed and developed by [Monogram](https://monogram.io)