howdy_notifications
Notifications for howdy apps: an inbox kept until read, pushed live to every page the user has open, with email, digests and Web Push.
gleam add howdy_notifications@1
Sending
import howdy/notifications
let notifier = notifications.new(db)
notifications.draft("comment.created", "Ana commented on your post")
|> notifications.body("Looks good to me!")
|> notifications.link("/posts/42#comment-7")
|> notifications.send(notifier, to: post.author_id)
The notification is saved first, in the package’s tables in your database
(notifications.schema() is its migrations, for PostgreSQL and SQLite,
alongside jobs.schema()),
then announced to the recipient’s open pages on every node of the cluster.
A page that was closed reads the inbox when it next connects, so nothing is
missed.
Showing them
notifications.routes(notifier, at: "/notifications", identify:) serves the
inbox as JSON, a stream of server-sent events, and routes to mark
notifications read, which also work as plain HTML forms. howdy_ui’s bell
uses them: a button with the unread count and a panel of the newest, kept
up to date as notifications arrive, each shown as a toast. A live view can
subscribe to a user’s inbox instead.
Email and Web Push
notifications.new(db)
|> notifications.with_defaults(fn(kind) {
case kind {
"security." <> _ -> Delivery(..default_delivery, email: EmailNow)
_ -> Delivery(..default_delivery, email: EmailDigest)
}
})
|> notifications.with_email(mailer, address:, render:)
|> notifications.email_after(120)
|> notifications.with_digest(every: 86_400, render: digest_email)
|> notifications.with_push(web_push.deliver(push))
Each user can choose per kind, with set_preference. Email waits to see
whether the notification is read in the app first, and digest kinds are
gathered into one email per period. howdy/notifications/web_push sends
the browser’s own notifications, encrypted for each browser (RFC 8291) and
signed with your VAPID keys (RFC 8292); gleam run -m howdy/notifications/web_push makes the keys. Other push services, such as
ones sending to phones, are added by name with with_push_service, and
each is sent and retried on its own.
Live announcements reach every node of an Erlang cluster. For servers
that are not clustered, add notifications.across_servers(notifier) to each
server’s supervisor: on PostgreSQL it sends them through the database, so
pages open on any server hear them.
Email and push are background jobs, queued in the
transaction that saves the notification: add them to your app’s runner with
jobs.runner(db) |> notifications.handle_jobs(notifier). They are retried
when they fail, and runners on several nodes share the work.
See the notifications docs and
examples/notifications.