Skip to main content

Comments Schema Deep Dive

Overview

The comments system enables users to leave feedback and reviews on items. Comments are linked to client_profiles (not directly to users), include a rating field, and support soft deletion via deletedAt. The moderation subsystem (reports and moderation_history) provides content reporting and administrative action tracking.

Source file: template/lib/db/schema.ts Relations file: template/lib/db/migrations/relations.ts


Table: comments

Stores user-submitted comments/reviews on items.

Columns

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
contentcontenttextNo-Comment body
userIduserIdtextNo-FK -> client_profiles.id (CASCADE)
itemIditemIdtextNo-Item slug
ratingratingintegerNo0Numeric rating
createdAtcreated_attimestamp (tz)Nonow()-
updatedAtupdated_attimestamp (tz)Nonow()-
editedAtedited_attimestamp (tz)Yes-When last edited
deletedAtdeleted_attimestamp (tz)Yes-Soft delete

Foreign Keys

ColumnReferencesOn Delete
userIdclient_profiles.idCASCADE

TypeScript Types

export type Comment = typeof comments.$inferSelect;
export type NewComment = typeof comments.$inferInsert;
Note on Foreign Key

Comments reference client_profiles.id, not users.id. This means the comment author must have a client profile created before they can post comments.


Moderation Tables

Table: reports

Content reporting system for both items and comments.

Columns

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
contentTypecontent_typetext (enum)No-item, comment
contentIdcontent_idtextNo-ID of reported content
reasonreasontext (enum)No-See enum below
detailsdetailstextYes-User-provided details
statusstatustext (enum)No'pending'See enum below
resolutionresolutiontext (enum)Yes-See enum below
reportedByreported_bytextNo-FK -> client_profiles.id (CASCADE)
reviewedByreviewed_bytextYes-FK -> users.id (SET NULL)
reviewNotereview_notetextYes-Admin review note
createdAtcreated_attimestampNonow()-
updatedAtupdated_attimestampNonow()-
reviewedAtreviewed_attimestampYes--
resolvedAtresolved_attimestampYes--

Report Enums

export const ReportContentType = {
ITEM: 'item',
COMMENT: 'comment'
} as const;

export const ReportReason = {
SPAM: 'spam',
HARASSMENT: 'harassment',
INAPPROPRIATE: 'inappropriate',
OTHER: 'other'
} as const;

export const ReportStatus = {
PENDING: 'pending',
REVIEWED: 'reviewed',
RESOLVED: 'resolved',
DISMISSED: 'dismissed'
} as const;

export const ReportResolution = {
CONTENT_REMOVED: 'content_removed',
USER_WARNED: 'user_warned',
USER_SUSPENDED: 'user_suspended',
USER_BANNED: 'user_banned',
NO_ACTION: 'no_action'
} as const;

Indexes

NameColumnsType
reports_content_type_idxcontentTypeB-tree
reports_content_id_idxcontentIdB-tree
reports_status_idxstatusB-tree
reports_reported_by_idxreportedByB-tree
reports_created_at_idxcreatedAtB-tree
reports_content_type_content_id_idx(contentType, contentId)Composite B-tree

TypeScript Types

export type Report = typeof reports.$inferSelect;
export type NewReport = typeof reports.$inferInsert;

Table: moderation_history

Tracks all moderation actions taken against users.

Columns

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
userIduser_idtextNo-FK -> client_profiles.id (CASCADE)
actionactiontext (enum)No-See enum below
reasonreasontextYes--
reportIdreport_idtextYes-FK -> reports.id (SET NULL)
performedByperformed_bytextYes-FK -> users.id (SET NULL)
contentTypecontent_typetext (enum)Yes-item, comment
contentIdcontent_idtextYes--
detailsdetailsjsonbYes-Additional context
createdAtcreated_attimestampNonow()-

Moderation Action Enum

export const ModerationAction = {
WARN: 'warn',
SUSPEND: 'suspend',
BAN: 'ban',
UNSUSPEND: 'unsuspend',
UNBAN: 'unban',
CONTENT_REMOVED: 'content_removed'
} as const;

Indexes

NameColumnsType
moderation_history_user_id_idxuserIdB-tree
moderation_history_action_idxactionB-tree
moderation_history_report_id_idxreportIdB-tree
moderation_history_performed_by_idxperformedByB-tree
moderation_history_created_at_idxcreatedAtB-tree

TypeScript Types

export type ModerationHistoryRecord = typeof moderationHistory.$inferSelect;
export type NewModerationHistoryRecord = typeof moderationHistory.$inferInsert;

Relations Diagram


Report Lifecycle


Query Examples

Get comments for an item

import { db } from '@/lib/db/drizzle';
import { comments } from '@/lib/db/schema';
import { eq, isNull, desc } from 'drizzle-orm';

const itemComments = await db
.select()
.from(comments)
.where(
and(
eq(comments.itemId, 'my-item-slug'),
isNull(comments.deletedAt)
)
)
.orderBy(desc(comments.createdAt));

Create a comment

await db.insert(comments).values({
content: 'Great tool, highly recommended!',
userId: clientProfileId, // Note: client_profiles.id, NOT users.id
itemId: 'my-item-slug',
rating: 5,
});

Soft delete a comment

await db
.update(comments)
.set({ deletedAt: new Date() })
.where(eq(comments.id, commentId));

Submit a report

import { reports } from '@/lib/db/schema';

await db.insert(reports).values({
contentType: 'comment',
contentId: commentId,
reason: 'spam',
details: 'This comment appears to be promotional spam.',
reportedBy: clientProfileId,
});

Get pending reports

const pendingReports = await db
.select()
.from(reports)
.where(eq(reports.status, 'pending'))
.orderBy(desc(reports.createdAt));

Record a moderation action

import { moderationHistory } from '@/lib/db/schema';

await db.insert(moderationHistory).values({
userId: targetClientProfileId,
action: 'warn',
reason: 'Posting spam content',
reportId: reportId,
performedBy: adminUserId,
contentType: 'comment',
contentId: commentId,
});