Esposter

Posts and Comments

One posts table carries both: a post is a root row (required title, parentId = null), a comment is a child row (parentId set, depth = parent + 1, no title). Deleting a post cascades to its comments.

How it works

Modelposts(id, userId, title, description, parentId, depth, noComments, noLikes, ranking) with DB-level length checks (title ≤ 300, description ≤ 1000); selectPostSchema vs selectCommentSchema differ only in which text field is required. Rows relate to their author via PostRelations and carry the viewer's own like as viewerLike (see likes).

Creating/post/create hosts the post form (Post/UpsertForm.vue); descriptions are Tiptap rich text (DescriptionRichTextEditor). Comments are created inline on the post page (Comment/CreateRichTextEditor). Both mutations run through the profanity-filter procedure, which censors the configured text fields in middleware, and compute the initial ranking from zero likes.

Comment bookkeepingcreateComment/deleteComment run in a transaction that also increments/decrements the parent's noComments, so counts never drift from the rows.

Ownership — update/delete are guarded by ownedBy(posts, id, userId) plus a parentId IS (NOT) NULL check, so post procedures can't touch comments and vice versa; there is no moderator override (moderation is an esbabbler concept, not a posts one).

Reading — the post page loads the post by route param, then pages its comments through the same readPosts procedure with parentId; an empty banner shows for zero comments, and the comment editor only renders for a signed-in session.

Procedures

ProcedureAuthInputPurpose
post.createPostauthed + profanitytitle, descriptioncreate a root post
post.updatePostauthed + profanity, ownid + fieldsedit own post
post.deletePostauthed, owniddelete own post (cascades)
post.createCommentauthed + profanityparentId, descriptioncomment (+noComments)
post.updateCommentauthed + profanity, ownid + descriptionedit own comment
post.deleteCommentauthed, owniddelete own comment (−count)

Key files

Paths relative to packages/app, except those starting with packages/, which are relative to the repo root.

FileRole
packages/db-schema/src/schema/posts.tstable + length checks + select schemas
server/trpc/routers/post.tsall CRUD procedures
server/trpc/procedure/getProfanityFilterProcedure.tstext censoring middleware
app/pages/post/create.vue, app/pages/post/[id].vuecreate + detail pages
app/components/Post/card, forms, comment components
app/store/post/comment/index.tscomment list store

Notes

  • depth is tracked on every comment but the UI renders a single flat level — nested reply threads are deferred.
  • Comments have no title by design; the shared table means any future post feature (likes, achievements, profanity filtering) applies to comments for free.