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 :useras recipient, routes NOT under/organizations/ - Announcements are system-wide — created by admins, displayed as banner + in-app notification
- No
app/services/— Slack formatting inOrganization::SlackNotifier(namespaced model class) - Notifiers live in
app/notifiers/— inherit fromApplicationNotifier < Noticed::Event
Implementation Steps
Phase 1: Foundation — Gem, Database, Core Models
- Add Noticed gem —
gem "noticed", "~> 3.0"inGemfile. Run:bundle install rails noticed:install:migrations rails db:migrate - Create Announcement model — Migration for
announcementstable: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
- 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]
-
Add
slack_webhook_urlto organizations — Migration addingslack_webhook_url:string - Configure Noticed initializer —
config/initializers/noticed.rb:Noticed.parent_class = "ApplicationJob"(use Solid Queue)- Extend
Noticed::Notificationwith custom scopes
- 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
- Configure ActionCable —
app/channels/application_cable/connection.rb:identified_by :current_userwith Devise Warden lookup
- Create
NotificationsChannel—app/channels/notifications_channel.rb:stream_for current_useron subscribe
Phase 2: Application Notifier & Delivery Methods
- Create
ApplicationNotifier—app/notifiers/application_notifier.rb::action_cable— real-time bell updates via Turbo Streams:email— conditional on user preferences; usesNotificationMailerbulk_deliver_by :slack— conditional on orgslack_webhook_url
- Create
NotificationMailer—app/mailers/notification_mailer.rb:- Generic
notification_emailmethod - Clean email template at
app/views/notification_mailer/notification_email.html.erb
- Generic
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
-
notifications_controller.jsStimulus controller — Toggle dropdown, unread badge, mark-as-read via@rails/request.js - 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
NotificationsController(top-level, cross-org):index— full page with Pagy pagination, read/unread filtermark_as_read— PATCH single notificationmark_all_as_read— PATCH collection
-
Notification partials —
_notification.html.erb,_dropdown.html.erb - Turbo Streams subscription —
turbo_stream_from current_user, :notificationsin layout
Phase 5: Announcement System
Admin::AnnouncementsController— Full CRUD +publishaction- Admin views — index, new, edit, _form
- Admin sidebar link — Add "Announcements" in
admin.html.erb - Admin routes —
resources :announcements do member { post :publish } end - Announcement banner —
_announcement_banner.html.erb— dismissible, uses localStorage announcement-bannerStimulus controller — dismiss + localStorage persistence- Render banner in layout — Above main content for signed-in users
Phase 6: Notification Preferences
Users::NotificationPreferencesController— index + update- Preferences view — Table with types as rows, channels as columns (toggles)
- Link in user dropdown — "Notification Preferences" in navbar
- Wire into notifiers —
config.ifchecks preferences per delivery channel
Phase 7: Organization Slack Integration
- Slack settings in org edit form — Text field for
slack_webhook_url(admin only) Organization::SlackNotifier— Namespaced model class for formatting/sending
Phase 8: Documentation Updates
- Update AGENTS.md — Add "Notifications with Noticed" section + add gem to approved list
- Update FILE_STRUCTURE.md — Add
app/notifiers/folder - Complete this doc — Architecture, guides, examples
Phase 9: Testing
- Fixtures —
noticed_events.yml,noticed_notifications.yml,announcements.yml,notification_preferences.yml - Model tests — Announcement, NotificationPreference, notification extensions
- Controller tests — NotificationsController, Admin::AnnouncementsController, Users::NotificationPreferencesController
- Notifier tests — Each notifier in
test/notifiers/(new directory) - 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/cipasses (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