Schema for polymorphic comments with unlimited threading depth.
Supports nested comment threads (Reddit-style) with self-referencing parent/child
relationships. Can be attached to any resource type via resource_type + resource_uuid.
Comment Status
published- Comment is visiblehidden- Comment is hidden by moderatordeleted- Comment deleted (soft delete)pending- Awaiting moderation approval
Fields
resource_type- Type of resource (e.g., "post", "entity", "ticket")resource_uuid- UUID of the resourceuser_uuid- Reference to the commenterparent_uuid- Reference to parent comment (nil for top-level)content- Comment textstatus- published/hidden/deleted/pendingdepth- Nesting level (0=top, 1=reply, 2=reply-to-reply, etc.)like_count- Denormalized like counterdislike_count- Denormalized dislike countermetadata- Arbitrary JSONB data (giphy reactions, custom flags, rich embeds, etc.)
Media
The has_many :media association links to PhoenixKitComments.CommentMedia
rows ordered by position. Content-or-media validation (content is
optional iff Giphy or media is present) checks the loaded association
on updates; the orchestrator passes has_media: true on insert when
it's about to attach files in the same transaction.
Summary
Functions
Changeset for creating or updating a comment.
Check if comment is deleted.
Check if comment is published.
Freezes who this comment is from. Server-set only — never from attrs.
Check if comment is a reply (has parent).
Check if comment is top-level (no parent).
Types
@type t() :: %PhoenixKitComments.Comment{ __meta__: term(), attributed_label: term(), attributed_project_uuid: term(), attribution_mode: term(), author_display_name: term(), children: [t()] | Ecto.Association.NotLoaded.t(), content: String.t(), depth: integer(), dislike_count: integer(), inserted_at: DateTime.t() | nil, like_count: integer(), media: [PhoenixKitComments.CommentMedia.t()] | Ecto.Association.NotLoaded.t(), metadata: map(), parent: t() | Ecto.Association.NotLoaded.t() | nil, parent_uuid: UUIDv7.t() | nil, resource_type: String.t(), resource_uuid: Ecto.UUID.t(), status: String.t(), updated_at: DateTime.t() | nil, user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t() | nil, user_uuid: UUIDv7.t() | nil, uuid: UUIDv7.t() | nil }
Functions
Changeset for creating or updating a comment.
Required Fields
resource_type- Type of resource being commented onresource_uuid- UUID of the resourceuser_uuid- Reference to commenter- Either
content, a Giphy attachment inmetadata["giphy"], or media
Options
:has_media— boolean. When set, overrides the inferred media presence (used byPhoenixKitComments.create_comment/4because the new comment has nouuidyet and themediaassociation is not loaded). Not part ofcast— callers cannot drive it via attrs.
Check if comment is deleted.
Check if comment is published.
@spec put_attribution(Ecto.Changeset.t(), map() | nil) :: Ecto.Changeset.t()
Freezes who this comment is from. Server-set only — never from attrs.
:personal pins the display name the reader will see, so a later rename,
a filled-in profile or a departure does not re-sign every comment the
person ever wrote.
:project additionally records that they were speaking for the project,
and pins the project's name as it read at the time. user_uuid is left
exactly as it was: the public sees the project, and internally the
author is still on the row for moderation and audit. A shared voice with
nobody accountable behind it is the failure mode of this feature.
Check if comment is a reply (has parent).
Check if comment is top-level (no parent).