# Notifications Vllnt

Per-subject directed inbox — deliver notifications with read/unread state and fan-out as a Bijection component

- Package: @bijectionhq/notifications-vllnt
- Category: Messaging
- Status: Reference
- License: MIT
- Bijection source (repository access required): https://github.com/bijectionhq/notifications-vllnt/tree/30678273bded3b391b72610c93eac590861b5a2e
- Upstream: https://github.com/vllnt/convex-notifications/tree/574a29f2bda9d07a0f016c1edb38154b2a3e38e0
- Documentation: https://docs.bijection.com/components/using
- Directory: https://bijection.com/components/notifications-vllnt

## Get the source

Bijection repository access is required. Request access at https://bijection.com/contact.

```sh
git clone https://github.com/bijectionhq/notifications-vllnt.git
cd notifications-vllnt
git checkout 30678273bded3b391b72610c93eac590861b5a2e
```

## Before using this component

Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute.

## Upstream package documentation

Recorded 2026-10-23. This describes @vllnt/convex-notifications; check the Bijection adaptation separately.

@vllnt/convex-notifications stores a notification inbox for each opaque subject reference. The application defines notification types and a typed payload, so the same component can serve different product surfaces.

deliver writes one notification per recipient, including fan-out to several recipients. list and unreadCount support inbox screens and badges through host queries. markRead and bounded markAllRead clear unread state. Retention cleanup removes eligible read notifications.

The host authenticates recipients, gates access, and chooses which events create notifications. This is an in-app data store; it does not send APNs/FCM push notifications, SMS, or email. Device delivery and channel preferences belong in your integration.

Maintained by [bntvllnt](https://bntvllnt.com) at [VLLNT](https://vllnt.com). Find the author on [GitHub](https://github.com/bntvllnt) and [X](https://x.com/bntvllnt).

### Use cases

- Team tools: notify people about mentions, assignments, or shared-resource updates.
- Mobile apps: retain account and product notices in an in-app inbox.
- Learning platforms: display lesson, feedback, or course activity notices.
- Games and communities: surface invitations, replies, or host-defined milestones.

### How it works

deliver writes one notification per recipient, including fan-out to several recipients. list and unreadCount support inbox screens and badges through host queries. markRead and bounded markAllRead clear unread state. Retention cleanup removes eligible read notifications.

The host authenticates recipients, gates access, and chooses which events create notifications. This is an in-app data store; it does not send APNs/FCM push notifications, SMS, or email. Device delivery and channel preferences belong in your integration.

### Upstream README

# @vllnt/convex-notifications

A per-subject directed inbox — notifications with read/unread state and fan-out, as a Convex component.

```ts
const inbox = new Notifications(components.notifications);
await inbox.deliver(ctx, recipients, "mention", { actor }); // one row per recipient
await inbox.list(ctx, subjectRef, paginationOpts, { unreadOnly: true }); // reactive inbox
await inbox.unreadCount(ctx, subjectRef);
await inbox.markRead(ctx, notificationId);
```

## Features

- **Deliver + fan-out** — `deliver(subjectRef | subjectRefs[], type, payload?)` writes one notification per recipient and mints a `notificationId` for each.
- **Read state** — every notification arrives unread; `markRead` (idempotent) and `markAllRead` (bounded, self-rescheduling) clear it.
- **Inbox queries** — `list` pages newest-first (all or unread-only), `unreadCount` totals the unread, `get` fetches one. Reactive in a Convex query.
- **Subject-bounded reads** — a `list`/`unreadCount` is keyed to one subject and never spans another's inbox.
- **Server-sourced time** — `createdAt`/`readAt` are stamped from the server clock; a caller can't supply a timestamp.
- **Typed, opaque payload** — `Notifications<TPayload>` types the stored `payload`; `payloadValidator` narrows it at the boundary.
- **Bounded purge + cron** — a daily cron sweeps read notifications past retention in batches; unread are never purged.
- **Mount-safe** — correct under multiple named `app.use` mounts; each instance is an isolated sandbox.

## Installation

```bash
pnpm add @vllnt/convex-notifications
```

Peer dependency: `convex@^1.41.0`.

## Usage

```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import notifications from "@vllnt/convex-notifications/convex.config";

const app = defineApp();
app.use(notifications);
export default app;
```

```ts
// convex/notify.ts — host owns auth; pass opaque subjectRefs in.
import { components } from "./_generated/api";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { Notifications } from "@vllnt/convex-notifications";

const inbox = new Notifications<{ actor: string }>(components.notifications, {
  payloadValidator: v.object({ actor: v.string() }).parse, // narrow at the boundary
});

export const notifyMention = mutation({
  args: { recipients: v.array(v.string()), actor: v.string() },
  handler: (ctx, { recipients, actor }) =>
    inbox.deliver(ctx, recipients, "mention", { actor }),
});

export const myUnread = query({
  args: { subjectRef: v.string(), paginationOpts: v.any() },
  handler: (ctx, { subjectRef, paginationOpts }) =>
    inbox.list(ctx, subjectRef, paginationOpts, { unreadOnly: true }),
});
```

## API Reference

| Method | Kind | Result |
|--------|------|--------|
| `deliver(ctx, subjects, type, payload?)` | mutation | `{ notificationIds }` (`subjects`: one ref or an array) |
| `markRead(ctx, notificationId)` | mutation | `null` |
| `markAllRead(ctx, subjectRef, opts?)` | mutation | `number` (marked in the first bounded pass) |
| `get(ctx, notificationId)` | query | `NotificationView \| null` |
| `list(ctx, subjectRef, paginationOpts, opts?)` | query | `PaginationResult<NotificationView>` (`opts`: `{ unreadOnly? }`) |
| `unreadCount(ctx, subjectRef)` | query | `number` |
| `purge(ctx, opts?)` | mutation | `number` (read notifications removed in the first bounded pass) |

Full reference: [docs/API.md](docs/API.md).

## React

Backend-only — no `./react` entry. An inbox, unread count, and unread list are ordinary reactive `useQuery` calls over the host's own re-exported `list` / `unreadCount` / `get` refs.

## Security

- Auth-agnostic — the host resolves identity and decides who may deliver to or read a `subjectRef`.
- Tables sandboxed — reached only through the exported functions; never touches host or sibling tables.
- Subject-bounded reads + server-sourced time; `subjectRef` / `payload` stay opaque to the component.

See [docs/API.md](docs/API.md).

## Testing

```bash
pnpm test           # single run
pnpm test:coverage  # enforced 100% on covered files
```

Tests run against the real component runtime via `convex-test` (`@edge-runtime/vm`), not mocks.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## Author

Built by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt)

Part of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com)

If this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt).

## License

MIT — see [LICENSE](LICENSE).