Notifications
Send push notifications from your app to a user, and optionally read them back. Homestead signs and delivers the web push to the user's devices and records it in their inbox. A notification always goes to the calling user — you don't manage push subscriptions or inbox rows yourself.
This page covers:
- Set up VAPID keys
- Send a notification
- Send from a custom method
- Send from a server route
- Read notifications
- Add notifications to a new app
- Test notifications
Set up VAPID keys
Push needs a VAPID keypair. Generate one per environment:
npx web-push generate-vapid-keysSet the keys in packages/homestead-app/.env (copy .env.example to start):
VAPID_PUBLIC_KEY=<public-key>
VAPID_PRIVATE_KEY=<private-key>
VAPID_EMAIL=mailto:you@example.comThe browser uses the public key to subscribe; the server uses both keys to sign pushes. Keep VAPID_PRIVATE_KEY secret.
Set the keys before you build: make build and make homestead bake the public key into the SPA bundle. Web push requires HTTPS (localhost is exempt).
Send a notification
Call one server-side helper from packages/homestead-core/server/notifications.ts. It returns a web Response, so it works in both server routes and app custom methods. There are two entry points:
import {
sendUserNotification, // authenticates the request, then sends
sendNotificationForAuth, // caller already authenticated (app methods)
type UserNotificationOptions,
} from '@rambleraptor/homestead-core/server/notifications';Both take a UserNotificationOptions:
| Field | Required | Description |
|---|---|---|
title | yes | Notification title the user sees |
body | yes | Notification body text |
tag | yes | Stable id; pushes with the same tag collapse instead of stacking |
url | yes | Path to open when the notification is clicked |
sourceCollection | no | aepbase plural this is about (e.g. 'people'), for icon + link |
sourceId | no | Record id, for the inbox icon and deep link |
Use a stable tag (e.g. 'grocery-notification') so repeated pushes replace each other. Set sourceCollection to the aepbase plural — 'people', not 'person'.
Send from a custom method
Use this when the notification comes from your app. App endpoints are resource custom methods (AEP-136): you declare them on a resource and the server's /api/aep gateway serves POST /api/aep/<plural>:<verb>, authenticating the caller first. Your handler receives the authenticated caller and passes it to sendNotificationForAuth(auth, …).
Write the handler. The grocery "list updated" push is the working example (packages/homestead-apps/groceries/methods/send-notification.ts):
import { sendNotificationForAuth } from '@rambleraptor/homestead-core/server/notifications';
import type { CustomMethodHandler } from '@rambleraptor/homestead-core/resources/types';
const handler: CustomMethodHandler = async ({ auth }) => {
return sendNotificationForAuth(auth, {
title: 'Grocery List Updated',
body: 'The grocery list has been updated. Check it out!',
tag: 'grocery-notification',
url: '/groceries',
sourceCollection: 'grocery_items',
});
};
export default handler;Wire it into the collection's resource definition via customMethods (packages/homestead-apps/groceries/resources.ts):
customMethods: {
'send-notification': {
target: 'collection',
load: () => import('./methods/send-notification'),
},
},Trigger it from the client with aepbase.customMethod('<plural>', '<verb>') (see useSendGroceryNotification.ts).
Send from a server route
Use this when you own a core server endpoint instead of an app method. The route authenticates the request itself, so it calls sendUserNotification(request, …) with Hono's raw Request (c.req.raw). The built-in test endpoint is the working example (packages/homestead-server/src/routes/notifications.ts):
import { Hono } from 'hono';
import { sendUserNotification } from '@rambleraptor/homestead-core/server/notifications';
export const notificationsRoute = new Hono();
notificationsRoute.post('/send-test', (c) =>
sendUserNotification(c.req.raw, {
title: 'Test Notification',
body: 'If you see this, push notifications are working!',
tag: 'test-notification',
url: '/notifications',
}),
);Read notifications
Most apps don't render notifications — link to /notifications, the shared inbox, instead. To read them in your app, use these hooks:
| Hook | Import | Returns |
|---|---|---|
useNotifications() | @rambleraptor/homestead-core/notifications/hooks/useNotifications | All notifications for the current user |
useNotificationStats() | @rambleraptor/homestead-core/notifications/hooks/useNotificationStats | { total, unread, read } |
useMarkNotificationAsRead() | @rambleraptor/homestead-core/notifications/hooks/useMarkNotificationAsRead | Mutation, takes a notification id |
For a top-N inbox preview, use useUnreadNotifications() from @rambleraptor/homestead-core/dashboard/hooks/useUnreadNotifications.
Each notification carries title and message (what the user sees), read and read_at (read state), and source_collection and source_id (the record it's about). For an app-scoped feed, read the notifications and filter client-side by source_collection === '<your-collection>'.
Add notifications to a new app
Write a custom-method handler under your app (e.g.
methods/send-notification.ts) that default-exports aCustomMethodHandlercallingsendNotificationForAuth(auth, { … }).Declare it on a resource in your app's
resources.tscustomMethodsmap:tscustomMethods: { 'send-notification': { target: 'collection', load: () => import('./methods/send-notification'), }, },On boot, the server serves it at
POST /api/aep/<plural>:<verb>.Trigger it from the client with
aepbase.customMethod('<plural>', '<verb>')(seeuseSendGroceryNotification.ts).Use a stable
tagso repeated pushes replace each other.Set
sourceCollectionto your aepbase plural ('people', not'person') andsourceIdto the record id.
Test notifications
Write a unit test: mock aepbase (done globally in packages/homestead-app/src/test/setup.ts) and assert your hook calls aepbase.customMethod(...) — or your server route calls sendNotificationForAuth — with the right payload.
Run a manual smoke test: with the dev stack running, POST to /api/notifications/send-test from the browser DevTools console. (The Settings page wires this up via useSendTestNotification.) You see a push and a new row in your inbox.