Each section covers one change. Read the sections between your current version and the latest. The default docs describe the latest published release. Sections marked next describe main, which isn’t published yet.
From better-inbox (0.7)
The package was renamed from better-inbox to @byo-oss/notifications in 0.7. The API is the same. Change your imports:
// before
import { inbox } from "better-inbox";
// after
import { inbox } from "@byo-oss/notifications";0.6
0.6 adds a seenAt column to notification and two tables, notificationDelivery and inboxPresence. Re-run migrate (or generate) after upgrading, even if you don’t use email.
If you hand useInbox your own client object rather than authClient, add markSeen to it:
markSeen: ({ ids }) => authClient.inbox.markSeen({ ids })InboxFetchClient now requires it. Without it nothing is marked seen, and every unread email goes out.
0.8
- Every reference now declares
onDelete: "cascade":notification.userId,notificationDelivery.notificationId,notificationDelivery.userIdandinboxPresence.userId. better-auth’s ownmigratealready cascaded. If you generate migrations with drizzle or prisma, regenerate and apply them so your foreign keys match. - The plugin now deletes notifications when an organization is deleted or a member is removed or leaves. See Notify an organization. If you wired your own cleanup for this, you can remove it.
- The panel’s tabs render “Unread” and “All” as text, instead of
unreadandallcapitalized by CSS. Tests that match the tab text exactly need the capitalized form. - Deleting a user now also removes their rows on adapters without foreign keys (memory, MongoDB).
- New, additive:
exportNotificationsData,PermanentDeliveryError,labels(includingbadge),organizationCleanup, and@byo-oss/notifications/testing, which needs vitest (an optional peer).
0.8.1
- The package ships an agent skill,
byo-notifications, inskills/. Install it withnpx skills add byo-oss/notifications, or runnpx skills syncto pick it up fromnode_modules. See Set up with a coding agent. Nothing else changed.
0.9
No schema change and no new options. Behaviour changes:
- Export cursor.
exportNotificationsDatanow pages bycreatedAtand resumes after the last row it returned, instead of by offset. A row deleted mid-export can no longer make the next page skip one, so you don’t need to stop writes first. Rows inserted mid-export are included if theircreatedAtis at or after the page you’re on; a row backdated behind it (an import, say) is not. A cursor from an earlier release (a bare number) now throws, because it can’t be resumed safely. Start the export again with no cursor, and keep one copy of eachid, since rows exported before the restart come back. - Inside a better-auth transaction.
notify,deleteOrganizationData,deleteMemberDataandexportNotificationsDatanow run in the caller’s transaction (runWithTransaction), so their writes commit and roll back with it. Before, they wrote outside it, and on a one-connection database (PGlite) they waited on it forever.notifynever sends inside a transaction: emails due right away wait for the nextprocessDeliveriesrun. If you deferrednotifyuntil after the transaction yourself, you can call it directly now. wrapsEmail. A missing or malformed recipient now fails at once withPermanentDeliveryErrorinstead of retrying untilmaxAttempts. Bounces and suppressions still arrive later as SES events, not as send errors.
0.10
No schema change. To upgrade:
- Install
@better-auth/core. Thebetter-authpeer is now^1.7.7(it was^1.6.23), and@better-auth/core^1.7.7is a new required peer, used for the after-commit hooks behind events. Install it at the same version asbetter-auth, and add it to any package that imports the auth client or server.@byo-oss/coreis a dependency and installs itself. - Don’t forward
notifications.*intonotify. If anonEventhandler turns events into notifications, skip the plugin’s own types first.notifyrejects them (RESERVED_NOTIFICATION_TYPE), and arouteskey with that prefix throws when the plugin is created. Events are in Events. - Check
batchSize.inbox()throws unlessbatchSizeis an integer from 1 to 1000, the rangeprocessDeliveriesalready accepts forlimit.
Breaking:
- Reserved types.
notifyrejects types that start withnotifications., the prefix for the plugin’s own events. - Stale sends with no attempts left. A sweep now fails them one row at a time, at most
limitrows per sweep, instead of all at once. When it stops at that cap after failing rows there,processDeliveriesreturnshasMore: true, sohasMorecan be true with nothing due to send. Loop whilehasMoreis true. Exhausted rows past the cap staysendinguntil a later sweep fails them; they’re never retried pastmaxAttempts. Rows the sweep can’t update (row-level security, say) still count againstlimit, so oncelimitof them (up to 180) come first in scan order, later exhausted rows wait.
New:
onEvent, withnotifications.notification.createdandnotifications.delivery.failed, delivered after commit. See Events. Of the calls that emit events, onlynotifyjoins a caller’s transaction. CallprocessDeliveries(orsweepAll) outside one: its writes commit as they happen, and itsdelivery.failedevents fire at once.byohooks for@byo-oss/core(sweepAll,deleteOrganizationData,deleteMemberData). The cleanups are left out underorganizationCleanup: false.NOTIFICATIONS_EVENT_TYPESand the event types (NotificationsEvent,NotificationCreatedEvent,DeliveryFailedEvent), exported for routing on an event bus.
0.11
No schema change and no new inbox() options. To upgrade:
- New:
mailerChannel.mailerChannel({ mailer, replyTo?, render? }), from@byo-oss/notifications/mailer, sends email through anyMailerfrom@byo-oss/mailer. The mailer owns the sender, so the channel has nofrom. Install@byo-oss/mailer(0.1.1 or later) in your app, since your code imports its adapter, along with the SDK your mailer needs, such as@wraps.dev/emailforwrapsMailer. wrapsEmailis deprecated. It still works and is unchanged, but it will be removed in a later breaking release. Move tomailerChannelwithwrapsMailer.fromandrendermap across as below;clientmaps only if it returns{ messageId }(see the differences):
import { WrapsEmail } from "@wraps.dev/email";
import { mailerChannel } from "@byo-oss/notifications/mailer";
import { wrapsMailer } from "@byo-oss/mailer/wraps";
// before
channels: { email: wrapsEmail({ from: "Acme <notify@acme.com>", client: new WrapsEmail({ region }), render }) },
// after
channels: { email: mailerChannel({ mailer: wrapsMailer({ from: "Acme <notify@acme.com>", client: new WrapsEmail({ region }) }), render }) },- Check the differences.
wrapsMailerdiffers fromwrapsEmailin four ways. It fails permanently on every@wraps.dev/emailValidationError(a malformedfrom,replyToor recipient, or an empty subject) and on SESMessageRejectedandInvalidParameterValue, wherewrapsEmailfails only a bad recipient at once and retries the rest, so fixing the config no longer rescues deliveries already queued. It throws aTypeErrorwhen it is created iffromis empty, wherewrapsEmailfailed at the first send. Itsclientmust resolve{ messageId }, so a fake or wrapper that returns anything else, such as aWrapsEmailSender, needs changing. And it sends a non-ASCII display name infromRFC 2047 encoded, which needs@byo-oss/mailer0.1.1 or later (0.1.0 sends it as given). See Wraps.