From 33cbc57b59b3883a69e1e17805a6460e4c4fac6a Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Wed, 1 Apr 2026 09:43:15 -0300 Subject: [PATCH] docs: add publishing engine improvements spec (Postiz comparison) --- ...1-publishing-engine-improvements-design.md | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-01-publishing-engine-improvements-design.md diff --git a/docs/superpowers/specs/2026-04-01-publishing-engine-improvements-design.md b/docs/superpowers/specs/2026-04-01-publishing-engine-improvements-design.md new file mode 100644 index 00000000..8a487dfa --- /dev/null +++ b/docs/superpowers/specs/2026-04-01-publishing-engine-improvements-design.md @@ -0,0 +1,257 @@ +# Publishing Engine Improvements + +Based on comparative analysis of Postiz's publishing engine vs ours. + +## 1. Rate Limit Retry (429 handling) + +### Problem + +When a platform API returns 429 (Too Many Requests), our publishers throw an exception and the post fails. The user has to manually retry. Postiz retries automatically with a 5-second delay, up to 3 times. + +### Solution + +Add a `retry()` middleware to all HTTP calls that hit social platform APIs. Laravel's HTTP client supports `retry()` natively. + +```php +// Before: +$response = Http::withToken($token)->post($url, $data); + +// After: +$response = Http::withToken($token) + ->retry(3, 5000, fn ($e, $request) => $e->response?->status() === 429) + ->post($url, $data); +``` + +### Implementation + +Create a trait `HasSocialHttpClient` that all publishers use: + +```php +trait HasSocialHttpClient +{ + protected function socialHttp(): PendingRequest + { + return Http::retry( + times: 3, + sleepMilliseconds: 5000, + when: fn ($exception, $request) => $exception->response?->status() === 429, + throw: false, + ); + } +} +``` + +Each publisher replaces `Http::withToken(...)` calls with `$this->socialHttp()->withToken(...)`. + +### Files Changed + +- Create: `app/Services/Social/Concerns/HasSocialHttpClient.php` +- Modify: All 11 publishers to use the trait + +--- + +## 2. Token Refresh Inline During Publishing + +### Problem + +We refresh tokens **before** publishing, but if the token expires **during** a long upload (e.g., 244MB YouTube video), the publish fails. Postiz retries up to 5 times with inline token refresh between attempts. + +### Solution + +Wrap the publish call in the `PublishToSocialPlatform` job with a retry loop that catches `TokenExpiredException`, refreshes the token, and retries: + +```php +// In PublishToSocialPlatform::handle() +$maxAttempts = 2; // 1 retry after token refresh + +for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { + try { + $result = $publisher->publish($this->postPlatform); + $this->postPlatform->markAsPublished(...); + break; + } catch (TokenExpiredException $e) { + if ($attempt < $maxAttempts) { + $this->refreshTokenAndRetry($e); + continue; + } + // Final attempt failed — disconnect account + $this->postPlatform->markAsFailed($e->getMessage()); + $this->postPlatform->socialAccount->markAsDisconnected($e->getMessage()); + } +} +``` + +The `refreshTokenAndRetry` method calls the platform-specific refresh (same logic as `ConnectionVerifier::refreshTokenIfNeeded`). + +### Files Changed + +- Modify: `app/Jobs/PublishToSocialPlatform.php` +- Extract: Token refresh logic from `ConnectionVerifier` into a reusable `TokenRefresher` service + +--- + +## 3. Concurrency Control Per Platform + +### Problem + +If 50 posts are scheduled for the same time, all 50 jobs hit Instagram's API simultaneously, causing rate limits and failures. Postiz uses per-platform task queues with `maxConcurrentJob`. + +### Solution + +Use Horizon's queue configuration to create per-platform queues with max processes: + +```php +// config/horizon.php +'environments' => [ + 'production' => [ + 'social-instagram' => [ + 'connection' => 'redis', + 'queue' => ['social-instagram'], + 'maxProcesses' => 2, + 'timeout' => 630, + ], + 'social-tiktok' => [ + 'connection' => 'redis', + 'queue' => ['social-tiktok'], + 'maxProcesses' => 2, + 'timeout' => 630, + ], + // ... per platform + ], +], +``` + +The `PublishToSocialPlatform` job dispatches to the platform-specific queue: + +```php +public function __construct(public PostPlatform $postPlatform) +{ + $this->onQueue('social-' . $postPlatform->platform->value); +} +``` + +### Platform Concurrency Limits (from Postiz) + +| Platform | Max Concurrent | Our Queue maxProcesses | +|---|---|---| +| Instagram | 400 | 3 | +| TikTok | 300 | 2 | +| YouTube | 200 | 1 | +| Facebook | default | 3 | +| LinkedIn | default | 2 | +| X/Twitter | default | 2 | +| Threads | default | 2 | +| Pinterest | default | 2 | +| Bluesky | default | 2 | +| Mastodon | default | 2 | + +### Files Changed + +- Modify: `config/horizon.php` — add per-platform queues +- Modify: `app/Jobs/PublishToSocialPlatform.php` — dispatch to platform queue +- Modify: `app/Jobs/PublishPost.php` — pass platform info when dispatching + +--- + +## 4. Webhooks Post-Publish + +### Problem + +Users building integrations (Zapier, Make, custom CRM) can't programmatically know when a post is published. Postiz fires webhooks after each successful publish. + +### Solution + +Add a `Webhook` model and fire webhooks after post status changes. This is a larger feature that deserves its own spec. + +### High-Level Design + +- `webhooks` table: `id, workspace_id, url, events (json), secret, is_active` +- Events: `post.published`, `post.failed`, `account.disconnected` +- Fire webhook in `PublishToSocialPlatform` job after status update +- Sign payload with HMAC-SHA256 using the webhook secret +- Async dispatch via a `SendWebhook` job +- Retry 3x with exponential backoff + +### Files Changed + +- Create: Migration, Model, Controller, FormRequest for Webhooks CRUD +- Create: `app/Jobs/SendWebhook.php` +- Modify: `app/Jobs/PublishToSocialPlatform.php` — dispatch webhook after publish +- Create: Frontend components for webhook management + +--- + +## 5. Threads / Comments Support + +### Problem + +Postiz supports posting a main post + sequential comments (Twitter threads, Instagram first comment). We only post single posts. + +### Solution + +This requires significant data model changes: + +- A `Post` can have ordered child `Post` records (thread items) +- The publisher publishes the first post, then iterates over children posting each as a reply/comment +- Each platform's comment API is different (Twitter reply_to, Instagram comment endpoint, etc.) + +### High-Level Design + +- Add `parent_post_platform_id` to `post_platforms` table +- Add `delay_seconds` column for delayed comments +- Extend each publisher with a `comment()` method (like Postiz) +- The job publishes main post → waits for delay → publishes each comment + +This is the largest feature. Deserves its own dedicated spec + plan. + +### Files Changed + +- Migration: Add columns to `post_platforms` +- Modify: All publishers to add `comment()` method +- Modify: Frontend to support thread/comment creation UI +- Modify: `PublishToSocialPlatform` job to handle sequential publishing + +--- + +## 6. Proactive Token Refresh + +### Problem + +Currently we only refresh tokens reactively (when publishing) and via daily `CheckSocialConnections`. Postiz runs a dedicated workflow per integration that sleeps until token expiry and proactively refreshes. + +### Solution + +Create a scheduled command that runs every hour and refreshes tokens expiring in the next 2 hours: + +```php +// app/Console/Commands/RefreshExpiringTokens.php +SocialAccount::query() + ->where('status', Status::Connected) + ->whereNotNull('token_expires_at') + ->where('token_expires_at', '<=', now()->addHours(2)) + ->where('token_expires_at', '>', now()) + ->chunk(50, function ($accounts) { + foreach ($accounts as $account) { + RefreshSocialToken::dispatch($account); + } + }); +``` + +### Files Changed + +- Create: `app/Console/Commands/RefreshExpiringTokens.php` +- Create: `app/Jobs/RefreshSocialToken.php` +- Modify: `routes/console.php` — schedule hourly + +--- + +## Priority Order + +| # | Feature | Impact | Effort | When | +|---|---|---|---|---| +| 1 | Rate limit retry (429) | High | Low | This sprint | +| 2 | Token refresh inline | High | Medium | This sprint | +| 3 | Concurrency control | Medium | Medium | This sprint | +| 6 | Proactive token refresh | Medium | Low | This sprint | +| 4 | Webhooks | Medium | High | Next sprint | +| 5 | Threads/Comments | High | Very High | Future |