48 tests. That is the ticket to ship: comment db (9), generator (5), routes (12), plus the existing draft suite carrying the rest.
The problem this solves is that comments are not posts with a destination. They are responses, and responses expire. A post draft can sit in your queue for 48 hours and still be relevant. A comment draft from Tuesday about a thread that peaked Monday is noise. The moment I started treating them as the same object, everything fought back.
No scheduling
The post pipeline has a whole slot system: quiet hours gating, cadence controls, a scheduled_at column. Comments get none of that. The comment_drafts table has no scheduled_at because the concept does not apply. You generate, you review, you post, or you discard. That is the entire lifecycle. There is no "schedule for 3pm" because by 3pm the thread you are replying to has moved on.
This sounds like a simplification. It is actually a hard constraint that propagated through every other decision.
Carry the target everywhere
Each comment draft stores the post it replies to: author, text, URL, and engagement numbers. Engagement lives as a JSON blob on the draft row because what "engagement" means differs by platform (likes vs reactions vs reposts vs shares), and I was not about to normalize a schema I do not fully control across three platforms at once.
The CommentCard in the frontend shows that target context above the editable reply field. This sounds obvious in retrospect. It was not obvious when I started. Without that context, reviewing a queue of comment drafts is guessing in the dark. You are editing replies without seeing what you are replying to.
Dedup is the annoying part
You do not want to comment on the same post twice. "Already commented" needs to cover two states: pending drafts in the queue, and actual posted comments in the ledger. The guard checks both. The queue cap sits at 5 pending per platform, which keeps the review burden manageable and stops the generator from running far ahead of human judgment.
Discovery: net new is the wrong default
Per platform discovery is read only candidate scraping composed from existing helpers. LinkedIn gets discover_candidates(), X and Threads pull from their existing comment infrastructure. Nothing net new on the discovery side.
That was the right call. Build the new plumbing (generate, store, review, post) around existing discovery rather than rewriting everything at once. The generate comments CLI command and the five routes (list, update, post, discard, generate) are the new surface. The discovery machinery underneath is unchanged.
What I would do differently
The engagement JSON blob. It is pragmatic today but means you cannot query "show me drafts targeting high engagement posts" without parsing JSON in the application layer. Separate normalized columns or a join table would be cleaner. I took the shortcut because the cross platform mapping is messy and I wanted to ship. Next time I would normalize it earlier, even imperfectly, so the data stays queryable.
The dashboard now has a Posts/Comments toggle per platform. Simple interface. I keep wondering if I overengineered the backend for what the UI actually does. But 48 tests passing and a working generate comments CLI say otherwise.
Top comments (0)