
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
better-inbox
Advanced tools
In-app notifications for better-auth apps. One plugin, one migration, one component — notifications live in your database, addressed to your users.
In-app notifications for better-auth apps. One plugin, one migration, one component — notifications live in your database, addressed to your users.
npm install better-inbox

notification table lives in your own database via better-auth's adapter (drizzle, prisma, kysely — whatever you already use).notify({ userId }) uses your better-auth user ids. Delete a user, their notifications cascade.notify({ organizationId, roles: ["owner", "admin"] }).<InboxButton /> gives you the bell, unread badge, and inbox panel, styled with shadcn CSS variables (works with any shadcn theme, zero runtime dependencies, no shadcn required).Community plugin — not affiliated with the better-auth team.
1. Add the plugin to your better-auth config:
// lib/auth.ts
import { betterAuth } from "better-auth";
import { inbox } from "better-inbox";
export const auth = betterAuth({
// ...your existing config
plugins: [inbox()],
});
2. Migrate. The plugin declares the notification table; your existing better-auth workflow creates it:
npx @better-auth/cli migrate --config lib/auth.ts # or: generate, for drizzle/prisma
(Point --config at your better-auth config if the CLI doesn't auto-detect it.)
3. Add the client plugin:
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { inboxClient } from "better-inbox/client";
export const authClient = createAuthClient({
plugins: [inboxClient()],
});
4. Drop in the component (any client component, e.g. your navbar):
"use client";
import { InboxButton } from "better-inbox/react";
import { useRouter } from "next/navigation";
import { authClient } from "@/lib/auth-client";
export function Navbar() {
const router = useRouter();
return <InboxButton client={authClient} onNavigate={(href) => router.push(href)} />;
}
5. Send notifications from any server code — a server action, a webhook handler, a cron job:
await auth.api.notify({
body: {
userId: comment.authorId,
type: "comment.reply",
title: `${user.name} replied to your comment`,
href: `/posts/${post.id}#comment-${comment.id}`,
},
});
notify is server-only: callable through auth.api, never exposed as an HTTP route.
With the better-auth organization plugin enabled, address an org instead of a user. One notification is created per member (fan-out on write), each stamped with organizationId:
// Stripe webhook: payment failed → tell the org's admins
await auth.api.notify({
body: {
organizationId: subscription.metadata.orgId,
roles: ["owner", "admin"], // optional — omit to notify every member
type: "billing.payment_failed",
title: "Payment failed — update your card",
href: "/settings/billing",
},
});
Fan-out is capped at 1,000 members by default (inbox({ maxFanout }) to change). Over the cap, notify throws instead of hammering your database.
better-inbox)| Endpoint | Access | Description |
|---|---|---|
auth.api.notify({ body }) | server-only | Create notification(s). Exactly one of userId / organizationId; optional roles, body, href, data (JSON) |
auth.api.listNotifications({ query }) | session | { filter?: "unread"|"all", limit? (≤100), offset?, organizationId? } → { notifications, hasMore } |
auth.api.markRead({ body: { id } }) | session | Marks one of the caller's notifications read |
auth.api.markAllRead({ body }) | session | Optional { organizationId } scope |
auth.api.unreadCount({ query }) | session | → { count } |
Every session endpoint is scoped to the caller — users can only ever see and mutate their own notifications.
better-inbox/react)<InboxButton client={authClient} /> — bell + badge + panel. Props: onNavigate, renderItem, pollInterval (default 30s; unread count only, full refresh on window focus and panel open), filter, organizationId, className. Opens on an Unread tab (fetched server-side with filter: "unread") with All second; passing filter locks the list and hides the tabs.useInbox(client, options) — build your own UI: { notifications, unreadCount, isLoading, hasMore, loadMore, markRead, markAllRead, refresh }. Options: pollInterval, pageSize, filter, organizationId.filter: "unread" fetches only unread rows, so a page is a full page — useInbox(authClient, { filter: "unread", pageSize: 5 }) yields five unread notifications instead of five rows you then filter down to however many are unread. Rows you mark read stay in the list until the next refresh(), and loadMore() accounts for them when paging.<InboxPanel inbox={useInbox(...)} /> — the panel without the bell. Pass view + onViewChange to control its tabs; uncontrolled it defaults to Unread.Add an index for the list query once you have real traffic — plugin schemas can't declare indexes, so it's one manual line, e.g. drizzle:
index("notification_user_created_idx").on(table.userId, table.createdAt)
Email/push channels, preferences, digests, realtime websockets — out of scope for v0.1 by design. Polling + focus refetch covers the badge honestly. If you need multi-channel delivery pipelines, look at Novu or betternotify; better-inbox is the thin, DB-owned layer for the in-app half.
MIT
FAQs
In-app notifications for better-auth apps. One plugin, one migration, one component — notifications live in your database, addressed to your users.
The npm package better-inbox receives a total of 1,339 weekly downloads. As such, better-inbox popularity was classified as popular.
We found that better-inbox demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.