revolution/laravel-threads
A Laravel package for adding threaded conversations to your models, with support for nested replies and message threads out of the box. Useful for building comment systems, discussions, and structured messaging in PHP apps.
This is a very small (2★, 0 dependents) wrapper around Meta's Threads API. Treat the source as the documentation — the README likely shows only the happy path, so budget time to read src/ before committing to it in a project.
Minimal setup:
composer require revolution/laravel-threads
php artisan vendor:publish --tag=threads-config # check the actual tag in the provider
Then set credentials from your Meta app dashboard (Threads API uses the same app as Instagram Graph):
THREADS_APP_ID=
THREADS_APP_SECRET=
THREADS_REDIRECT_URI=https://yourapp.test/threads/callback
Where to look first, in this order:
src/ThreadsServiceProvider.php — see what it binds (singleton vs. per-request), which config keys it reads, and whether it registers routes.config/threads.php — the authoritative list of options; anything commented out is probably unimplemented.composer.json — confirm the Guzzle/Laravel version constraints; a 2-star package often lags a major Laravel release.First use case: the OAuth connect flow for a single account, then publish one text post. That exercises config, HTTP client, token handling, and the publish path in one go — if any of those are broken, you find out on day one rather than in production.
OAuth is a two-legged dance, not a one-liner. Redirect the user to the authorization URL, receive the code on your callback route, exchange it for a short-lived token, then immediately exchange that for a long-lived (~60 day) token. Persist it against your accounts/social_accounts model, not in the config file. The package may expose getAuthorizationUrl() / exchangeCode() — if it doesn't, drop to Http:: and keep the package for the publishing side only.
Instantiate per account, don't lean on the facade. A singleton client bound to one token is the single most common failure mode in multi-tenant social integrations. Prefer building a client per account:
$client = app(ThreadsClient::class)->withToken($account->threads_token);
$client->publish($text);
If the package only ships a container-managed singleton, wrap it in your own ThreadsPublisher that news up the client with an explicit token. That one adapter also gives you a seam to swap the package out later.
Publications are two-step. The Threads Graph API creates a media container first, then publishes it by ID — POST /{user-id}/threads returns a container ID, then POST /{user-id}/threads_publish. Whether the package hides this behind one method or exposes both, model it in your own code as a PublishThread job so retries don't create duplicate containers.
Queue everything outbound. Wrap publishes and insight pulls in jobs with $tries = 3 and backoff() that increases, and a RateLimited exception handler. Publishing synchronously in a web request will hang your UI on Graph API latency spikes.
Test with Http::fake(). Assert on the URLs and bodies your code sends rather than on package behaviour — this pins the contract and means a package rewrite doesn't break your tests. Keep recorded Graph API response fixtures for the error shapes you care about.
Schedule the token refresh. Long-lived tokens expire in ~60 days; a nightly job that refreshes anything within 15 days of expiry saves you a silent-failure incident.
Container IDs expire fast (roughly 24 hours). If your queue backs up or a job retries the next day with a stored container ID, publishing fails. Create and publish in the same job execution, and treat container IDs as ephemeral.
Rate limits are per-user and surprisingly low on Threads — on the order of a few hundred publishes per 24 hours. Add a per-account counter or a short RateLimiter key so a bulk backfill doesn't get the account throttled for a day.
Graph API errors are inconsistent. You'll see errors returned with an error object, sometimes with an HTTP 200, and subcodes that matter more than the top-level message. Normalise into a single exception type in your adapter and log the raw payload — debugging from "message": "Unknown error" alone is miserable.
Config caching bites. php artisan config:cache in deploy will freeze credentials; make sure tokens live in the database, and that any token read from config is dev-only.
Pin the version exactly ("revolution/laravel-threads": "0.1.*" or a commit hash). At 2 stars with zero dependents there is no ecosystem pressure to keep BC; a patch bump can rename the facade. Also check whether it's a thin wrapper over facebook/graph-sdk or hand-rolled Http:: calls — the former brings its own version conflicts.
Extension points worth using: bind your own implementation of the client interface in a service provider so you can intercept requests (logging, retries, metrics) without forking; listen for any events the provider dispatches after a successful publish to decouple analytics; and use Http::globalMiddleware or a Guzzle handler stack to add correlation IDs to every Graph call.
Have an exit plan. Write your application code against your own 3–4 method interface (connect, publish, replies, insights) from the start. Given the maturity level, the realistic outcome is you replace this package with 200 lines of Http:: calls within a year — and that should be a one-file change.
How can I help you explore Laravel packages today?