Build on the supported package contract
Use @absolutejs/sync-pack-comments through its supported public entry points.
@absolutejs/sync-pack-commentsv0.4.5betaData & SyncThreaded comments pack for @absolutejs/sync — per-resource ACL-scoped, author/moderator gates, optional CRDT bodies
bun add @absolutejs/sync-pack-commentsThreaded comments as a sync pack for @absolutejs/sync. Per-resource ACL injection, author/moderator gates, optional CRDT bodies. Plugs into a SyncEngine with one engine.registerPack(...) call.
Surface — Name — What it does
Collection — comments — Subscribe with params: { resourceId } — returns the comment tree
Mutation — comments:create — Args: { resourceId, body, parentCommentId? } — stamps authorId
Mutation — comments:edit — Args: { commentId, body } — author only, stamps editedAt
Mutation — comments:delete — Args: { commentId } — author or moderator
When bodyCrdt is set, the engine auto-registers a comments:merge mutation through registerCrdt — clients call that to merge CRDT body updates concurrently with regular edits.
The collection returns a flat list of rows for the resource; the client builds the tree by walking parentCommentId. Depth is stored on the row so a client can short-circuit-render without traversing the full chain.
Default: per-instance in-memory store. To use a persistent backend (Drizzle, Postgres, …), pass a custom store:
The getById method is required (used by edit + delete to verify ownership and by create to walk the parent chain for depth math).
To run two comments packs on the same engine (e.g. one per product surface), pass a prefix to each. It scopes the owned table, the collection, and the mutation names:
This pack composes with the rest of your sync graph via subscriptions. A presence pack that wants to show "Alice is replying to this thread" should subscribe to comments and presence separately — it should NOT call comments: from inside its own handler. See the design doc rules in syncPacks.design.md.
Set joinUsers to additionally register a comments-with-author join collection that pairs each comment with the host's user row. The pack does NOT own the users table; it adds it to readsTables so the engine knows the dependency and your devtools see the full graph.
The engine inner-joins on comment.authorId === user.id; comments whose author is missing from the users table are excluded from the join (but still appear in the base comments collection). canReadResource gates the join the same way it gates the base.
In-thread full-text search via registerSearch on the comments
table.
Reactions — a reactionsTable config that adds an emoji-reaction
side table with create/remove/list mutations.
Outcomes
Use @absolutejs/sync-pack-comments through its supported public entry points.
Hardening checklist
Follow in order
# @absolutejs/sync-pack-comments
bun add @absolutejs/sync-pack-commentsWorking example for Usage.
import { createSyncEngine } from '@absolutejs/sync/engine';
import { createCommentsPack } from '@absolutejs/sync-pack-comments';
const engine = createSyncEngine();
engine.registerPack(
createCommentsPack({
// REQUIRED: gate read access on a resource. The host knows which
// resources a given ctx can see; the pack does not duplicate that.
canReadResource: (resourceId, ctx) =>
hostAcl.canRead(resourceId, ctx.session.userId),
// REQUIRED in practice: how the pack reads the current actor id
// from your app's ctx. Default is `(ctx) => ctx.userId`.
getActorId: (ctx) => ctx.session.userId,
// OPTIONAL: moderator predicate. Used by comments:delete (author OR
// moderator can delete). Default `() => false`.
canModerate: (ctx) => ctx.session.isModerator,
// OPTIONAL: max thread depth (top-level = 0). Default 8.
maxDepth: 8,
// OPTIONAL: wire the comment body as a CRDT field via registerCrdt
// so concurrent edits merge instead of clobbering. Pass anything
// implementing `CrdtMergeable<T>` — e.g. yjsText from
// @absolutejs/sync-yjs. The pack does NOT import Yjs.
// bodyCrdt: yjsText,
})
);Working example for Row shape.
type CommentRow = {
id: string;
resourceId: string;
parentCommentId: string | null; // null on top-level; parent id on replies
authorId: string;
body: string;
depth: number; // 0 for top-level, parent.depth + 1 for replies
createdAt: number;
editedAt: number | null;
};Default: per-instance in-memory store. To use a persistent backend (Drizzle, Postgres, …), pass a custom store:
import {
createCommentsPack,
type CommentsStore,
} from '@absolutejs/sync-pack-comments';
const store: CommentsStore = {
getById: (id) => /* SELECT * FROM comments WHERE id = $1 */,
reader: { all: () => /* SELECT * FROM comments */ },
writer: {
insert: (row) => /* INSERT */,
update: (row) => /* UPDATE */,
delete: (row) => /* DELETE WHERE id = $1 */,
},
};
engine.registerPack(createCommentsPack({ store, canReadResource, getActorId }));Supported entry points declared by this package manifest.
Package entry point declared in package.json.
Scripts declared by this package manifest.
Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.
A comment row stored in the pack's owned table.
type CommentRow = {
id: string;
resourceId: string;
/** Null on top-level comments, the parent's id on replies. */
parentCommentId: string | null;
authorId: string;
body: string;
/** Depth in the thread (0 for top-level, parent.depth + 1 for replies). */
depth: number;
createdAt: number;
/** Null until the comment is edited; otherwise the timestamp of the most
* recent edit. */
editedAt: number | null;
};@absolutejs/sync-pack-comments