Notifications
Notifications keep you informed about what is happening in your project. Background job completions, mentions from team members, and system events all appear in your notification inbox.
Overview
Every user has a personal notification inbox accessible from the admin dashboard. Notifications are private — you only see notifications meant for you.
Foir automatically creates notifications for background job results, @mentions in notes, note replies, and system events. You can view, filter, and manage notifications from the inbox or programmatically via the CLI and API.
Notification Types
| Type | Description | Example Trigger |
|---|---|---|
JOB_COMPLETED | A background job finished successfully | Sync completed, search index updated |
JOB_FAILED | A background job failed | Sync error, processing failure |
JOB_PROGRESS | A long-running job has a progress update | Large import/export operations |
MENTION | Someone @mentioned you in a note | Team member tags you for feedback |
NOTE_REPLY | Someone replied to your note | Response to your comment |
NOTE_RESOLVED | A note you are involved in was resolved | Discussion concluded |
SYSTEM | System event notification | Maintenance notices, feature updates |
ANNOUNCEMENT | Platform-wide announcement | Important platform news |
Notification Properties
Each notification includes:
| Field | Description |
|---|---|
title | Short summary (e.g., “Shopify sync completed”) |
message | Optional detail message |
actionUrl | Link to the relevant content or page |
type | Notification category (see types above) |
isRead | Whether you have read this notification |
createdAt | When the notification was created |
In the Admin
Viewing Notifications
- Click the notification bell icon in the header
- See your recent notifications with unread count
- Click a notification to navigate to the related content
- Mark individual notifications as read or mark all as read
Filtering
Filter notifications by:
- Unread only — show only notifications you have not read
- Type — filter by notification type (jobs, mentions, system)
Managing Notifications
- Mark as read (single) — click a notification or use the mark-as-read action
- Mark all as read — click Mark all as read to clear your unread count
- Delete — remove notifications you no longer need (deleted notifications cannot be recovered)
Automatic Notifications
Foir creates notifications automatically for:
- Background jobs — When you trigger operations like syncs, search indexing, or file processing, you receive a notification when the job completes or fails. This lets you navigate away and continue working.
- Mentions — When a team member @mentions you in a note, you receive a notification linking directly to the note.
- Note activity — When someone replies to a note you authored or resolves a note you are involved in.
Via the CLI
# List your notifications
foir notifications list
# Mark a notification as read
foir notifications read <id>
# Mark all notifications as read
foir notifications read-allVia the API
Real-time Notifications via SSE
The realtime service provides an SSE endpoint for streaming notifications as they happen, without polling. Subscribe to the notifications:{userId} channel:
Endpoint:
GET https://realtime.foir.io/sse/notifications:{userId}?token={customerToken}Auth is via query parameter: pass a customer JWT as ?token= or an API key as ?apiKey=.
Example (JavaScript):
const source = new EventSource(
`https://realtime.foir.io/sse/notifications:${userId}?token=${customerToken}`
);
source.onmessage = (event) => {
const { event: eventType, data } = JSON.parse(event.data);
console.log(`New notification: ${data.title}`);
};
source.onerror = () => {
// EventSource reconnects automatically
};Each event contains a channel, event type, and data payload with the notification fields.
See Real-time & Subscriptions for the full SSE and WebSocket channel documentation.
Customer Push Notifications
The admin inbox above is for operators. For customer-facing apps (mobile and web), the public API ships a small surface for sending notifications, push delivery, and per-category preferences. These fields run in a customer session (or an api-key carrying the matching scope).
Sending a Notification
Send a notification to one of your customers with sendNotification. Requires the notifications:write scope. customerId and title are required; everything else is optional.
mutation Send {
sendNotification(input: {
customerId: "cust_123"
title: "Your order has shipped"
message: "Tracking number 1Z999..."
category: "orders"
actionUrl: "/orders/abc"
imageUrl: "https://cdn.foir.dev/media/box.png"
expiresAt: "2026-07-01T00:00:00Z"
})
}| Field | Type | Notes |
|---|---|---|
customerId | ID! | Recipient customer (required) |
title | String! | Short headline (required) |
message | String | Optional detail body |
category | String | Category key, used for per-category preferences |
actionUrl | String | Link to open when the notification is tapped |
imageUrl | String | Thumbnail / rich-push image URL |
metadata | JSON | Arbitrary data carried with the notification |
channels | JSON | Per-channel delivery overrides (see below) |
expiresAt | DateTime | When the notification should stop being delivered |
Per-channel overrides
The optional channels field is a free-form JSON map keyed by channel name (push, email, …). It is a convention, not a typed-per-channel schema — absent keys fall back to the core fields (title, message, actionUrl, imageUrl). Add a channel by adding its key. By convention:
push—deep_link,thread_id,image,badge, and a free-formdataobjectemail—subject,body,reply_to, andctas(an array of{ label, url })
mutation SendWithChannels {
sendNotification(input: {
customerId: "cust_123"
title: "Your order has shipped"
channels: {
push: {
deep_link: "myapp://orders/abc"
thread_id: "orders"
image: "https://cdn.foir.dev/media/box.png"
badge: 1
data: { orderId: "abc" }
}
email: {
subject: "Your order is on its way"
body: "Hi Jane, your order has shipped..."
reply_to: "support@example.com"
ctas: [{ label: "Track order", url: "https://example.com/track/abc" }]
}
}
})
}Sending in Bulk
Send the same notification to many customers at once with sendBulkNotifications. Requires the notifications:write scope. It takes the same fields as sendNotification, except customerId is replaced by customerIds (a non-empty list of IDs).
mutation SendBulk {
sendBulkNotifications(input: {
customerIds: ["cust_123", "cust_456"]
title: "Weekend sale starts now"
message: "20% off everything until Sunday"
category: "promotions"
actionUrl: "/sale"
})
}Registering a Device
Register the device’s push token so the platform can deliver push notifications to it. Requires the notifications:write scope. platform is one of IOS, ANDROID, or WEB; deviceName is optional.
mutation Register {
registerDeviceToken(input: {
token: "fcm-or-apns-device-token"
platform: IOS
deviceName: "Jane's iPhone"
})
}
mutation Unregister {
unregisterDeviceToken(token: "fcm-or-apns-device-token")
}Both mutations return a Boolean.
Notification Preferences
Read the authenticated customer’s per-category notification preferences. Requires the notifications:read scope.
query Prefs {
notificationPreferences {
category
enabled
channels
}
}Each entry reports a category, whether it is enabled, and the delivery channels for that category. Use the updateNotificationPreference mutation (notifications:write) to change a preference.
Web push (browser)
Foir delivers native Web Push (VAPID) to browsers — Chrome, Edge, Firefox, and Safari (including installed iOS 16.4+ PWAs) — with no Firebase in your web frontend. There are two steps: host a service worker, then enrol the browser.
1. Host the service worker
Web Push requires a service worker served from your own origin (it cannot be cross-origin). Save this as foir-sw.js at your site root:
// foir-sw.js -- Foir Web Push service worker.
self.addEventListener('push', (event) => {
const p = event.data ? event.data.json() : {};
event.waitUntil(
self.registration.showNotification(p.title || 'Notification', {
body: p.body,
image: p.image,
badge: p.badge,
tag: p.category,
data: { deepLink: p.deepLink, ...(p.data || {}) },
}),
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = (event.notification.data && event.notification.data.deepLink) || '/';
event.waitUntil(
clients
.matchAll({ type: 'window', includeUncontrolled: true })
.then((wins) => {
for (const w of wins) {
if (w.url === url && 'focus' in w) return w.focus();
}
return clients.openWindow(url);
}),
);
});The payload keys (title, body, deepLink, image, badge, category, data) are exactly what Foir sends — keep them in sync with the handler above.
2. Enrol the browser
Use the @foir/sdk/push helper. It requests notification permission, registers the worker, fetches your project’s VAPID public key, subscribes, and registers the subscription as a WEB device token:
import { enableWebPush, disableWebPush } from '@foir/sdk/push';
// Call from a user gesture (e.g. a "Turn on notifications" button):
const result = await enableWebPush({ adapter, env });
if (!result.ok) {
// result.reason: 'unsupported' | 'denied' | 'no-config'
}
// To turn it back off:
await disableWebPush({ adapter, env });adapter and env are the same SessionAdapter + FoirEnv you already use with @foir/sdk — the customer’s bearer token is attached and refreshed automatically. From then on, any customer notification you send with push enabled is delivered to the browser. Expired subscriptions are detected on send (a 404/410 from the push service) and their device token is deactivated automatically.
Browser support. Web Push works in Chrome, Edge, and Firefox on desktop and Android. On Apple platforms it needs Safari 16.4+, and on iOS/iPadOS the site must be added to the Home Screen (installed as a PWA) before
enableWebPush()can subscribe.
Best Practices
- Check notifications regularly — they surface important job results and team activity.
- Use action URLs — click notifications to jump directly to the relevant content.
- Mark as read — keep your inbox clean so new notifications stand out.
- Monitor job notifications — failed jobs may need your attention.
- Use SSE or WebSocket channels for real-time integrations instead of polling the API.