Inspiration
I recently started using Threads and wanted to grow my account. I wanted a mobile app to answer what works, when is the best time for me to post, and schedule posts. Most solutions are desktop only, but I don't want to use a computer to do these things.
What it does
Insights. Views, likes, replies, and follower growth as a trend, with a baseline and personal records so a creator knows what "good" means for their own account. Best hour. The API keeps an append-only history of metrics per post and computes the hour of the week that performs best in the user's time zone. Onboarding sets that hour on one dial. Schedule. A calendar queue of posts with a photo or video, multi-part chains published as replies to the part before, and a push notification when each one goes live. Pro. The free tier gives 3 posts per week and 7 days of analytics. Pro through RevenueCat unlocks unlimited posts and the full range. The paywall and the entitlement checks live on both the client and the API.
How we built it
The repo is a Bun monorepo with three parts.
- Mobile. Expo with expo-router, Unistyles, React Query, Clerk for sign-in, and the RevenueCat SDK. The app never talks to Threads. It only calls the API.
- API. A Cloudflare Worker on Hono and Effect. Every route is an Effect program, and one function maps typed errors to HTTP responses. Threads tokens live in a Durable Object per user per channel. The Durable Object's alarm refreshes the token on day 45 of the 60-day window, so no token touches Postgres. A one-minute cron sweeps due posts into a Cloudflare Queue. A six-hour cron enqueues analytics syncs. Media goes to R2 and is deleted when no post needs it.
- Shared core. Effect Schemas for every request and response, shared by the API and the app. The app validates every response at runtime. Status enums in Postgres are generated from the same unions, so an invalid value cannot reach the database.
Challenges we ran into
Threads publishes in two phases. You create a container, then publish it. If the worker dies between the two, a retry would post twice. I made publish idempotent and added a reclaim sweep for rows stuck in the publishing state. Chains. Each part of a chain has to be a reply to the part before, which needs an extra permission scope. I found that out from an error body, so now the worker logs the Threads error body on every failed request. Time zones. "Best hour" and "posts this week" both depend on the user's zone, not the server's. Week boundaries are computed from zoned parts, and the free-tier quota counts pending, publishing, and published posts in that zoned week. Failure handling. Anything the queue consumer cannot finish goes to a dead-letter queue. The same worker consumes it, logs once, marks the post failed, and pushes the owner. A published post is never rewritten. Metrics that lie. My own chained replies counted as engagement on the parent post. Media lifetime. R2 lifecycle rules cannot tell a pending post two weeks out from an orphan. A daily cron deletes only objects older than 30 days that no live post references, and nulls the URL on the rows that pointed at them.
Accomplishments that we're proud of
What we learned
Durable Objects are a good fit for a per-user secret with a refresh timer. One object, one alarm, no cron scanning a token table.
What's next for Primsy
The backend is channel-agnostic so the plan is to add more channels as we grow.
Built With
- clerk
- cloudflare
- effect
- expo.io
- planetscale
- revenuecat
Log in or sign up for Devpost to join the conversation.