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.

✨ Search Document