Notifications & Announcements — Project Plan

Status: In Progress Gem: Noticed v3.0 Delivery Channels: In-App (bell dropdown), Email, Slack (org-level webhook)


Architecture Overview

User receives notification
  ├── In-App (ActionCable → Turbo Stream → bell dropdown)
  ├── Email (ActionMailer via NotificationMailer)
  └── Slack (org-level webhook, bulk delivery)

Admin publishes announcement
  ├── Banner (dismissible, stored in localStorage)
  └── Notification to all users (via AnnouncementNotifier)

Key Models

Model Purpose
Noticed::Event Noticed engine — stores event data
Noticed::Notification Noticed engine — per-recipient notification record
Announcement Admin-created system announcements
NotificationPreference Per-user, per-type delivery channel toggles

Patterns

  • Notifications are cross-org (user-level) — belongs_to :user as recipient, routes NOT under /organizations/
  • Announcements are system-wide — created by admins, displayed as banner + in-app notification
  • No app/services/ — Slack formatting in Organization::SlackNotifier (namespaced model class)
  • Notifiers live in app/notifiers/ — inherit from ApplicationNotifier < Noticed::Event

Implementation Steps

Phase 1: Foundation — Gem, Database, Core Models

  1. Add Noticed gemgem "noticed", "~> 3.0" in Gemfile. Run:
    bundle install
    rails noticed:install:migrations
    rails db:migrate
    
  2. Create Announcement model — Migration for announcements table:
    • title:string!, body:text!, kind:string! (enum: info, warning, maintenance, feature)
    • published_at:datetime, expires_at:datetime, active:boolean (default: true)
    • user_id:references (admin who created it)
    • System-wide, NOT scoped to organization
  3. Create NotificationPreference model — Migration for notification_preferences:
    • user_id:references!, notification_type:string!
    • email:boolean (default: true), in_app:boolean (default: true), slack:boolean (default: true)
    • Unique index on [user_id, notification_type]
  4. Add slack_webhook_url to organizations — Migration adding slack_webhook_url:string

  5. Configure Noticed initializerconfig/initializers/noticed.rb:
    • Noticed.parent_class = "ApplicationJob" (use Solid Queue)
    • Extend Noticed::Notification with custom scopes
  6. Add associations to User — In app/models/user.rb:
    • has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
    • has_many :notification_preferences, dependent: :destroy
  7. Configure ActionCableapp/channels/application_cable/connection.rb:
    • identified_by :current_user with Devise Warden lookup
  8. Create NotificationsChannelapp/channels/notifications_channel.rb:
    • stream_for current_user on subscribe

Phase 2: Application Notifier & Delivery Methods

  1. Create ApplicationNotifierapp/notifiers/application_notifier.rb:
    • :action_cable — real-time bell updates via Turbo Streams
    • :email — conditional on user preferences; uses NotificationMailer
    • bulk_deliver_by :slack — conditional on org slack_webhook_url
  2. Create NotificationMailerapp/mailers/notification_mailer.rb:
    • Generic notification_email method
    • Clean email template at app/views/notification_mailer/notification_email.html.erb

Phase 3: Notifiers for Business Events

# Notifier Trigger Location Recipients
11 InvitationReceivedNotifier Organizations::InvitationsController#create Invited user
12 InvitationApprovedNotifier Users::InvitationsController#approve Org admins
13 InvitationRejectedNotifier Users::InvitationsController#reject Org admins
14 MembershipRequestReceivedNotifier Users::MembershipRequestsController#create Org admins
15 MembershipRequestApprovedNotifier Organizations::MembershipRequestsController#approve Requesting user
16 MembershipRequestRejectedNotifier Organizations::MembershipRequestsController#reject Requesting user
17 MemberRemovedNotifier Organizations::MembershipsController#destroy Removed user
18 MemberRoleChangedNotifier Organizations::MembershipsController#update Affected member
19 OrganizationTransferredNotifier Organizations::TransfersController#update New owner + admins
20 AnnouncementNotifier Admin::AnnouncementsController#publish All users

Phase 4: Notification UI — Bell Dropdown & Real-Time

  1. notifications_controller.js Stimulus controller — Toggle dropdown, unread badge, mark-as-read via @rails/request.js

  2. Bell icon in navbar — Between dark mode toggle and user avatar in _navbar.html.erb:
    • Bell SVG + unread count badge (red dot/number)
    • Dropdown: recent 20 notifications, unread first
    • Each: icon by type, message, timestamp, link
    • "Mark all as read" + "View all" buttons
  3. NotificationsController (top-level, cross-org):
    • index — full page with Pagy pagination, read/unread filter
    • mark_as_read — PATCH single notification
    • mark_all_as_read — PATCH collection
  4. Notification partials_notification.html.erb, _dropdown.html.erb

  5. Turbo Streams subscriptionturbo_stream_from current_user, :notifications in layout

Phase 5: Announcement System

  1. Admin::AnnouncementsController — Full CRUD + publish action
  2. Admin views — index, new, edit, _form
  3. Admin sidebar link — Add "Announcements" in admin.html.erb
  4. Admin routesresources :announcements do member { post :publish } end
  5. Announcement banner_announcement_banner.html.erb — dismissible, uses localStorage
  6. announcement-banner Stimulus controller — dismiss + localStorage persistence
  7. Render banner in layout — Above main content for signed-in users

Phase 6: Notification Preferences

  1. Users::NotificationPreferencesController — index + update
  2. Preferences view — Table with types as rows, channels as columns (toggles)
  3. Link in user dropdown — "Notification Preferences" in navbar
  4. Wire into notifiersconfig.if checks preferences per delivery channel

Phase 7: Organization Slack Integration

  1. Slack settings in org edit form — Text field for slack_webhook_url (admin only)
  2. Organization::SlackNotifier — Namespaced model class for formatting/sending

Phase 8: Documentation Updates

  1. Update AGENTS.md — Add "Notifications with Noticed" section + add gem to approved list
  2. Update FILE_STRUCTURE.md — Add app/notifiers/ folder
  3. Complete this doc — Architecture, guides, examples

Phase 9: Testing

  1. Fixturesnoticed_events.yml, noticed_notifications.yml, announcements.yml, notification_preferences.yml
  2. Model tests — Announcement, NotificationPreference, notification extensions
  3. Controller tests — NotificationsController, Admin::AnnouncementsController, Users::NotificationPreferencesController
  4. Notifier tests — Each notifier in test/notifiers/ (new directory)
  5. System tests — Bell dropdown, mark as read, announcement banner, admin publish

Decisions Log

Decision Choice Rationale
Notification gem Noticed v3.0 Battle-tested, supports all 3 channels, Solid Queue compatible
Notification scope Cross-org (user-level) Notifications are personal, not org-scoped
Slack level Organization-level webhook Each org configures their own channel
Announcement display Banner + notification Maximum visibility for system announcements
User preferences Full preferences from start Per-type, per-channel toggles with defaults all enabled
Bell location Navbar (top bar) Consistent with SaaS patterns, visible on all pages
Slack formatting Organization::SlackNotifier Namespaced model class, no services folder

How to Create a New Notification

# 1. Generate notifier
# rails generate noticed:notifier NewFeatureNotifier

# 2. Define in app/notifiers/new_feature_notifier.rb
class NewFeatureNotifier < ApplicationNotifier
  deliver_by :action_cable do |config|
    config.channel = "NotificationsChannel"
    config.stream = -> { recipient }
  end

  deliver_by :email do |config|
    config.mailer = "NotificationMailer"
    config.method = :notification_email
    config.if = -> { recipient.notification_preference_enabled?(:new_feature, :email) }
  end

  bulk_deliver_by :slack do |config|
    config.url = -> { params[:organization]&.slack_webhook_url }
    config.json = -> { Organization::SlackNotifier.format(notification) }
    config.if = -> { params[:organization]&.slack_webhook_url.present? }
  end

  notification_methods do
    def message
      t(".message", name: params[:name])
    end

    def url
      root_path
    end
  end
end

# 3. Fire in controller
NewFeatureNotifier.with(record: @record, organization: @organization)
                  .deliver(recipients)

# 4. Add notification_type to NotificationPreference::TYPES
# 5. Add locale in config/locales/en.yml under notifiers.new_feature_notifier.notification

Verification Checklist

  • bin/ci passes (lint, security, tests)
  • Bell icon shows in navbar with unread count
  • Trigger invitation → notification appears in real-time
  • Click notification → marks as read + navigates
  • "Mark all as read" clears badge
  • Admin creates + publishes announcement → banner + notification appear
  • Dismiss banner → stays dismissed (localStorage)
  • Notification preferences page → toggle email off → no email sent
  • Organization Slack webhook → trigger event → Slack message arrives
  • Full page notifications index with pagination + filters
  • Mobile: bell visible in navbar, dropdown works