The first Instagram DM showed up in the Engagement Hub on a Tuesday morning. I didn't see it come in — I saw it sitting there, waiting, like it had been there a while and I'd only just noticed. A test account I'd used to verify the auto-responder, a message I'd sent myself two days earlier to confirm the webhook was connected.
It worked. And I didn't know whether to feel relieved or suspicious.
I went with suspicious, which turned out to be the right call.
This is the second entry in an ongoing series. Read Log #001 here.
The Week Instagram DMs Started Arriving
The Instagram DM was in the hub. But only one — out of several I'd sent across different test sessions. I went into the Railway logs and found nothing obviously wrong. Checked the webhook handler, checked the database. No errors surfacing anywhere.
Then I looked closer at the exact SQL Prisma was generating.
The upsert I'd written to save incoming messages was doing a WHERE lookup on a platform field — which in the schema is a PostgreSQL enum. Prisma was binding that value as plain text. PostgreSQL doesn't compare text to an enum natively; it needs an explicit cast. The error code was 42883 — operator does not exist: text = "Platform". Except it wasn't surfacing as an error in the logs. Prisma was catching it internally, the record was never saved, and nothing was returned. It was failing silently and completely.
Fix: replace the upsert with a findFirst — no platform in the WHERE clause, just the external message ID — followed by a separate create or update depending on what came back. A few lines of code that should have been how I wrote it the first time.
Ten Instagram DMs appeared in the hub within the next ten minutes. The records hadn't been lost. They'd just never been written. Every social media management tool that relies on PostgreSQL enums and Prisma ORM has this exposure — the upsert pattern silently drops records without raising an exception.
When One Clean Reply Matters More Than Two
There is a thing Meta does that took me an embarrassingly long time to fully understand. When someone sends an Instagram DM, Meta fires two webhook events. The first one arrives almost immediately — a message_edit event with num_edit: 0, which is not actually an edit but Meta's way of signalling that a new message thread has started. No message text. No content. Just a notification that something is coming.
The second event arrives a fraction of a second later — the actual message, with text, sender ID, message ID, everything you need.
The Instagram auto-responder was listening to both events and treating each one as a separate message. Two webhook arrivals. Two rule matches. Two replies. If a user sent "PRICE" expecting one response about our pricing, they were getting the same response twice in two seconds — which looks like a technical glitch, which is the last thing an Instagram DM auto-reply should look like.
I tried a dedup check first — a flag on the saved record that would prevent a second reply if one had already been sent. It didn't work. The two events arrived so close together that both handlers would check the flag before either had a chance to set it. Race condition. Classic.
The fix was simpler once I stopped trying to coordinate between the two handlers: make only one of them responsible for replies. The message_edit handler saves the conversation to the inbox. The full message handler, which arrives with the actual text, is the only one that checks rules and sends a reply. Two events, two jobs, no overlap. One reply per DM, every time.
Building What the Permission Wasn't Ready to Deliver
I was getting close to recording the Meta App Review screencast for instagram_manage_comments — the permission that lets the app read and respond to comments posted on Instagram media. The reviewer's feedback from the previous submission was specific: show a complete moderation loop. Post a comment, moderate it, delete it, confirm in the native app.
I posted a comment on the test account. Opened Railway logs. Watched.
Nothing came through.
No webhook event. No record. The comment existed on Instagram — I could see it in the native app — but it had produced no signal in the application whatsoever. I ran the test again. Same result. The webhook endpoint was live, the handler was registered, everything was connected. And the logs were empty.
The reason: Meta does not deliver Instagram comment webhook events to an application that has not had instagram_manage_comments approved for production use. You can build the handler. You can configure the subscription. But until the permission is approved, the events simply don't arrive. There's no error, no notification, no indication that the subscription exists — just silence. For any social media agency building on the Instagram Graph API, this is a wall you hit exactly once and never forget.
I could either wait for the approval to come back, or I could build something that didn't require it.
The Feature That Wasn't in Any Roadmap
I built the Post Comment feature in one sitting. A purple button in the Engagement Hub header labelled "Post Comment" — clicking it opens a modal that loads the twelve most recent posts from the connected Instagram account, displayed as thumbnail cards. You pick a post, write your comment, click Post Comment, and the comment goes live on Instagram through the Graph API, then lands immediately in the Engagement Hub as a new message in the feed.
From there, the Hide and Delete buttons are already on every comment in the hub. Delete calls the Graph API's delete endpoint — confirmed working, the comment disappears from Instagram native immediately. Hide calls the Graph API's hide parameter on the comment endpoint.
That last one had a typo in my original implementation. I had written hidden=true. The Instagram Graph API expects hide=true. One character. The API accepted the request with a 200, changed nothing, and returned no error. Found it by checking the Instagram Graph API reference directly. Fixed. Pushed.
The complete Instagram comment moderation loop — post, hide, delete — now lives entirely inside eWork Social without requiring the inbound webhook to exist yet. For social media managers in Nigeria and across Africa handling high-volume brand accounts, this means moderating Instagram comments without switching tools or opening Meta Business Suite. When instagram_manage_comments is approved and inbound webhook events start arriving, comments from external users will land in the hub automatically. Until then, the outbound moderation flow is fully operational.
Conversations That Belong to the Human, Not the App
There was another bug I found while testing the reply flow — one that had been there since the Engagement Hub shipped. When a team member sent a manual reply to a DM, the conversation was automatically being marked as resolved. Not by the person replying. Not by a button they'd clicked. Just automatically, by code that had made a decision no one asked it to make.
A resolved conversation is closed. You can't reply to it again without manually reopening it — except there was no reopen button. Once resolved, the conversation was effectively locked. For a social media management tool where a single DM thread might need multiple follow-ups before it's genuinely done, that's not a moderation feature. That's a trap.
Fix: remove the auto-resolve from the reply handler entirely. Add a toggleable resolve that the user controls. Add a Reopen button on the resolved banner so closed conversations can be brought back at any time. Replies don't resolve. Only people do.
Next
The screencast is almost ready to record. The full flow — connect, authenticate, post comment, hide, delete — now works cleanly enough to put on camera without explaining my way around anything. The goal this week is to submit that recording to Meta and wait on the second permission with the same patience that the first one eventually rewarded.
The hub is awake. Instagram DMs are coming in. The Instagram comment moderation loop is in place. If you're a freelance social media manager or agency owner who wants to see how eWork Social handles multi-platform scheduling and unified inbox management before committing to anything, the two-minute free trial is still open — and I read every message that comes through during onboarding.
— Bernard Oshapi, Founder, eWork Social