DEV Community

Alireza Nourbakhsh
Alireza Nourbakhsh

Posted on

Guide on Rhino's Notifications Module

About Rhino

Rhino is a framework built around Model-Driven Development (MDD), which is the core of its "vibe-coding" friendly approach. Instead of making you write repetitive boilerplate code, Rhino automatically generates secure API endpoints and routes directly from your data models. This allows you to just focus on defining your application's logic—like your Category, Blog, and BlogPost models—and trust the framework to handle the rest. This model-first philosophy is also naturally AI-friendly, as the well-defined models give AI tools the precise context they need to understand your intent and help you build features with minimal friction.

Overview

The Rhino Notifications module (rhino_project_notifications) is a Rails engine that wraps and extends the activity_notification gem (v2.3.3) to provide a complete notification system for Rhino-based applications. It provides both backend (Rails API) and frontend (React) components for managing user notifications.

This guide will first explain the base activity_notification gem capabilities, then detail what Rhino adds on top, and finally walk through setup and usage.


Part 1: The Base activity_notification Gem

Before understanding what Rhino adds, let's understand the underlying gem's capabilities.

Core Concepts

The activity_notification gem provides a flexible notification system with these key features:

1. Target (Receiver of Notifications)

Models that can receive notifications use acts_as_target:

class User < ApplicationRecord
  acts_as_target email: :email
end
Enter fullscreen mode Exit fullscreen mode

This makes User a notification recipient and provides methods like:

  • user.notifications - All notifications
  • user.unopened_notification_count - Count of unread
  • user.notification_opened?(notification) - Check if opened
  • user.open_notification(notification) - Mark as read

2. Notifiable (Trigger of Notifications)

Models that trigger notifications use acts_as_notifiable:

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :post

  acts_as_notifiable :users,
    targets: ->(comment, key) { [comment.post.user] },
    group: :post,
    printable_name: ->(comment) { comment.body.truncate(30) }
end
Enter fullscreen mode Exit fullscreen mode

3. Automatic Triggering

Notifications can be created automatically on model actions:

acts_as_notifiable :users,
  tracked: { only: [:create] }  # Auto-notify on creation
Enter fullscreen mode Exit fullscreen mode

Or manually triggered:

comment.notify :users, key: "comment.create"
Enter fullscreen mode Exit fullscreen mode

4. Grouping & Keys

  • Keys: String identifiers for notification types (e.g., "comment.create")
  • Grouping: Consolidate related notifications to prevent spam
acts_as_notifiable :users,
  group: :post,  # Group all comments on same post
  dependent_notifications: :update_group_and_destroy
Enter fullscreen mode Exit fullscreen mode

Result: Multiple comments on the same post = 1 notification

5. Multiple Delivery Channels

  • Database (default) - Store in DB
  • Email - Send email notifications
  • Push - Web push notifications
  • ActionCable - Real-time WebSocket updates

Example: Rails App Without Rhino

Here's how you'd use activity_notification in a standard Rails app:

Step 1: Installation

bundle add activity_notification
rails generate activity_notification:install
rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Step 2: Configuration

# config/initializers/activity_notification.rb
ActivityNotification.configure do |config|
  config.email_enabled = true
  config.mailer = "ActivityNotification::Mailer"
  config.action_cable_enabled = true
  config.group_expiry_threshold = 5.minutes
end
Enter fullscreen mode Exit fullscreen mode

Step 3: Models

# app/models/user.rb
class User < ApplicationRecord
  acts_as_target email: :email
  has_many :posts
  has_many :comments
end

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :post

  acts_as_notifiable :users,
    targets: ->(comment, key) { [comment.post.user] },
    notifier: :user,
    group: :post,
    printable_name: ->(comment) { comment.body.truncate(30) }
end
Enter fullscreen mode Exit fullscreen mode

Step 4: Controller

# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    @comment = @post.comments.build(comment_params.merge(user: current_user))

    if @comment.save
      @comment.notify :users, key: "comment.create"
      redirect_to @post, notice: "Comment added!"
    else
      render :new
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Step 5: Views

<!-- app/views/layouts/application.html.erb -->
<% current_user.notifications.unopened.each do |notification| %>
  <%= render_notification notification %>
<% end %>

<!-- app/views/activity_notification/notifications/_comment_create.html.erb -->
<%= link_to notification.notifier.name, user_path(notification.notifier) %>
commented on your
<%= link_to "post", post_path(notification.group) %>:
"<%= notification.notifiable.body.truncate(20) %>"
Enter fullscreen mode Exit fullscreen mode

How Everything Connects

Step What Happens
1 User adds a comment
2 Comment triggers notify :users, key: "comment.create"
3 Gem finds targets (post author via lambda)
4 Notification record created in DB
5 Delivery channels execute (email, ActionCable)
6 Author sees it in their notifications list
7 When viewed, opened_at timestamp is set

Key Features Summary

Feature Description
Target (receiver) acts_as_target in User
Notifiable (trigger) acts_as_notifiable in Comment
Grouping group: :post consolidates notifications
Key-based templates "comment.create" maps to view partial
Channels DB, email, ActionCable configurable
Persistence Stored in notifications table
Read state Managed via .open!, .unopened, .opened
Deletion Manual (not automatic)

Part 2: What Rhino Adds to activity_notification

Rhino wraps activity_notification to provide a modern SPA architecture with these additions:

1. 🎯 RESTful JSON API Endpoints

Base gem: Provides HTML views and controllers for traditional Rails apps

Rhino adds: Automatic JSON API endpoints

# rhino/rhino-project/gems/rhino_project_notifications/config/routes.rb
Rails.application.routes.draw do
  scope Rhino.namespace do
    notify_to :users, api_mode: true, with_devise: :users
  end
end
Enter fullscreen mode Exit fullscreen mode

Creates these endpoints:

  • GET /api/users/:user_id/notifications - List with filtering
  • GET /api/users/:user_id/notifications/:id - Show single
  • PUT /api/users/:user_id/notifications/:id/open - Mark as opened
  • POST /api/users/:user_id/notifications/open_all - Mark all opened
  • DELETE /api/users/:user_id/notifications/:id - Delete

JSON Response Format:

{
  "data": {
    "count": 3,
    "notifications": [
      {
        "id": 1,
        "notifiable_type": "Comment",
        "notifiable_id": 42,
        "notifiable_path": "/articles/10/comments/42",
        "printable_notifiable_name": "Comments on Article Title",
        "key": "comment.create",
        "group_owner_id": 10,
        "opened_at": null,
        "created_at": "2025-10-17T10:30:00.000Z",
        "parameters": {
          "article_id": 10
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Query Parameters:

  • filter=unopened - Only unopened notifications
  • filter=opened - Only opened notifications
  • filter=all - All notifications
  • limit=10 - Limit results

2. 🎨 Complete Frontend React Integration

Base gem: No frontend - you build your own

Rhino provides:

React Query Hooks

// rhino/rhino-project/packages/core/src/queries/notifications.js
import { useQuery, useMutation } from '@tanstack/react-query';
import { networkApiCall } from '../lib/networking';
import { useUserId } from '../hooks/auth';

const basePath = (userId) => `/api/users/${userId}/notifications`;
const fullPath = (userId, queryPath) => `${basePath(userId)}/${queryPath}`;

// Fetch unopened notifications
export const useNotifications = () => {
  const userId = useUserId();

  return useQuery({
    queryKey: ['notifications-index'],
    queryFn: ({ signal }) =>
      networkApiCall(fullPath(userId, '?filter=unopened&limit=10'), { signal }),
    enabled: !!userId
  });
};

// Mark all as opened
export const useNotificationsOpenAll = () => {
  const userId = useUserId();

  return useMutation({
    mutationFn: () =>
      networkApiCall(fullPath(userId, 'open_all'), { method: 'post' })
  });
};

// Mark single notification as opened
export const useNotificationsOpen = () => {
  const userId = useUserId();

  return useMutation({
    mutationFn: (notificationId) =>
      networkApiCall(fullPath(userId, `${notificationId}/open`), {
        method: 'put'
      })
  });
};
Enter fullscreen mode Exit fullscreen mode

Pre-built NotificationMenu Component

// rhino/rhino-project/packages/core/src/components/app/NotificationMenu.js
import { NavLink } from 'react-router-dom';
import {
  Badge,
  DropdownItem,
  DropdownMenu,
  DropdownToggle,
  UncontrolledDropdown
} from 'reactstrap';
import { NavIcon } from '../icons';
import {
  useNotifications,
  useNotificationsOpen,
  useNotificationsOpenAll
} from '../../queries/notifications';

export const NotificationMenu = () => {
  const { data: { data: notifications } = {}, refetch } = useNotifications();
  const { mutate: openAll } = useNotificationsOpenAll();
  const { mutate: openOne } = useNotificationsOpen();
  const hasNotifications = notifications?.count > 0;

  const handleItemClick = (notificationId) =>
    openOne(notificationId, {
      onSuccess: () => refetch()
    });

  const handleClick = () => openAll({ onSuccess: () => refetch() });

  return (
    <UncontrolledDropdown nav direction="up">
      <DropdownToggle nav caret className="d-flex align-items-center text-light no-arrow">
        <NavIcon icon="bell" extraClass="flex-shrink-0" />
        <span className="d-block ms-2 overflow-hidden flex-grow-1">
          Notifications
        </span>
        {hasNotifications && <Badge pill>{notifications?.count}</Badge>}
      </DropdownToggle>

      <DropdownMenu dark end>
        {notifications?.notifications?.map((n) => (
          <DropdownItem
            key={n.id}
            tag={NavLink}
            to={n.notifiable_path}
            onClick={() => handleItemClick(n.id)}
          >
            {n.printable_notifiable_name}
          </DropdownItem>
        ))}

        {hasNotifications ? (
          <>
            <DropdownItem divider />
            <DropdownItem disabled={!hasNotifications} onClick={handleClick}>
              Mark All Opened
            </DropdownItem>
          </>
        ) : (
          <DropdownItem disabled>
            <em>No unread notifications</em>
          </DropdownItem>
        )}
      </DropdownMenu>
    </UncontrolledDropdown>
  );
};
Enter fullscreen mode Exit fullscreen mode

3. 🔗 Rhino Routing Integration

Base gem: You manually define notifiable_path

Rhino adds: route_frontend helper that auto-generates hierarchical frontend paths

class Comment < ApplicationRecord
  belongs_to :article
  belongs_to :user

  # Rhino ownership integration
  rhino_owner :article
  rhino_references %i[article user]

  acts_as_notifiable :users,
    targets: ->(comment, _key) { [comment.article.user] },
    notifiable_path: :comment_notifiable_path,
    printable_name: ->(comment) { "Comment on #{comment.article.title}" }

  # Uses Rhino's routing helper
  def comment_notifiable_path
    route_frontend  # Auto-generates: "/123/articles/456/comments/789"
  end
end
Enter fullscreen mode Exit fullscreen mode

The route_frontend method integrates with Rhino's ownership model (rhino_owner) to build hierarchical URLs based on your base owner (e.g., Organization).


4. 🚀 Simplified Installation

Base gem: Multiple manual steps

Rhino provides: One-command generator

rails generate rhino_notifications:install
Enter fullscreen mode Exit fullscreen mode

This single command:

  1. ✅ Runs activity_notification:install (creates initializer)
  2. ✅ Runs activity_notification:migration (creates DB tables)
  3. ✅ Adds acts_as_target email: :email to User model

5. ⚙️ API-Optimized Defaults

Base gem defaults:

  • Email enabled by default
  • Subscriptions enabled by default
  • ActionCable optional

Rhino defaults (API-first):

# config/initializers/activity_notification.rb
config.email_enabled = false              # Disabled for API-only
config.subscription_enabled = false       # Disabled for simplicity
config.action_cable_enabled = false       # Disabled by default
config.action_cable_api_enabled = false   # Disabled by default
Enter fullscreen mode Exit fullscreen mode

This creates a cleaner API-only setup without email/WebSocket complexity unless you explicitly enable them.


6. 🔐 Authorization Integration

The notify_to :users, with_devise: :users route configuration integrates with Rhino's authentication system to ensure users can only access their own notifications.


Comparison: Base Gem vs. Rhino

Feature Base activity_notification Rhino Addition
Model configuration acts_as_target, acts_as_notifiable ✅ Same (manual)
Grouping ✅ Yes ✅ Same
Notification keys ✅ Yes ✅ Same
Parameters ✅ Custom JSON data ✅ Same
HTML Views ✅ Provided ❌ Not used (API-only)
JSON API ⚠️ Basic (optional) ⭐️ Full RESTful API
Frontend ❌ Build your own ⭐️ React components + hooks
Routing Helper ❌ Manual ⭐️ route_frontend integration
Ownership Model ❌ None ⭐️ Rhino owner hierarchy
Installation ⚠️ Multiple steps ⭐️ One-command generator
Email Delivery ✅ Enabled by default ⚠️ Disabled by default
ActionCable ✅ Optional ⚠️ Disabled by default
Subscriptions ✅ Enabled by default ⚠️ Disabled by default

Part 3: Setup and Usage in a Rhino Project

Installation

Step 1: Run the Generator

rails generate rhino_notifications:install
Enter fullscreen mode Exit fullscreen mode

This creates:

File 1: config/initializers/activity_notification.rb

  • Configuration for the notification system
  • Contains ~100+ configuration options
  • Rhino sets sensible API-first defaults

File 2: db/migrate/TIMESTAMP_create_activity_notification_tables.rb

  • Creates notifications table (stores all notifications)
  • Creates subscriptions table (for subscription management if enabled)

File 3: Modifies app/models/user.rb

  • Adds acts_as_target email: :email

Step 2: Run Migrations

rails db:migrate
Enter fullscreen mode Exit fullscreen mode

This creates the database tables.

Database Schema

Notifications Table

create_table :notifications do |t|
  t.belongs_to :target,     polymorphic: true, index: true, null: false  # Who receives it (User)
  t.belongs_to :notifiable, polymorphic: true, index: true, null: false  # What it's about (Comment, etc.)
  t.string     :key,                                        null: false  # Notification type identifier
  t.belongs_to :group,      polymorphic: true, index: true              # Groups related notifications
  t.integer    :group_owner_id,                index: true              # Owner of notification group
  t.belongs_to :notifier,   polymorphic: true, index: true              # Who triggered it
  t.text       :parameters                                              # Custom data (JSON)
  t.datetime   :opened_at                                               # Read/unread tracking
  t.timestamps null: false
end
Enter fullscreen mode Exit fullscreen mode

Key Fields:

  • target: Who receives the notification (polymorphic, typically User)
  • notifiable: What the notification is about (polymorphic, can be any model)
  • key: String identifier (e.g., "comment.create")
  • group: Optional grouping to consolidate related notifications
  • notifier: Who/what caused the notification
  • parameters: JSON text field for custom data
  • opened_at: NULL = unread, NOT NULL = read

Subscriptions Table

create_table :subscriptions do |t|
  t.belongs_to :target,     polymorphic: true, index: true, null: false
  t.string     :key,                           index: true, null: false
  t.boolean    :subscribing,                                null: false, default: true
  t.boolean    :subscribing_to_email,                       null: false, default: true
  t.datetime   :subscribed_at
  t.datetime   :unsubscribed_at
  t.datetime   :subscribed_to_email_at
  t.datetime   :unsubscribed_to_email_at
  t.text       :optional_targets
  t.timestamps null: false
end
add_index :subscriptions, [:target_type, :target_id, :key], unique: true
Enter fullscreen mode Exit fullscreen mode

Configuration

After installation, review and customize:

# config/initializers/activity_notification.rb
ActivityNotification.configure do |config|
  config.enabled = true
  config.orm = :active_record
  config.notification_table_name = "notifications"
  config.subscription_table_name = "subscriptions"

  # Email notifications (disabled by default in Rhino)
  config.email_enabled = false

  # Subscription management (disabled by default in Rhino)
  config.subscription_enabled = false

  # ActionCable/WebSocket (disabled by default in Rhino)
  config.action_cable_enabled = false
  config.action_cable_api_enabled = false

  config.opened_index_limit = 10
end
Enter fullscreen mode Exit fullscreen mode

Note on acts_as_target email: :email:
This parameter tells the gem where to find the email field (field mapping), NOT whether to send emails. The config.email_enabled = false is what actually disables email sending.


Model Configuration

Target Model (User)

The generator already configured this:

# app/models/user.rb
class User < Rhino::User
  acts_as_target email: :email

  # This provides methods:
  # - user.notifications
  # - user.notification_index(filter: 'unopened', limit: 10)
  # - user.unopened_notification_count
  # - user.notification_opened?(notification)
  # - user.open_notification(notification)
  # - user.open_all_notifications
end
Enter fullscreen mode Exit fullscreen mode

Notifiable Models (Manual Configuration)

Configure any model that should trigger notifications:

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article
  belongs_to :user

  # Rhino ownership (if using organizations/multi-tenancy)
  rhino_owner :article
  rhino_references %i[article user]

  # Notification configuration
  acts_as_notifiable :users,
    # Who receives notifications (lambda returns array of Users)
    targets: ->(comment, _key) {
      ([comment.article.user] + comment.article.reload.commented_users.to_a).uniq
    },

    # Group notifications by article (prevents spam)
    group: :article,

    # Which actions trigger notifications
    tracked: { only: [:create] },  # or { except: [:update] }

    # How to handle deletion
    dependent_notifications: :update_group_and_destroy,
    # Options: :delete_all, :destroy, :restrict_with_error, :update_group_and_destroy

    # Frontend path (uses Rhino routing)
    notifiable_path: :comment_notifiable_path,

    # Display name
    printable_name: :comment_printable_name,

    # Enable real-time updates (if ActionCable enabled)
    action_cable_api_allowed: true,

    # Additional data to store
    parameters: { article_id: :article_id }

  # Method for frontend routing
  def comment_notifiable_path
    route_frontend  # Rhino helper
    # Returns: "/123/articles/456/comments/789"
  end

  # Method for display name
  def comment_printable_name
    "Comments on #{article.title}"
  end
end
Enter fullscreen mode Exit fullscreen mode

Group Model (Optional)

If using grouping, mark the group model:

# app/models/article.rb
class Article < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_many :commented_users, through: :comments, source: :user

  rhino_owner_base  # If Article is your base owner
  rhino_references [:user]

  acts_as_notification_group  # Marks as a group model
end
Enter fullscreen mode Exit fullscreen mode

Usage Patterns

1. Automatic Notification Creation

With tracked: { only: [:create] }, notifications are created automatically:

# In your controller - no explicit notification call needed!
def create
  @comment = @article.comments.build(comment_params.merge(user: current_user))

  if @comment.save
    # Notification automatically created via acts_as_notifiable
    redirect_to @article, notice: "Comment added!"
  else
    render :new
  end
end
Enter fullscreen mode Exit fullscreen mode

2. Manual Notification Creation

You can also trigger notifications manually:

# Basic manual trigger
comment.notify_to(users)

# With options
comment.notify_to(
  users,
  key: 'custom.notification.key',
  parameters: { custom_field: 'value' }
)

# Notify single user
comment.notify_to(user)
Enter fullscreen mode Exit fullscreen mode

3. Dynamic Notification Targets

Complex target logic in the lambda:

acts_as_notifiable :users,
  targets: ->(comment, key) {
    targets = []

    # Notify article author
    targets << comment.article.user

    # Notify other commenters
    targets += comment.article.comments.map(&:user)

    # Notify mentioned users (custom method)
    targets += comment.extract_mentions

    # Remove duplicates and commenter themselves
    (targets.uniq - [comment.user])
  }
Enter fullscreen mode Exit fullscreen mode

4. Custom Parameters

Store additional context with notifications:

acts_as_notifiable :users,
  parameters: {
    article_id: :article_id,
    comment_count: ->(comment) { comment.article.comments.count },
    is_urgent: ->(comment) { comment.body.include?('URGENT') }
  }

# Access in frontend/API
notification.parameters['article_id']
notification.parameters['comment_count']
notification.parameters['is_urgent']
Enter fullscreen mode Exit fullscreen mode

5. Notification Grouping

Prevent notification spam by grouping:

acts_as_notifiable :users,
  group: :article,
  dependent_notifications: :update_group_and_destroy

# Result: Multiple comments on same article = 1 notification
# The notification updates with latest comment info
Enter fullscreen mode Exit fullscreen mode

Frontend Integration

Using the Pre-built Component

// In your Rhino app layout/navigation
import { NotificationMenu } from '@rhino-project/core';

function AppNavigation() {
  return (
    <nav>
      {/* Your other nav items */}
      <NotificationMenu />
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's it! The component handles:

  • Fetching unopened notifications
  • Displaying count badge
  • Click-to-navigate to notification target
  • Mark as opened
  • Mark all as opened

Custom Frontend Implementation

If building your own frontend:

import { 
  useNotifications, 
  useNotificationsOpen 
} from '@rhino-project/core';

function CustomNotifications() {
  const { data: { data: notifications } = {}, refetch } = useNotifications();
  const { mutate: openOne } = useNotificationsOpen();

  const handleClick = (notificationId, path) => {
    openOne(notificationId, {
      onSuccess: () => {
        refetch();
        navigate(path);
      }
    });
  };

  return (
    <div>
      <h2>Notifications ({notifications?.count || 0})</h2>
      {notifications?.notifications?.map((n) => (
        <div key={n.id} onClick={() => handleClick(n.id, n.notifiable_path)}>
          {n.printable_notifiable_name}
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

API Endpoints

The following endpoints are automatically available:

List Notifications

GET /api/users/:user_id/notifications?filter=unopened&limit=10
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "data": {
    "count": 3,
    "notifications": [
      {
        "id": 1,
        "notifiable_type": "Comment",
        "notifiable_id": 42,
        "notifiable_path": "/articles/10/comments/42",
        "printable_notifiable_name": "Comments on Article Title",
        "key": "comment.create",
        "opened_at": null,
        "parameters": { "article_id": 10 }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Query Parameters:

  • filter=unopened (default) - Only unopened
  • filter=opened - Only opened
  • filter=all - All notifications
  • limit=10 - Limit results

Show Single Notification

GET /api/users/:user_id/notifications/:id
Enter fullscreen mode Exit fullscreen mode

Mark as Opened

PUT /api/users/:user_id/notifications/:id/open
Enter fullscreen mode Exit fullscreen mode

Mark All as Opened

POST /api/users/:user_id/notifications/open_all
Enter fullscreen mode Exit fullscreen mode

Delete Notification

DELETE /api/users/:user_id/notifications/:id
Enter fullscreen mode Exit fullscreen mode

Advanced Features (Optional)

These features are disabled by default but can be enabled:

Email Notifications

# config/initializers/activity_notification.rb
config.email_enabled = true
config.mailer_sender = 'notifications@yourapp.com'

# app/models/user.rb
acts_as_target email: :email, email_allowed: true

# app/models/comment.rb
acts_as_notifiable :users,
  email_allowed: true,
  notifier: :user,
  email_subject: ->(notification) { 
    "New comment on #{notification.notifiable.article.title}" 
  }
Enter fullscreen mode Exit fullscreen mode

ActionCable Real-time Updates

# config/initializers/activity_notification.rb
config.action_cable_enabled = true
config.action_cable_api_enabled = true

# app/models/comment.rb
acts_as_notifiable :users,
  action_cable_api_allowed: true

# Frontend will receive real-time notification updates
Enter fullscreen mode Exit fullscreen mode

Subscription Management

# config/initializers/activity_notification.rb
config.subscription_enabled = true

# Users can manage subscriptions
user.create_subscription(key: 'comment.create')
user.subscribe_to_email('comment.create')
user.unsubscribe('comment.create')
Enter fullscreen mode Exit fullscreen mode

Testing

Example test for notification creation:

# test/models/comment_test.rb
require 'test_helper'

class CommentTest < ActiveSupport::TestCase
  test "creates notification for article author" do
    article = articles(:one)
    author = article.user

    assert_difference 'author.notifications.count', 1 do
      Comment.create!(
        article: article,
        user: users(:commenter),
        body: "Great article!"
      )
    end

    notification = author.notifications.last
    assert_equal 'Comment', notification.notifiable_type
    assert_nil notification.opened_at  # Unopened
  end

  test "groups multiple comments on same article" do
    article = articles(:one)
    author = article.user

    # First comment creates notification
    Comment.create!(
      article: article,
      user: users(:commenter),
      body: "First comment"
    )

    initial_count = author.notifications.count

    # Second comment updates existing notification (grouped)
    Comment.create!(
      article: article,
      user: users(:another_commenter),
      body: "Second comment"
    )

    # Count should be same (grouped)
    assert_equal initial_count, author.notifications.count
  end
end
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Always define printable_name - Makes notifications readable in the UI
  2. Always define notifiable_path - Enables click-through navigation
  3. Use grouping for related notifications - Prevents notification spam
  4. Be specific with targets lambda - Only notify relevant users
  5. Use parameters for context - Store data needed for display/linking
  6. Track only necessary actions - Don't over-notify users
  7. Clean up old notifications - Archive or delete periodically
  8. Test notification creation - Verify targets and data
  9. Consider notification fatigue - Less is more
  10. Use meaningful keys - Follow convention like "model.action"

Troubleshooting

Notifications Not Creating

Check:

  • config.enabled = true in initializer
  • acts_as_notifiable configured correctly
  • targets lambda returns array of users
  • ✅ Action is in tracked list (or manually calling notify_to)
  • ✅ Database migration ran successfully

Frontend Not Showing Notifications

Check:

  • ✅ User is authenticated (useUserId() returns value)
  • ✅ API endpoint permissions correct
  • ✅ Network tab for API errors
  • NotificationMenu included in layout
  • ✅ React Query devtools for cache status

Too Many Notifications

Solutions:

  • Use group option to consolidate
  • Adjust tracked to limit actions
  • Implement filtering logic in targets lambda
  • Consider digest/summary notifications

Performance Issues

Solutions:

  • Add database indexes on target_id, opened_at, notifiable_type
  • Limit query results with limit parameter
  • Use dependent_notifications: :delete_all for bulk operations
  • Archive old notifications to separate table
  • Use database queries instead of loading all notifications

Summary

The Rhino Notifications module provides:

✅ Polymorphic notifications (any model can notify any target)

✅ RESTful JSON API endpoints

✅ React Query hooks and UI components

✅ Notification grouping and consolidation

✅ Read/unread tracking

✅ Custom notification paths and display names

✅ Optional email and real-time support

✅ Subscription management (optional)

✅ Flexible target and parameter configuration

✅ Integration with Rhino's ownership model

✅ One-command installation

What you configure manually (same as base gem):

  • acts_as_notifiable on notifiable models
  • targets lambda defining who gets notified
  • group, tracked, parameters options
  • printable_name and notifiable_path methods
  • Notification keys and types

What Rhino provides automatically:

  • JSON API endpoints
  • Frontend components and hooks
  • Routing integration
  • Authentication/authorization
  • API-optimized defaults
  • Simplified installation

The system is production-ready and follows Rails/React best practices for maintainability and extensibility.

Top comments (0)