KaryoSpace has a real-time notification bell in the top bar that updates without polling. Every important event across the platform — new email, DM, assigned task, incident created, calendar invite, @mention — fires a notification that appears in the bell within milliseconds of the triggering action. Here is the architecture.
Every notification is a MongoDB document with a common schema regardless of which module created it. The type field gates filtering; the link field tells the UI where to navigate on click.
type Notification struct {
ID primitive.ObjectID `bson:"_id"`
UserID string `bson:"user_id"`
OrgID string `bson:"org_id"`
Type string `bson:"type"` // email|incident|task|mention|calendar
Title string `bson:"title"`
Body string `bson:"body"`
Link string `bson:"link"`
Read bool `bson:"read"`
CreatedAt time.Time `bson:"created_at"`
SourceID string `bson:"source_id"` // email ID, incident ID, etc.
SourceMod string `bson:"source_mod"`
}
Each module calls createNotification() at the moment the event occurs. No event bus, no message queue. Direct function call in the same goroutine, with the MongoDB write happening synchronously so the notification exists before the HTTP response returns to the caller.
// Called from incident/handlers.go when an incident is assigned
func notifyIncidentAssigned(inc *Incident, assigneeID string) {
createNotification(Notification{
UserID: assigneeID,
OrgID: inc.OrgID,
Type: "incident",
Title: "Incident assigned to you",
Body: inc.Title,
Link: fmt.Sprintf("/incidents/%s", inc.ID.Hex()),
SourceID: inc.ID.Hex(),
SourceMod: "incident",
})
// Push to WebSocket immediately after storing
pushNotificationWS(assigneeID, notif)
}
After storing the notification in MongoDB, the same function pushes it to the user's active WebSocket connection via the messaging hub. This is the same WebSocket connection used for chat messages. The hub routes by user ID, so the push reaches all browser tabs the user has open simultaneously.
func pushNotificationWS(userID string, notif Notification) {
payload, _ := json.Marshal(WSMessage{
Type: "notification.new",
Payload: notif,
})
hub.sendToUser(userID, payload)
}
// In the browser (topbar.html)
ws.addEventListener('message', e => {
const msg = JSON.parse(e.data);
if (msg.type === 'notification.new') {
incrementBell();
showToast(msg.payload.title, msg.payload.body);
}
});
WebSocket connections drop for many reasons: idle timeouts, mobile network switches, browser tab backgrounding. When the WebSocket is not available, the bell falls back to polling GET /api/notifications/unread-count every 60 seconds. The count endpoint is a single MongoDB CountDocuments call with an index on user_id + read + created_at. It runs in under 2ms.
The notification dropdown shows filter pills: All, Email, Incident, Task, Mention, Calendar. Selecting a pill filters the visible notifications in-browser from the cached set. The full list is fetched once when the bell is opened; subsequent filter changes are instant client-side operations.
// GET /api/notifications?limit=50
// Returns last 50 notifications for current user, sorted newest first
func listNotificationsHandler(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
opts := options.Find().
SetSort(bson.D{{Key: "created_at", Value: -1}}).
SetLimit(50)
cursor, _ := db.Collection("notifications").Find(ctx,
bson.M{"user_id": userID, "org_id": getOrgID(r)}, opts)
// ...
}
When a user installs KaryoSpace as a PWA and grants notification permission, the browser registers a push subscription. The subscription endpoint and auth keys are stored in MongoDB. When a notification is created for an offline user (no active WebSocket), the server sends a Web Push via the VAPID protocol.
func sendWebPush(userID string, notif Notification) error {
subs, _ := getPushSubscriptions(ctx, userID)
for _, sub := range subs {
payload, _ := json.Marshal(map[string]string{
"title": notif.Title,
"body": notif.Body,
"url": notif.Link,
})
// webpush.SendNotification handles VAPID header signing
resp, err := webpush.SendNotification(payload, &sub, &webpush.Options{
VAPIDPublicKey: os.Getenv("VAPID_PUBLIC_KEY"),
VAPIDPrivateKey: os.Getenv("VAPID_PRIVATE_KEY"),
TTL: 300, // 5 minutes
})
if resp != nil && resp.StatusCode == 410 {
// Subscription expired — remove it
removePushSubscription(ctx, sub.Endpoint)
}
}
return nil
}
The 410 status check is critical. When a user uninstalls the PWA or revokes push permission, the push service returns 410 Gone. If you do not remove the subscription on 410, you accumulate dead subscriptions and start hitting rate limits.
Clicking a notification marks it read. The mark-read API uses a MongoDB UpdateOne with the notification ID and a user_id filter. The user ID filter is not redundant — it prevents a user from marking another user's notification as read by guessing an ID. Every mutation in the notification API includes both notification_id and user_id in the filter.
Questions about KaryoSpace or this article?
Thanks for reaching out. We'll get back to you shortly.