github gitroomhq/postiz-app v2.26.0
Encrypted channel tokens, connect links, Amazon SES & 40+ fixes

52 minutes ago

Highlights

This release brings encryption of the channel tokens in the database (opt-in, with a local key or AWS KMS), connect links so the public API or an MCP agent can hand someone a link that adds a channel to your workspace, Amazon SES as an email provider, and a channel checklist in the save and delete dialogs of multi-channel posts. Instagram carousels go up to 20 items and send alt text, and a sweep of billing and provider fixes removes false "may have already published" warnings, duplicate reconnect notifications and several silent billing dead ends.

Important

This release contains two access-control fixes (billing routes, and post saves across organizations), all users are recommended to upgrade. It also changes the Prisma schema (new indexes only) and adds optional environment variables. No existing Temporal workflow version changed, one new workflow was added. See Upgrade notes.

🔒 Security

  • Billing routes require an organization admin — only the invoices route checked the role, so a plain member could call the others directly (cancel, change the plan, end the trial, request a refund). They now answer 402 with "Only an admin of this organization can do this." (#2421)
  • Post saves are scoped to the organization — a save that carries post or group ids of another organization is refused with a 404 instead of being applied (#2422)
  • Channel tokens can be encrypted at rest, see below (#2430)

✨ Features

Token encryption

  • Channel tokens encrypted in the database — with ENCRYPTION_PROVIDER set, the access and refresh tokens of every channel are stored encrypted (AES-256-GCM). Two providers: local (the key in ENCRYPTION_KEY) and aws-kms (a data key that KMS decrypts once at start). Off by default, and nothing changes without it (#2430)
  • No migration needed: plain and encrypted tokens work side by side, and a plain token is encrypted the next time it is refreshed
  • New encryptTokensWorkflow encrypts the tokens that were stored before, started by hand from the Temporal UI or CLI
  • ENCRYPTION_READ_ONLY for rollouts where the backend and the orchestrator do not restart at the same moment
  • Orchestrator activities decrypt the tokens of a channel they receive from a workflow, the groundwork for keeping tokens out of the Temporal history (#2433)

Public API / MCP

  • Connect links — POST /public/v1/integrations/connect returns a link, valid for one hour, to a Postiz page where a person (with or without a Postiz account) reads what connecting the channel means and continues to the platform to approve the access. An optional redirectUrl sends them back to your app afterwards
  • New MCP tool integrationConnectTool, so an agent can give the user a link to connect a new channel
  • Connect links cover the platforms that connect with a plain redirect; platforms that need an API key, an instance URL, a wallet or the browser extension are still added from the dashboard. The channel limit of the plan is checked before the person is sent to the platform

Email

  • Amazon SES — EMAIL_PROVIDER="ses" with SES_REGION, using SES_ACCESS_KEY / SES_SECRET_ACCESS_KEY or the default AWS credential chain
  • Emails carry a List-Unsubscribe header that opens the account settings, where email notifications can be turned off (Resend, SES and Nodemailer)
  • Billing emails: the cancellation email goes to the customer instead of the internal address, and a new set-to-cancel email states the exact end date (#2393, #2394)

App

  • Choose channels in the save and delete dialogs — for a post created for several channels, Delete, Post now, Update / Schedule and Save as draft list the channels with a checkbox each, instead of "all channels / only this channel" (#2422)
  • Instagram: carousels of up to 20 media items (#2408), and media alt text is sent as alt_text on image containers (#2355)
  • YouTube: the official YouTube icon is used wherever a YouTube channel is shown (YouTube API Services branding requirements), the settings name the Privacy Status and show that the post text is the video description, and the title accepts 1 to 100 characters
  • Google My Business: the location picker shows the location address (#2390)
  • Sign-up attribution — a generic attribution provider layer reports new accounts to the configured tools, with LeadJourney as the first provider (LEADJOURNEY_POSTBACK_URL / LEADJOURNEY_POSTBACK_SECRET)
  • Optional RUNPOD_API_URL to point media processing at a local postiz-uploader during development (#2271)

🛡️ Reliability

  • Loading posts no longer blocks the backend on large organizations — the list is built in one pass and repeating posts are expanded only inside the requested date range, and HTTP keep-alive timeouts now outlast the load balancer's idle timeout, which removes a source of 502s
  • Token refresh: a failed refresh no longer stops the refresh of the remaining channels (#2357), a disconnect notifies once instead of once per failed refresh (#2358), and a concurrent refresh is waited for before the channel is flagged for reconnect (#2380)
  • The missing-post sweep re-checks the post state before it starts a workflow, and skips deleted posts (#2392)
  • Classified provider errors are kept through runInConcurrent instead of turning into generic failures (#2344)
  • Media reads record the real status when a ranged read is not answered with 206 (#2378)
  • Short links: the original URL is kept when the short link provider fails (#2417)
  • The streak counts in-flight and recurring posts (#2342)
  • New indexes for the calendar, notifications, login and admin stats queries

🔌 Provider fixes

  • LinkedIn: a token rejected on the create call is refreshed and the post is published, instead of stopping with a false "may have already published" warning (#2431); a LinkedIn Page channel that never finished page selection fails fast (#2409)
  • Instagram: a post Instagram published despite rejecting media_publish is recovered (#2361), media_publish is retried when Instagram says the media is not ready (#2410), and standalone connections surface token exchange failures instead of "Not enough scopes" and log the permissions Instagram returned (#2371, #2399)
  • TikTok: a video chunk upload answered with 5xx or 429 is retried (#2379), and inbox uploads that TikTok reports FAILED after the fact are logged (#2418)
  • TikTok Business: fails early when the OAuth redirect has no auth_code, and "The access token is invalid" is treated as a token error (#2343, #2345)
  • Threads: post length is counted in characters, not UTF-8 bytes (#2397), and the invalid link attachment error 4279047 is mapped (#2388)
  • Pinterest: the token is probed before the pin create is armed (#2360), and the "blocked this link because it may lead to spam" error is mapped (#2398)
  • Bluesky: the login identifier is normalized and connect-time errors are mapped (#2367)
  • Discord: a bot-token 401 is a post error instead of a reconnect (#2354)
  • Tumblr: the public post permalink is returned as the release URL (#2411)
  • Slack: the channel picker lists public channels only, and the groups:read scope is no longer requested
  • Extension channels reconnect through the extension flow (#2420)

🐛 Fixes

  • Billing: the cancel flow no longer ends silently at the discount offer (#2419), the finish-trial button says that it charges now (#2416), and plain members are told to ask an admin instead of being sent to the billing page (#2425)
  • Stripe: the subscription is always cancelled and the card detached when the trial card check fails (#2402); unpaid invoices of cancelled subscriptions are voided (#2405); the card that pays an unpaid invoice of a cancelled subscription is kept (#2395), and paying such an invoice re-subscribes the customer (#2406)
  • Admin: failed refunds are surfaced, and refunds of disputed charges are blocked (#2415)
  • Invites: an existing user who accepts an invite by signing in (OAuth or email and password) while logged out is now added to the inviting organization (#1644), and accepting an invite to an organization you already belong to no longer errors (#2368)
  • Calendar: the day view no longer crashes when a post's channel is missing (#2389), lists each channel once per slot with stable keys (#2391, #2407), and the calendar keeps a single drag and drop manager (#2356)
  • Public API: reconnecting a channel with GET /public/v1/social/:integration?refresh=<id> works with the integration id, as documented (#2396)
  • MCP: the Facebook url setting is described in the schema (#2359)
  • Analytics: neutral empty-state copy that says reconnect instead of refresh (#2424)

Upgrade notes

Database. The Prisma schema changed, indexes only. If you run the official Docker image, the schema is pushed on startup as usual. Otherwise run pnpm run prisma-db-push. New indexes:

  • Post: (organizationId, publishDate) and (publishDate, state, deletedAt, parentPostId, integrationId, organizationId, creationMethod)
  • Notifications: (organizationId, createdAt, deletedAt)
  • User: (providerId, providerName)
  • Errors: (createdAt, platform)

On a very large Post table the two new indexes take a while to build, and writes to the table wait until they are done. Plan the upgrade for a quiet moment if you hold millions of posts.

New optional environment variables (all can stay unset):

  • ENCRYPTION_PROVIDER (local or aws-kms), ENCRYPTION_KEY, ENCRYPTION_KMS_REGION, ENCRYPTION_KMS_DATA_KEY, ENCRYPTION_KMS_ACCESS_KEY, ENCRYPTION_KMS_SECRET_ACCESS_KEY, ENCRYPTION_READ_ONLY — encryption of the channel tokens
  • SES_REGION, SES_ACCESS_KEY, SES_SECRET_ACCESS_KEY — with EMAIL_PROVIDER="ses"
  • LEADJOURNEY_POSTBACK_URL, LEADJOURNEY_POSTBACK_SECRET — sign-up attribution
  • RUNPOD_API_URL — local development only, leave unset in production
  • MUSE_CHALLENGE — serves the Muse connector verification file at /.well-known/muse-challenge/<token>

Token encryption (only if you turn it on):

  • Keep the key safe: without it every channel has to be connected again.
  • When the backend and the orchestrator do not restart at the same moment, set the provider together with ENCRYPTION_READ_ONLY=true first, and remove it only once all of them run with the key. A service that does not have the key yet cannot use an encrypted token.
  • Once encrypted tokens exist, do not roll back to a release older than this one: it would send the encrypted value to the platforms.
  • Tokens stored before are encrypted the next time they are refreshed, or all at once by starting encryptTokensWorkflow on the task queue main.

Slack. Private channels no longer appear in the Slack channel picker, and new connections do not request groups:read. If you run your own Slack app, you can remove that scope from it.

Repeating posts. GET /public/v1/posts and the calendar expand a repeating post only inside the requested range, so occurrences dated before startDate are no longer returned.

Workflows. No existing Temporal workflow version changed in this release. One workflow was added: encryptTokensWorkflow, which only runs when you start it.

Full Changelog: v2.25.3...v2.26.0

Don't miss a new postiz-app release

NewReleases is sending notifications on new releases.