Compatibility: convex@^1.41.0
Construct the client with the mounted component and an optional host body validator:
import { Comments } from "@vllnt/convex-comments";
import { v } from "convex/values";
const comments = new Comments<MyBody>(components.comments, {
bodyValidator: v.string().parse, // narrow the stored body (plain text here)
});Comments<TBody = unknown> is generic over the host's opaque comment body type.
All methods take the host ctx (a query or mutation context) as the first
argument.
Time is server-sourced. Every handler stamps createdAt/updatedAt/editedAt
from Date.now() itself; no method accepts a caller-supplied clock.
Authorship. edit, remove, and resolve require the original authorRef;
a non-author caller is rejected (NOT_AUTHOR). The component enforces only this
authorship invariant — the host owns who may post or moderate.
Validation. When bodyValidator is set it runs at the client boundary: over
the value written by post / edit (before storage) and over the value returned
by get / list (on read). It must return the typed value or throw. Omit it to
leave the opaque body unvalidated.
Post a comment on resourceRef authored by authorRef and return its id. The
comment is inserted open with createdAt/updatedAt stamped from the server
clock. resourceRef and authorRef are opaque host refs; body is opaque host
data validated against bodyValidator before storage. parentId, when given,
threads the comment as a reply.
A parentId that names no comment throws
ConvexError({ code: "PARENT_NOT_FOUND" }); a parent on a different resource
throws PARENT_MISMATCH; a soft-deleted parent throws PARENT_DELETED.
Replace a comment's body, recording editedAt/updatedAt from the server clock.
The new body is validated against bodyValidator before storage. Only the
original author may edit (NOT_AUTHOR); a soft-deleted comment cannot be edited
(DELETED); a missing id throws NOT_FOUND.
Soft-delete a comment: the row is kept (its replies are preserved) but moved to
deleted status with its body cleared, so the prune cron can sweep it after the
retention window. Idempotent — re-deleting a deleted comment is a no-op. Only the
original author may delete (NOT_AUTHOR); a missing id throws NOT_FOUND.
Toggle a comment's resolved state. resolved defaults to true (open → resolved); pass false to reopen. Idempotent — setting the state it already holds
is a no-op. Only the original author may resolve (NOT_AUTHOR); a soft-deleted
comment cannot be resolved (DELETED); a missing id throws NOT_FOUND.
opts: { before?: number; batch?: number } (defaults: before = Date.now(),
batch = 200).
Delete up to batch soft-deleted comments whose updatedAt < before, oldest
first (via the by_status_updated index), and return the count removed in the
first pass. Open and resolved comments are never pruned. If a full batch was
removed the sweep self-reschedules through the component scheduler until the
deleted tail is clean. Idempotent — safe to run anytime. A built-in daily cron
drives it automatically; call prune directly only for an extra or custom-cadence
sweep.
The current view of commentId, or null if no such comment is held.
CommentView is { commentId, resourceRef, authorRef, parentId?, body?, status, editedAt?, createdAt, updatedAt }; body is narrowed by the host validator when
set, and is undefined once the comment is soft-deleted.
opts: { parentId?: string; includeDeleted?: boolean }.
Page comments on one resourceRef, oldest first. By default pages the resource's
top level (parentId === undefined, excluding soft-deleted) via the by_resource
index. Pass opts.parentId to page one comment's direct replies via the
by_resource_parent index, or opts.includeDeleted to surface removed comments.
Takes the standard Convex paginationOpts and returns the standard paginated
envelope (page, isDone, continueCursor) with each row narrowed.
The number of visible (open or resolved) comments on resourceRef. Soft-deleted
comments are never counted.
Coded ConvexErrors thrown by the component (error.data.code):
| Code | Thrown by | Meaning |
|---|---|---|
PARENT_NOT_FOUND |
post |
The parentId names no comment. |
PARENT_MISMATCH |
post |
The parent belongs to a different resource. |
PARENT_DELETED |
post |
The parent comment is soft-deleted. |
NOT_FOUND |
edit, remove, resolve |
No comment has this commentId. |
NOT_AUTHOR |
edit, remove, resolve |
The caller is not the comment's author. |
DELETED |
edit, resolve |
The comment is soft-deleted and cannot be mutated. |
The component registers one cron (crons.ts):
| Job | Cadence | Action |
|---|---|---|
comments:prune |
every 24h (PRUNE_INTERVAL) |
runs prune with batch = PRUNE_BATCH (200), self-rescheduling until the soft-deleted tail is clean |
Cadence is a static module constant (Convex cron definitions are static per
deployment). A host wanting a different cadence drives prune from its own
scheduler with an explicit before cutoff. The cron is per-mount, so each
app.use(component, { name }) instance prunes its own sandbox independently.