Comments Plugin Pattern
Attach discussion threads to any parent resource using a canonical comments schema.
This guide demonstrates the canonical Brick-TS comments plugin pattern, attaching threaded discussions to any parent resource with target foreign-key validation.
Canonical Schema
The comments pattern uses a single, unified database schema across all services:
import { sqliteTable, text } from "@brickkit/core";
export const commentsTable = sqliteTable("comments", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
targetId: text("target_id").notNull(),
body: text("body").notNull(),
});Schema
These columns match the Drizzle tables in the example. Defaults marked as application defaults run in Drizzle, rather than in SQL.
Table Name: comments
| Field | Type | Description |
|---|---|---|
id | text | Row identifier. PK Default: crypto.randomUUID() (application) |
target_id | text | Parent record identifier, validated by the application. |
body | text | Text content. |
Table Name: pages
| Field | Type | Description |
|---|---|---|
id | text | Row identifier. PK Default: crypto.randomUUID() (application) |
title | text | Display title. |
Plugin Implementation
Implement the comments factory using defineResourcePlugin and @brickkit/crud:
import { ActionExecutionError, defineResourcePlugin, eq } from "@brickkit/core";
import { crud } from "@brickkit/crud";
import { commentsTable } from "./schema";
export { commentsTable } from "./schema";
/** Resource plugin that attaches comments to any host resource. */
export function comments(options: { table: typeof commentsTable }) {
return defineResourcePlugin({
name: "example/comments",
setup({ resource, service }) {
const comment = service
.resource({
name: `${resource.name}Comment`,
table: options.table,
id: options.table.id,
})
.use(
crud({
// Anyone may read and post comments; editing and deleting
// existing comments requires an authenticated user.
access: {
authorize: ({ operation, ctx }) => {
if (operation === "update" || operation === "delete") {
return !!ctx.user;
}
return true;
},
},
fields: {
id: { read: true, filter: ["eq"] },
targetId: { read: true, create: true, filter: ["eq"] },
body: {
read: true,
create: true,
update: true,
filter: ["contains"],
},
},
hooks: {
beforeCreate: async ({ data, ctx }) => {
data.body = String(data.body).trim();
if (!data.body) {
throw new ActionExecutionError(
"BAD_REQUEST",
"Comment body cannot be blank",
400,
);
}
// Verify parent resource exists in the database
const parents = await ctx.db
.select({ id: resource.id })
.from(resource.table)
.where(eq(resource.id, data.targetId))
.limit(1);
if (!parents.length) {
throw new ActionExecutionError(
"NOT_FOUND",
"Comment target not found",
404,
);
}
},
beforeUpdate: ({ data }) => {
if (data.body !== undefined) {
data.body = String(data.body).trim();
if (!data.body) {
throw new ActionExecutionError(
"BAD_REQUEST",
"Comment body cannot be blank",
400,
);
}
}
},
},
}),
);
return { comments: comment };
},
});
}Attaching to Resources
Attach the comments plugin to any host resource with .use():
import {
defineDatabase,
defineService,
sqliteTable,
text,
} from "@brickkit/core";
import { crud } from "@brickkit/crud";
import { comments, commentsTable } from "../extensions/comments";
export const pagesTable = sqliteTable("pages", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
title: text("title").notNull(),
});
const database = defineDatabase({ tables: { pagesTable, commentsTable } });
export const pagesService = defineService("pages", { database });
// Attach comments plugin to the page resource:
export const page = pagesService
.resource({ name: "page", table: pagesTable, id: pagesTable.id })
.use(
crud({
access: "public",
fields: { id: { read: true }, title: { read: true, create: true } },
}),
)
.use(comments({ table: commentsTable }));This automatically mounts the standard CRUD REST endpoints for the comment resource (under the service prefix, e.g. /api):
POST /api/pageComment(create)POST /api/pageComment/query(list)POST /api/pageComment/countandPOST /api/pageComment/existsGET /api/pageComment/:id(public),PATCH /api/pageComment/:idandDELETE /api/pageComment/:id(authenticated)
The recipe has no per-comment ownership, so any authenticated user can edit or delete any comment. If comments should be editable only by their author, add an owner column to the table and return an ownership scope from the access policy, alongside authorize.