Add and send in-app notifications in an Avo admin panel with the avo-notifications add-on — a bell dropdown in the navbar plus a full notifications resource, sent with `Avo::Notifications.send(to:, title:, ...)` from actions, model callbacks, or jobs, with severity levels, type tags, up to 3 action buttons, per-recipient read/saved/done state, and optional real-time delivery over ActionCable. Use when the user wants to notify admins when an order comes in, tell a user their export/report is ready, alert admins about a signup spike, ping a reviewer when something needs approval, add an in-app inbox / notification center / bell badge, let admins mark messages read, push a maintenance message to all users, add Approve/Reject buttons to an alert, or get real-time notifications without a page refresh.
Add and send in-app notifications in an Avo admin panel with the avo-notifications add-on — a bell dropdown in the navbar plus a full notifications resource, sent with `Avo::Notifications.send(to:, title:, ...)` from actions, model callbacks, or jobs, with severity levels, type tags, up to 3 action buttons, per-recipient read/saved/done state, and optional real-time delivery over ActionCable. Use when the user wants to notify admins when an order comes in, tell a user their export/report is ready, alert admins about a signup spike, ping a reviewer when something needs approval, add an in-app inbox / notification center / bell badge, let admins mark messages read, push a maintenance message to all users, add Approve/Reject buttons to an alert, or get real-time notifications without a page refresh.
Avo Notifications is a paid add-on that gives an Avo admin panel an in-app notification system: a bell icon with an unread badge in the navbar, a dropdown of recent items, and a full Avo resource for browsing/managing them. You create notifications from anywhere in the app with one call — Avo::Notifications.send(...) — and each one is delivered to a specific recipient, optionally in real time over ActionCable.
Read the guide before implementing anything non-trivial, and the reference whenever you need the exact signature, default, or validation of a parameter.
Install
The add-on ships an installer that generates a migration, an initializer, an Avo resource, and a controller. Do the steps in order:
Generates: a migration for the avo_notifications_notifications table, config/initializers/avo_notifications.rb, app/avo/resources/avo_notification.rb, and app/controllers/avo/avo_notifications_controller.rb.
Migrate:
bin/rails db:migrate
Include the concern in the recipient model (usually User):
This adds an avo_notifications association plus the per-user read-state helpers.
The bell component then mounts itself in the navbar automatically — no view wiring needed.
When this applies
Reach for this add-on when the request is "let the admin see / send an in-app message" — a bell, an inbox, an alert, an approval ping:
Request
What to build
"Notify admins when an order comes in", "ping a reviewer when something needs approval"
send to a user or a relation, usually from a model callback or an action
"Tell the user their export/report is ready"
send from the background job, level: :success, a url: to the result
"Alert admins about a signup spike"
send to User.where(admin: true)
"Push a maintenance message to everyone"
send(to: :all, level: :warning)
"Add Approve/Reject buttons to an alert"
send with buttons: (up to 3)
"Add an in-app inbox / notification center / unread badge"
install the add-on — the bell + resource are automatic
"Let admins mark messages read / saved / done"
the bell hover actions + resource bulk actions are built in; drive them in code with the state API
"Real-time notifications without a page refresh"
keep config.realtime = true and configure ActionCable
If the notification is sent from an Avo action (bulk-approve, then notify the owner), build the action with the avo-actions skill and call Avo::Notifications.send inside its handle.
Workflow
1. Send a notification
to: and title: are the only required arguments. Everything else is optional.
Avo::Notifications.send(
to: user,
title:"Your export is ready",
body:"The report finished processing.",
level::success,
url:"/admin/exports/42"
)
level: — :info (default), :success, :warning, or :error; sets the icon and color.
notification_type: — a freeform tag ("mention", "system", "billing") rendered as a small label on the row.
url: — where clicking the title navigates.
sender: — the record that sent it, for attribution (its name is resolved via user_display_name_method).
buttons: — up to 3 action buttons, each { label:, url:, method: } (method defaults to "get").
2. Choose who receives it
The to: argument fans out into one row per recipient, so state is tracked per user:
# One user — returns the notificationAvo::Notifications.send(to:@user, title:"Welcome!")
# A relation — one row each, returns an ArrayAvo::Notifications.send(to:User.where(admin:true), title:"New signup spike")
# Everyone (every record of `user_class`) — returns an ArrayAvo::Notifications.send(to::all, title:"Maintenance tonight at 10 PM", level::warning)
3. Send from where the event happens
From an Avo action's handle:
# app/avo/actions/approve_project.rbdefhandle(query:, current_user:, **args)
query.each do |project|
project.approve!
Avo::Notifications.send(
to: project.owner,
title:"Your project was approved",
body:"#{current_user.name} approved '#{project.name}'.",
level::success,
sender: current_user,
url:"/admin/projects/#{project.id}"
)
end
succeed "#{query.count} project(s) approved."end
From a model callback:
# app/models/order.rb
after_update :notify_status_changedefnotify_status_changereturnunless saved_change_to_status?
Avo::Notifications.send(to: user, title:"Order ##{id} is now #{status}", url:"/admin/orders/#{id}")
end
From a background job — send after the work finishes, with a url: to the result:
Avo::Notifications.send(to: user, title:"Your export is ready", level::success, url: export.download_url)
Avo::Notifications.for_user(user, limit:10) # the inbox (not-done, newest first)Avo::Notifications.unread_count(user) # drives the bell badgeAvo::Notifications.mark_all_as_read(user)
After the HasNotifications concern, the model exposes the same directly: user.unread_avo_notifications_count, user.mark_all_avo_notifications_read!, user.avo_notification_unread?(notification).
6. Configure (optional)
Every option in config/initializers/avo_notifications.rb has a sensible default; an empty block works out of the box.
Avo::Notifications.configure do |config|
config.ttl = 30.days # kept this long before cleanup can delete
config.realtime = true# real-time delivery via ActionCable
config.dropdown_limit = 10# rows in the bell dropdown
config.user_class = "User"# model used to resolve `to: :all`
config.user_display_name_method = :name# method for sender attributionend
7. Schedule cleanup
Every notification gets an expires_at from ttl. Delete expired rows with the rake task — schedule it daily:
Send — Avo::Notifications.send(to:, title:, body: nil, level: :info, notification_type: nil, url: nil, sender: nil, buttons: nil) (alias notify). Return value depends on to: — a single record returns the notification; an Array or :all returns an Array.
No global notifications. Every recipient gets a row. to: fans out: a relation or :all returns an Array; a single record returns one notification. Don't expect a shared row you can mutate once for everyone.
A blank to: raises; an empty Array is a no-op.to: nil or to: "" raises Avo::Notifications::Error — notifications must be addressed. to: [] deliberately sends nothing and returns [], so send(to: User.where(...)) on a relation that matches no one is safe.
title is required and capped at 255 chars. Blank or over 255 raises. body is the place for longer text.
level must be one of the four. Any other symbol raises — validate user-supplied levels before passing them.
buttons validation is strict. Max 3; each must be a Hash with a non-blank label and url; method, if present, must be a supported verb. Anything else raises.
Real-time is best-effort. With realtime = true but no ActionCable configured (or no running server), broadcasting is silently skipped — notifications still land and appear on the next page load. Broadcast errors are logged, never raised. Don't rely on real-time as the only delivery path.
The generated resource is hidden from the sidebar by design (self.visible_on_sidebar = false) — it's reached through the bell's "View all" link. It's a plain Avo resource: edit app/avo/resources/avo_notification.rb to add fields/filters/actions or flip it onto the sidebar.
Sending from an action is the avo-actions skill's job. Build the action there; just call Avo::Notifications.send inside its handle.
Report
When done, tell the user:
Whether you installed the add-on (gem + generator + migrate + concern) or just added sending code to an existing install.
Where notifications are sent from (which action/model callback/job) and to whom (to: a record / relation / :all).
The shape of each notification: level, any notification_type, url, sender, and buttons.
Any follow-ups the user still owns: adding include Avo::Notifications::HasNotifications to the recipient model, configuring ActionCable for real time, tuning config/initializers/avo_notifications.rb, or scheduling avo_notifications:cleanup.