Jfreaks Software Solutions, Aug 2024 to present
TweetStorm.ai
Jfreaks Software Solutions, August 2024 to present, in a shared team codebase (604 of the app's 1,002 commits are mine). I built the thread scheduler, its publish worker and the bookmark manager, and led the Prisma to Knex query migration.
A scheduled thread can be claimed once, resumed mid-thread after a crash, and never posts a tweet twice.
Tools for X that X does not ship: AI writing, a thread scheduler, a bookmark manager, and filtered bulk actions run inside x.com by browser extensions.


TypeScript, Next.js 16, MySQL, Knex, Prisma, BullMQ, Redis, Plasmo, Stripe, X API v2
Problem
The product started as an AI tweet generator with a Stripe subscription. Users wanted to schedule threads through the official X API, keep their bookmarks somewhere searchable, and clean up years of posts in bulk. Each of those needs a different trust boundary: X's OAuth for posting, the user's own browser session for bookmarks and bulk actions, and a server that can crash mid-thread without double posting.
Hardest problem
Making a scheduled thread post exactly once. A thread is several tweets posted in sequence, and the worker can crash or be retried halfway through. Publishing is claimed with a conditional UPDATE from scheduled to publishing, and the claim can be retaken only once it is older than 45 seconds, a window chosen to sit between BullMQ's job lock and the 60 second retry backoff. Each tweet and media upload persists its X ID the moment it succeeds, so a retry resumes at the first unpublished tweet instead of reposting the thread.
What I built
- The thread scheduler end to end: composer, per-account posting slots and timezones, media uploads via presigned URLs, plan quotas, and BullMQ delayed jobs
- A standalone BullMQ worker that publishes threads to X API v2, with its own CI, staging and production stacks
- The bookmark manager: folders to depth three, tags, smart folders, archive and read states, plan-gated storage, and a backup sync through the X API for users without the extension
- Of the three browser extensions, I wrote the Bookmark Manager (Chrome MV3 and Firefox MV2, in Plasmo) and most of the Mass Tweet Deletion extension's content scripts: engagement, reply, like and verified-account filters, pause on tab hide, and scroll recovery on X's infinite feeds
- Stripe lifecycle handling across two plan families (Core and Bookmarks) that a user can hold independently, receipts and invoice tracking
- Led the Prisma to Knex query migration in January 2025, keeping Prisma for the schema and migrations
Engineering decisions
- Errors are classified: tweet too long or media too large are terminal and stop retries; network and X 5xx errors rethrow for BullMQ's three attempts. Usage is refunded and the user emailed only when the thread finally fails
- X refresh-token rotation is serialised per account with a Redis SET NX PX lock and a re-read after acquire, so two jobs for one account cannot both rotate the token
- The Bookmark Manager syncs from the user's own logged-in session in batches of twenty, and adapts at runtime when X changes its web client
- Bookmark dedupe is scoped by user, X handle and tweet ID, so several X accounts can share one TweetStorm account without collisions
- Public thread IDs are a bijection over 2^40 encoded in Crockford base32, so URLs do not leak sequential database IDs
- The checkout.session.completed and customer.subscription.updated webhooks race. Both handlers catch the duplicate-key error on the subscriptions insert and treat it as already created
In production
- Deploys are sha-pinned images in a self-hosted registry, with two versions kept for rollback and separate staging and production worker stacks
Request flow
- 1.User composes a thread. Media goes straight from the browser to object storage through a presigned PUT
- 2.POST /api/scheduler/threads validates media composition (four photos, or one GIF, or one video), thread structure, the account's schedulability and the monthly quota, then inserts the thread and tweets in one transaction
- 3.A BullMQ publish job is enqueued with a delay to the scheduled time, three attempts and 60 second exponential backoff. The job ID is stored on the thread
- 4.At the scheduled time the worker claims the thread with a conditional UPDATE, refreshes the X token under a Redis lock, and pre-checks weighted tweet length
- 5.For each tweet: stream media from storage, upload to X, post with in_reply_to_tweet_id, persist the X IDs. Already-published tweets are skipped on retry
- 6.On success the thread is marked published, media is deleted and an audit event is written. On terminal failure usage is refunded once and paying users are emailed
Architecture
Three trust boundaries. In the browser, extensions work inside x.com with the user's own session and sync into the app. On the servers, a Next.js 16 app owns the UI, the API and the schema, and enqueues delayed jobs. A separate BullMQ worker is the only thing that posts, through X's official API. MySQL is the source of truth; Redis carries the delayed jobs and per-account locks.
Connections
- 1.Web app ↔ Next.js app: REST, NextAuth session
- 2.Extensions → Next.js app: Bookmark batches of twenty
- 3.Extensions ↔ x.com: Runs inside the user's session
- 4.Stripe, OpenAI ↔ Next.js app: Webhooks in, generation out
- 5.Next.js app ↔ MySQL: Thread and tweets in one transaction
- 6.Next.js app → Redis: Delayed publish job (queued or scheduled)
- 7.Redis → Publish worker: Fires at the scheduled time (queued or scheduled)
- 8.Publish worker ↔ MySQL: Claim, resume, audit
- 9.Publish worker ↔ X API v2: Upload media, post each tweet
Components
- Web appReact 19, NextAuth
- The Next.js web app: AI writing, the thread composer and scheduler, the bookmark manager and billing. Sign-in with Google, X or credentials through NextAuth.
- ExtensionsPlasmo, Chrome, Firefox
- Three Plasmo browser extensions for Chrome and Firefox. The Bookmark Manager syncs bookmarks from the user's own session into the app using the NextAuth session cookie. Mass Tweet Deletion runs filtered bulk actions inside x.com, paced to stay within X's limits. The AI Generator adds a prompt bar under X's composer and authenticates with a per-user API key.
- x.comThe user's own session
- The user's logged-in x.com tab. Extension work happens here, with the user's own session, and never passes through TweetStorm's servers.
- Next.js appAPI routes, Knex
- Next.js 16 App Router. Every API route goes through one handleRequest wrapper for auth, validation and error shape: AI generation with tiered credit debit, scheduler threads and slots, bookmarks CRUD and sync, extension routes, public rate-limited generators, and Stripe webhooks. Prisma owns the schema and migrations; Knex runs every query.
- Stripe, OpenAIBilling, generation
- Stripe for two plan families (Core and Bookmarks) a user can hold independently, which is why every webhook handler keys on the plan family, not the customer. Azure OpenAI hosts generation, with automatic fallback to OpenAI direct.
- MySQLSource of truth
- MySQL 8: users, plans, subscriptions, receipts, credits and usage, scheduled threads, tweets and media, scheduler events, X accounts, bookmarks, folders, smart folders and tags.
- RedisDelayed jobs, locks
- Redis 7 with AOF holds the BullMQ publish queue (delayed jobs) and the per-account token-refresh locks.
- Publish workerNode, BullMQ
- A standalone Node process consuming the publish queue. Claims a thread atomically, refreshes the X OAuth token under a Redis lock, streams media from object storage, uploads and posts each tweet through X API v2, persists X IDs for resume, and writes audit events. Deployed as separate staging and production stacks.
- X API v2OAuth 2.0 with PKCE
- X's official API: OAuth 2.0 with PKCE, token refresh, media upload and posting with in_reply_to_tweet_id, plus bookmark listing for the backup sync.