Skip to main content

Overview

Zen Nurture uses the Web Push protocol to deliver reminder notifications to users’ devices. This API allows you to manage push subscriptions and send notifications.

Push Subscription Schema

interface PushSubscription {
  _id: Id<"pushSubscriptions">;
  userId: string;
  endpoint: string; // Web Push endpoint URL
  keys: {
    p256dh: string; // Encryption key
    auth: string;   // Authentication secret
  };
  createdAt: string;
}

Subscribe to Push Notifications

Register a device for push notifications.
import { useMutation } from "convex/react";
import { api } from "./convex/_generated/api";

// Step 1: Request permission
const permission = await Notification.requestPermission();
if (permission !== "granted") {
  throw new Error("Push permission denied");
}

// Step 2: Get service worker registration
const registration = await navigator.serviceWorker.ready;

// Step 3: Subscribe to push manager
const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});

// Step 4: Save subscription to Convex
const subscribe = useMutation(api.push.subscribe);

const subscriptionJSON = subscription.toJSON();
await subscribe({
  endpoint: subscriptionJSON.endpoint!,
  keys: {
    p256dh: subscriptionJSON.keys!.p256dh!,
    auth: subscriptionJSON.keys!.auth!
  }
});

Parameters

endpoint
string
required
Push service endpoint URL
keys
object
required
Encryption keys for the subscription

Response

subscriptionId
Id<'pushSubscriptions'>
The subscription ID (creates new or updates existing based on endpoint)
If a subscription with the same endpoint already exists, it will be updated with the new keys instead of creating a duplicate.

Unsubscribe from Push

Remove a push subscription.
const unsubscribe = useMutation(api.push.unsubscribe);

// Get current subscription
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();

if (subscription) {
  // Unsubscribe from push manager
  await subscription.unsubscribe();
  
  // Remove from database
  await unsubscribe({
    endpoint: subscription.endpoint
  });
}

Parameters

endpoint
string
required
The push endpoint to unsubscribe

List My Subscriptions

Retrieve all push subscriptions for the authenticated user.
const subscriptions = useQuery(api.push.listByUser);

// Response:
[
  {
    _id: "...",
    userId: "user123",
    endpoint: "https://fcm.googleapis.com/fcm/send/...",
    keys: {
      p256dh: "...",
      auth: "..."
    },
    createdAt: "2024-03-05T10:00:00Z"
  }
]

Response

subscriptions
array
Array of push subscription objects for the current user

List Family Subscriptions

Retrieve all push subscriptions for members of a family.
const familySubscriptions = useQuery(api.push.listAllForFamily, {
  familyId
});

Parameters

familyId
Id<'families'>
required
The family ID

Response

subscriptions
array
Array of all push subscription objects for family members
This is useful for sending notifications to all caregivers in a family.

VAPID Keys Setup

VAPID (Voluntary Application Server Identification) keys are required for Web Push.
1

Generate VAPID keys

Use the web-push npm package to generate keys:
npx web-push generate-vapid-keys
Output:
Public Key: BMxY...
Private Key: abcd...
2

Add to environment variables

Set environment variables in Convex dashboard:
VAPID_PUBLIC_KEY=BMxY...
VAPID_PRIVATE_KEY=abcd...
VAPID_SUBJECT=mailto:your-email@example.com
3

Add public key to client

Make the public key available to your frontend:
// In your .env.local
NEXT_PUBLIC_VAPID_KEY=BMxY...

Service Worker Setup

Register a service worker to handle push notifications.
public/sw.js
self.addEventListener('push', (event) => {
  const data = event.data.json();
  
  const options = {
    body: data.body,
    icon: '/icon-192.png',
    badge: '/badge-72.png',
    tag: data.tag || 'default',
    requireInteraction: true,
    data: {
      url: data.url || '/'
    }
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  
  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  );
});
Register in your app:
app/layout.tsx
useEffect(() => {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('SW registered', reg))
      .catch(err => console.error('SW registration failed', err));
  }
}, []);

Cron-Based Reminder Delivery

Zen Nurture uses a cron job to check for due reminders and send push notifications.

Cron Setup

convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

crons.interval(
  "check reminders",
  { minutes: 1 }, // Check every minute
  internal.pushCron.checkAndSendReminders
);

export default crons;

How It Works

1

Cron triggers every minute

The scheduled function runs every minute to check for due reminders.
2

Query upcoming reminders

For each baby profile, compute reminders that are due or overdue.
3

Send push notifications

For each due reminder, send push notifications to all subscribed family members.
4

Handle failures gracefully

If a push fails (expired subscription, device offline), the subscription is automatically removed from the database.

Notification Payload

Push notifications sent by Zen Nurture have this structure:
{
  title: "Feeding Time",
  body: "It's time to feed Emma",
  tag: "reminder_abc123",
  url: "/?babyId=xyz789"
}
title
string
Notification title (usually reminder title)
body
string
Notification body (includes baby name and context)
tag
string
Unique tag to group/replace notifications
url
string
Deep link URL to open when notification is clicked

Browser Support

Browsers with full Web Push support:
  • Chrome 50+ (Desktop & Android)
  • Firefox 44+ (Desktop & Android)
  • Edge 17+
  • Opera 37+
  • Samsung Internet 4+

Best Practices

Don’t ask for notification permission immediately on page load. Wait until the user performs an action that benefits from notifications (e.g., creating a reminder).
If the user denies permission, provide alternative notification methods (email, SMS) or in-app alerts.
Push notifications behave differently across browsers and operating systems. Test thoroughly.
Never expose your private VAPID key in client code. Store it securely in environment variables.
Push endpoints can expire. Your cron job should automatically remove failed subscriptions.
Include the baby’s name and specific action in the notification body for clarity.

Troubleshooting

Check these common issues:
  • Browser notification permission granted?
  • Service worker registered successfully?
  • VAPID keys configured correctly?
  • Device online and not in “Do Not Disturb” mode?
Possible causes:
  • Invalid VAPID public key format
  • Service worker not registered
  • Browser doesn’t support push (check Browser Support tab)
Solution: Push endpoints can expire. Implement logic to re-subscribe when subscription.expirationTime is near or when pushManager.getSubscription() returns null.

Reminders

Create reminder rules that trigger push notifications

Reminders Guide

Feature guide for smart reminders

Web Push Spec

Official Web Push API specification

web-push Library

Node.js library for sending push notifications

Build docs developers (and LLMs) love