{"id":51317907,"url":"https://github.com/vllnt/convex-notifications","last_synced_at":"2026-07-01T09:31:16.660Z","repository":{"id":364821481,"uuid":"1268769160","full_name":"vllnt/convex-notifications","owner":"vllnt","description":"Per-subject directed inbox — deliver notifications with read/unread state and fan-out as a Convex component","archived":false,"fork":false,"pushed_at":"2026-06-14T15:47:07.000Z","size":106,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T17:23:36.384Z","etag":null,"topics":["convex","convex-component","inbox","notifications","typescript","vllnt"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@vllnt/convex-notifications","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vllnt.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"bntvllnt"}},"created_at":"2026-06-13T23:12:57.000Z","updated_at":"2026-06-14T15:55:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vllnt/convex-notifications","commit_stats":null,"previous_names":["vllnt/convex-notifications"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/vllnt/convex-notifications","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-notifications","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-notifications/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-notifications/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-notifications/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vllnt","download_url":"https://codeload.github.com/vllnt/convex-notifications/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-notifications/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35001648,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["convex","convex-component","inbox","notifications","typescript","vllnt"],"created_at":"2026-07-01T09:31:15.814Z","updated_at":"2026-07-01T09:31:16.644Z","avatar_url":"https://github.com/vllnt.png","language":"TypeScript","funding_links":["https://github.com/sponsors/bntvllnt"],"categories":[],"sub_categories":[],"readme":"\u003c!-- Badges --\u003e\n[![convex-component](https://img.shields.io/badge/convex-component-EE342F.svg)](https://www.convex.dev/components)\n[![npm](https://img.shields.io/npm/v/@vllnt/convex-notifications.svg)](https://www.npmjs.com/package/@vllnt/convex-notifications)\n[![CI](https://github.com/vllnt/convex-notifications/actions/workflows/ci.yml/badge.svg)](https://github.com/vllnt/convex-notifications/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/@vllnt/convex-notifications.svg)](./LICENSE)\n\n# @vllnt/convex-notifications\n\nA per-subject directed inbox — notifications with read/unread state and fan-out, as a Convex component.\n\n```ts\nconst inbox = new Notifications(components.notifications);\nawait inbox.deliver(ctx, recipients, \"mention\", { actor }); // one row per recipient\nawait inbox.list(ctx, subjectRef, paginationOpts, { unreadOnly: true }); // reactive inbox\nawait inbox.unreadCount(ctx, subjectRef);\nawait inbox.markRead(ctx, notificationId);\n```\n\n## Features\n\n- **Deliver + fan-out** — `deliver(subjectRef | subjectRefs[], type, payload?)` writes one notification per recipient and mints a `notificationId` for each.\n- **Read state** — every notification arrives unread; `markRead` (idempotent) and `markAllRead` (bounded, self-rescheduling) clear it.\n- **Inbox queries** — `list` pages newest-first (all or unread-only), `unreadCount` totals the unread, `get` fetches one. Reactive in a Convex query.\n- **Subject-bounded reads** — a `list`/`unreadCount` is keyed to one subject and never spans another's inbox.\n- **Server-sourced time** — `createdAt`/`readAt` are stamped from the server clock; a caller can't supply a timestamp.\n- **Typed, opaque payload** — `Notifications\u003cTPayload\u003e` types the stored `payload`; `payloadValidator` narrows it at the boundary.\n- **Bounded purge + cron** — a daily cron sweeps read notifications past retention in batches; unread are never purged.\n- **Mount-safe** — correct under multiple named `app.use` mounts; each instance is an isolated sandbox.\n\n## Installation\n\n```bash\npnpm add @vllnt/convex-notifications\n```\n\nPeer dependency: `convex@^1.41.0`.\n\n## Usage\n\n```ts\n// convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport notifications from \"@vllnt/convex-notifications/convex.config\";\n\nconst app = defineApp();\napp.use(notifications);\nexport default app;\n```\n\n```ts\n// convex/notify.ts — host owns auth; pass opaque subjectRefs in.\nimport { components } from \"./_generated/api\";\nimport { mutation, query } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { Notifications } from \"@vllnt/convex-notifications\";\n\nconst inbox = new Notifications\u003c{ actor: string }\u003e(components.notifications, {\n  payloadValidator: v.object({ actor: v.string() }).parse, // narrow at the boundary\n});\n\nexport const notifyMention = mutation({\n  args: { recipients: v.array(v.string()), actor: v.string() },\n  handler: (ctx, { recipients, actor }) =\u003e\n    inbox.deliver(ctx, recipients, \"mention\", { actor }),\n});\n\nexport const myUnread = query({\n  args: { subjectRef: v.string(), paginationOpts: v.any() },\n  handler: (ctx, { subjectRef, paginationOpts }) =\u003e\n    inbox.list(ctx, subjectRef, paginationOpts, { unreadOnly: true }),\n});\n```\n\n## API Reference\n\n| Method | Kind | Result |\n|--------|------|--------|\n| `deliver(ctx, subjects, type, payload?)` | mutation | `{ notificationIds }` (`subjects`: one ref or an array) |\n| `markRead(ctx, notificationId)` | mutation | `null` |\n| `markAllRead(ctx, subjectRef, opts?)` | mutation | `number` (marked in the first bounded pass) |\n| `get(ctx, notificationId)` | query | `NotificationView \\| null` |\n| `list(ctx, subjectRef, paginationOpts, opts?)` | query | `PaginationResult\u003cNotificationView\u003e` (`opts`: `{ unreadOnly? }`) |\n| `unreadCount(ctx, subjectRef)` | query | `number` |\n| `purge(ctx, opts?)` | mutation | `number` (read notifications removed in the first bounded pass) |\n\nFull reference: [docs/API.md](docs/API.md).\n\n## React\n\nBackend-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.\n\n## Security\n\n- Auth-agnostic — the host resolves identity and decides who may deliver to or read a `subjectRef`.\n- Tables sandboxed — reached only through the exported functions; never touches host or sibling tables.\n- Subject-bounded reads + server-sourced time; `subjectRef` / `payload` stay opaque to the component.\n\nSee [docs/API.md](docs/API.md).\n\n## Testing\n\n```bash\npnpm test           # single run\npnpm test:coverage  # enforced 100% on covered files\n```\n\nTests run against the real component runtime via `convex-test` (`@edge-runtime/vm`), not mocks.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## Author\n\nBuilt by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt)\n\nPart of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com)\n\nIf this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt).\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvllnt%2Fconvex-notifications","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvllnt%2Fconvex-notifications","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvllnt%2Fconvex-notifications/lists"}