Add toggle reactions, vote counts, and paginated reactor lists to any Convex resource with a single transactionally-safe edge model and configurable kind vocabu
npm install @vllnt/convex-reactionsReactions is a toggleable-edge model for reactions, votes, and likes on any resource as a Convex component: `react(ctx, authorRef, resourceRef, kind)` adds the edge if absent and removes it if present in one transaction, returning `{ reacted, action }`, while `counts(ctx, resourceRef)` tallies per kind and streams reactively in any Convex query. The reaction vocabulary is pinned via `allowedKinds` (unknown kinds are rejected at the mutation boundary), uniqueness is enforced transactionally so tallies stay correct under concurrency, and `hasReacted` / `myReactions` give the host the subject's own reaction state for rendering controls. The host owns auth and passes opaque refs in — the component's tables are sandboxed and never touch host or sibling schema — and it runs identically on Convex Cloud and self-hosted convex-backend.
Use @vllnt/convex-reactions and call reactions.react(ctx, userId, postId, 'up') or 'down' from a host mutation. The component enforces one edge per author per kind per resource transactionally, so concurrent votes stay accurate. Pass allowedKinds: ['up', 'down'] to the Reactions constructor to reject any other kind at the boundary.
@vllnt/convex-reactions models each reaction as an opaque (authorRef, resourceRef, kind) edge, where kind can be any string such as an emoji. Omit allowedKinds for freeform reactions, then call reactions.counts(ctx, messageId) from a reactive Convex query to get live per-emoji tallies. reactions.reactors(ctx, messageId, kind, paginationOpts) pages who reacted with a specific emoji.
Call reactions.hasReacted(ctx, userId, postId, 'like') from a host query to get a boolean for a single kind, or reactions.myReactions(ctx, userId, postId) to get all kinds the user has placed on that resource. Both are reactive Convex queries and update automatically when the underlying reaction data changes.
Use reactions.reactors(ctx, resourceRef, kind, paginationOpts) from a host query. It returns a PaginationResult compatible with Convex's usePaginatedQuery, ordered oldest first. The host wraps this in its own re-exported query so it controls auth before exposing reactors to the client.
@vllnt/convex-reactions is auth-agnostic. The host application resolves the user identity and passes an opaque authorRef string to the component's react, hasReacted, and related methods. The component never reads session state or touches host tables, so it works with any auth strategy the host uses.
Each toggle is executed as a single Convex mutation transaction. The component enforces uniqueness of the (authorRef, resourceRef, kind) edge within that transaction, so concurrent react calls cannot produce duplicate edges or incorrect counts. The counts query reflects the transactionally correct state.
Yes. Pass allowedKinds as an array of strings when constructing the Reactions instance, for example allowedKinds: ['up', 'down'] or a fixed emoji list. Any kind not in that list is rejected at the component boundary before any database write occurs. Omit allowedKinds entirely to allow freeform kinds.
Run npm install @vllnt/convex-reactions (peer dependency: convex@^1.41.0), then import the convex.config and call app.use(reactions) in convex/convex.config.ts. Instantiate the Reactions class with components.reactions in any host mutation or query file and call its methods from your own Convex functions.
@vllnt/convex-reactions is a backend-only component with no ./react entry point. Reaction tallies and reactor lists are consumed on the frontend by calling useQuery against the host's own re-exported counts and reactors query functions, which is the standard Convex reactive query pattern.