Skip to content

Releases: GkhanKINAY/postqueen-app

v3.4.2

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 09 Aug 17:57
8d91593

Dependabot was switched on today and immediately reported seven critical advisories. Four of them are closed here.

Package What it was Fixed in
protobufjs Arbitrary code execution 7.6.5
handlebars JavaScript injection via AST type confusion 4.7.9
shell-quote quote() failing to escape newlines 1.10.0
vitest Arbitrary file read and execute while the UI server listens 3.2.6

Three of the four arrive through other packages, so version pins are what actually close them — bumping a direct dependency leaves the older copies in place.

24 findings closed. 181 remain, down from 264 this morning.

The three criticals still open both need major upgrades: tar 7.x, which arrives through bcrypt's build tooling, and happy-dom 20.x. Each is a change with its own risk and will be taken on its own rather than swept through. The full list, with a reason per entry, is in .github/osv-known.txt.

Verified by booting the container image on the production host before deploying it.

Full diff: v3.4.1...v3.4.2

v3.4.1

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 09 Aug 17:09
34a710d

A security release, and an honest one.

What was wrong

Nothing had ever scanned this project's dependencies. pnpm audit cannot run here at all — it exhausts memory and is killed even with an 8 GB heap, so the failure was never a Node version problem; the dependency graph is simply too large for it. Dependabot was off. The state was unknown rather than clean.

Scanning the lockfile directly found 264 known vulnerabilities across 75 packages. One was checked before acting on the number: GHSA-35jp-ww65-95wh, a full man-in-the-middle via prototype pollution in axios, affecting every 1.x up to 1.16.0. We were on 1.14.0.

What this release fixes

59 closed, including all 28 in axios and all 18 in undici, plus dompurify, postcss, ws, brace-expansion, form-data and follow-redirects. Seven packages are pinned to single patched versions, because a direct bump was not enough on its own — axios moved to 1.19.0 while an older transitive copy stayed behind.

The scan now runs in CI, on every lockfile change and weekly, because a lockfile that has not moved still becomes vulnerable the day an advisory lands against something in it. It fails on findings that are new, not on the ones already known and recorded with a reason.

What is not fixed, and why

205 remain. tar needs 7.x and multer 2.x, both major upgrades; lodash's advisory names a version its own consumers will not accept. Each is a change with its own risk and will be judged on its own rather than swept through in a batch. The full list, with reasons, is in .github/osv-known.txt.

A note on how this went

The first attempt at this release closed 170 rather than 59 — and stopped the backend from starting. It was deployed, the API answered 502, and it was rolled back within three minutes. No users exist yet, so nobody was affected.

The cause was a dependency moving 36 minor versions inside its own caret range and landing on an incompatible peer. The reason it got through was simpler: all three applications were built and typechecked, and not one of them was run.

This release was verified by booting the actual container image on the production host before deploying it.

Full diff: v3.4.0...v3.4.1

v3.4.0

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 09 Aug 14:34
28aa862

Everything upstream has done since the fork, brought in. 41 of gitroomhq/postiz-app's 42 post-fork commits, across eight PRs.

The one that mattered most

Large videos no longer go through memory. Provider uploads read the whole file into a Buffer before sending it — on a container capped at 4 GB and already sitting at 2.2 GB, with backend, frontend and orchestrator all inside it. One large upload could take the application down, not just the upload. Eleven providers now stream the range they are about to send.

Publishing correctness

A post can no longer be published twice. Some platforms accept a post and confirm it later; publishing against them blind is how the same content reaches an audience twice. There is now a publish-then-confirm contract, implemented across twelve providers.

Publishing an already-published post needs an explicit opt-in. Saving one as a schedule used to clear its release id and start the workflow again. The confirmation now names the channel and the original publish date, and says so when the post recurs.

Google Business posts that Google answered with REJECTED, or with a non-JSON body, were recorded as published. Pinterest video pins uploaded whatever media happened to be first rather than the mp4. Reddit parsed its upload response without a guard. AI video generation hung forever when the provider reported a failure.

Security

SSRF protection reaches the paths that use axios rather than fetch, and the media and webhook fetches pin DNS at request time rather than trusting what was resolved at save time.

Sign-in and accounts

Email lookups are case-insensitive for login, password reset and impersonation — anyone who registered as Ali@… and later typed ali@… could not get in. The organization switcher shows each member's role, and the Developers screen names the organization an OAuth app will belong to.

New

Admin coupon tools for support, X's article format, and admin statistics per platform. Sentry now has user, organization and Stripe customer context ready for whenever it is switched on.

Not taken

One commit, 48bf76af, points the security policy at upstream's own advisory intake. Reporting a vulnerability in this product should not route to a different company.

Upgrading

No schema change and no manual Temporal step. Post workflows are per-post: new posts use v1.0.7 while anything already in flight keeps replaying the version it started with.

How the sync is done, and why dependency versions are held in step with upstream, is written down in docs/upstream-sync.md.

Full diff: v3.3.3...v3.4.0

v3.3.3

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 09 Aug 11:25
e4a5edb

A self-audit of v3.3.2, on the principle that the round which fixes ten things is the round most likely to break an eleventh. Three did not survive the re-read.

The one that mattered

A founding purchase could have taken channels away. v3.3.2 added a channel sync to the redeemed-code path so that someone whose subscription lapsed and who then bought a lifetime deal would get their channels back. That sync runs in both directions. A founding purchase always grants Pro, thirty channels, and an organization still in its trial reads as Agency, a million — so a customer converting with more than thirty live channels would have had the excess switched off, at the moment they paid, on a path where nothing happened at all before. The give-back half is now its own function and that path calls only it.

Two smaller ones

A refused multipart upload was post-processed anyway: the media controller reads the upload location off whatever the handler returns, and several branches inside answer the request themselves — an unsupported extension, a body that does not match its declared type, and, new in v3.3.2, a key the organization never created. That threw on top of an already-sent response. The client always got the right status; the server logged noise it did not need to.

Reconnecting a channel left behind the marker that says the system switched it off, so a live channel could carry a claim that was no longer true. Nothing acts on it today, but a half-true invariant is how the next change goes wrong.

What was checked and left alone

Recorded in docs/ui-migration-log.md so the next pass does not re-derive it: the third-party function allowlist resolves the real provider's methods and refuses the inherited ones; the request organization does carry its subscription, and cancellation hard-deletes it, so the new autopost tier check reads Free after a cancellation as intended; the orchestrator never calls the method whose signature changed; and the upload-ownership map works on installs without Redis, because each app runs as a single process.

The previous round's guarantees were re-tested rather than trusted — a downgrade still switches off the newest channels first, an upgrade still respects a channel the user disabled themselves, and an owned upload key still gets through.

Upgrading

Nothing to do. No schema change and no workflow change in this release.

Full diff: v3.3.2...v3.3.3

v3.3.2

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 09 Aug 10:39
a4f6b12

The recorded follow-up backlog, cleared rather than carried into launch. Four PRs, one release.

What a lapsed subscription used to do

It destroyed scheduled posts in silence. getPost answers falsy for an org with no subscription exactly as it does for a post that no longer exists; the publish workflow turns either into ERROR / 'No Post'; and that string is on the silence list, so nothing reached the customer. The hourly recovery sweep could not save it either, because it only collects posts still in QUEUE. Content someone scheduled and paid for was gone, with no notification and no reason they would recognise on the calendar. The reason written now is Subscription required, and it notifies. The post stays in error rather than returning to the queue on purpose: a post scheduled three weeks ago should not fire the instant someone resubscribes.

It switched channels off with nothing to switch them back on. A returning customer found their channels dark and had to click through them one at a time, while team members — disabled two branches below the channel branch in the same method — came back on their own. Telling "the system took this away" from "I turned this off deliberately" needed a new column, so an upgrade cannot override someone's own choice. Only channels a plan change disabled are returned, oldest-lost first, never past the new limit, never one that needs reconnecting. Autopost rules are deliberately not resumed: enabling a channel publishes nothing by itself, an autopost rule does.

The safety net had none of its own

missingPostWorkflow is the only thing in the product that catches a post the normal path dropped, and it had no try/catch and no continueAsNew. One exhausted activity ended it, and nothing restarted it until the next backend restart — silently, with the first symptom being posts not going out days later. History growth would have terminated it eventually anyway. v2 follows the shape autoPostWorkflowV2 already established.

Separately, TEMPORAL_TLS=true skipped registering the two search attributes that both the start path and the cancel sweep address workflows through, so a TLS install would quietly schedule nothing.

Four places that trusted the caller

None exploitable, all the same mistake: third-party's function dispatch applied any name off the URL to the provider instance; an OAuth app's logo could point at another org's media, on the screen where a user decides whether to trust the app; the five R2 multipart endpoints acted on any key handed to them; and local upload filenames came from Math.random, which is the only thing guarding a file served from a public directory.

Also

An uploaded mp4 was published to Google Business and Telegram as a still image — updateMedia labelled every attachment as an image. Enabling an autopost rule now needs a plan that includes autopost; switching one off still needs nothing.

Three items on the follow-up list turned out to be already fixed. They are struck through in docs/ui-migration-log.md rather than deleted, because a list nobody re-verifies sends the next pass chasing finished work.

Upgrading

Integration.autoDisabledAt is a new nullable column. After deploying, terminate the old missing-post-workflow execution in Temporal — it is an infinite loop that never ends on its own, so until then both the old and new sweep run.

Full diff: v3.3.1...v3.3.2

v3.3.1

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 09 Aug 02:35
d53680d

The last release before launch. Three fixes, one of them a security hole.

Security

A session cookie could mint a free unlimited organization. All three /public/v1/… enterprise routes took the caller's Authorization header and verified it against JWT_SECRET, which is the same secret that signs ordinary login cookies. Any registered user could therefore call them and create an organization on a lifetime AGENCY subscription. They now verify against ENTERPRISE_SECRET, and refuse outright when that variable is unset.

Payments

The payment path could cancel the subscription it just sold. checkValidCard attempted an off-session $1 authorization on every first customer.subscription.created, and cancelled the subscription and detached the card whenever it did not come back requires_capture — including on any thrown error. In an SCA region that is the expected outcome for a 3DS card, not an exception. Stripe Checkout already validates the card, so the check now logs and returns true.

Alongside it, in the same path: statuses that are not entitlements (canceled, unpaid, incomplete_expired, paused) no longer write a paid tier; products.list pages to 100 so the catalog cannot outgrow the default page of 10; uniqueId is restored to plan-change metadata so both webhooks can find the row; invoice.payment_failed is typed info rather than success, so a customer who turned success mail off still hears about a failed renewal; an unsigned request to /stripe returns 400 instead of a 500 with a stack trace; and the post-checkout screen stops polling after 60 tries and offers a way out instead of holding the customer in an uncloseable overlay.

Correctness

Config-off paths tell the truth. Password reset and team invites checked their expiry window without first checking the field existed, so a null compared as "not yet expired". The AI availability rule lived in three places and disagreed with itself; it is now one exported function. Date formatting read the browser at module load, which does not exist during SSR — it reads the resolved snapshot instead. post.workflow.v1.0.6 replaces v1.0.5's generic-error branch, which fell through and re-ran postSocial after a failure that had already been handled.

Full diff: v3.3.0...v3.3.1

PostQueen v3.3.0

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 08 Aug 23:09
55fc31c

Autopost and Webhooks hardening, launch polish, and the three subscription gates that were left open before launch. 18 commits since v3.2.0.

Fixed

Autopost never actually published. Every rule produced drafts. Turning that on exposed the rest of the path:

  • The same feed item could be published more than once — the de-dupe cursor was written after the posts were scheduled, so a Temporal retry re-posted. The item is now claimed before scheduling.
  • A PUT that omitted lastUrl wiped the cursor, and autoPublish switched itself off the same way. Defaults now apply on create only.
  • Feed items vanished during a token outage. Skipping channels that cannot publish is right for auto-publish and wrong for drafts, so the filter is conditional on the mode.
  • One failed post sent up to fifteen notifications and fifteen emails, two of them internal sentinels, one on a post that had actually succeeded.
  • The failure reason shown in the calendar was either empty or a raw stack trace.

Webhooks were never delivered to any endpoint that redirects. redirect: 'manual' was not the SSRF protection — the dispatcher pins DNS on every hop, so redirects can be followed safely. Webhook deliveries now also carry real post data where they previously carried an empty array; if you consume PostQueen webhooks, check your receiver handles a populated payload.

A notification could abort the Stripe webhook, so a mail hiccup meant the customer was charged and the subscription row was never written.

The throttler answered 500 to anyone who asked. Route matching ran against req.url, which carries the query string, so POST /auth/login?x=/public/v1/posts entered the rate limiter and died on an undefined organisation — unauthenticated, repeatable, on any POST route. Separately, every upload and every public-API post threw when REDIS_URL was unset.

Auto Post was gated by the webhook quota rather than by its own pricing flag: an organisation that had used its webhook allowance could not create an autopost for an unrelated reason. Creator now includes Auto Post.

/copilot/chat was the only AI route with no subscription check.

The released version never reached the app. The container build passed the tag in under a name nothing read, so v3.1.4 through v3.2.0 all displayed "v3.0" and every Sentry event was filed under release 3.0.2.

Changed

  • Loading and empty states consolidated: one skeleton, one spinner, ghosts that mirror the markup they replace, and no indefinite animation that ignores prefers-reduced-motion.
  • Sign-up no longer waits on an email round-trip; the emailed code is a way to sign in, not a way to sign up.
  • ESLint runs again — and CI runs at all, for the first time in this repository.
  • .env.example documents 30 variables that the code read and it did not mention.

Upgrading

Deploy the orchestrator before the backend, then backend and frontend together. processCron now starts autoPostWorkflowV2 with TERMINATE_EXISTING; if the backend goes first, saving a rule terminates its running v1 execution and replaces it with a workflow type the old worker does not know.

Create the new index by hand first, in a low-traffic window:

CREATE INDEX CONCURRENTLY IF NOT EXISTS "Post_releaseId_idx" ON "Post"("releaseId");

The migration creates it without CONCURRENTLY — Prisma runs migrations in a transaction — which takes a write lock on Post for the duration of the build.

If you run with PRISMA_MIGRATE=true, confirm _prisma_migrations contains 0_init before deploying. A database originally created with db push has no migration history, and migrate deploy will fail replaying the initial migration against a populated schema. Baseline it once with prisma migrate resolve --applied 0_init.

Check REDIS_URL is set. Rate limiting now falls back to a per-process counter without it, so N backend instances give N times the intended limit.

PostQueen v3.2.0

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 08 Aug 17:35
162751e

First release since v3.1.7, gathering eight merged pull requests: the sign-up
flow changes below plus the accumulated frontend redesign work.

Read this first if you self-host

PASSWORDLESS_LOGIN has changed meaning. It used to replace the password
form on both the sign-in and the sign-up screen, so every new account had to
wait for an emailed code before it could do anything. It now means the sign-in
screen also offers an emailed code, next to the password. Sign-up always uses
email and password and goes straight into the app.

Nothing to change in your environment. If you had the flag on, you keep the code
option where it is useful and lose the wait where it was costing you sign-ups.
Accounts created through the old code-only flow have no password, and they keep
working: the code path on the sign-in screen is still there, so they sign in
exactly as before. No migration.

REQUIRE_EMAIL_ACTIVATION is new, and off by default. Activation used to be
inferred from having an email provider configured, which tied two unrelated
decisions together: wanting working password-reset mail forced you to also gate
sign-up behind a click in the inbox. Those are now separate. If you want new
accounts to confirm their address before they can log in, set it to true; it
also needs EMAIL_PROVIDER, otherwise the link can never be delivered and every
new account is locked out.

Existing accounts are untouched either way. Nothing here is retroactive, and
there is no database migration in this release.

Sign-up and sign-in

  • Sign-up no longer waits on an email round-trip (#20)
  • The emailed code is a way to sign in, not a way to sign up (#22)

Interface

  • Calendar, Channels and Settings fidelity pass (#13)
  • Launch panel smart tab, media picker and overlay (#14)
  • Uppy thumbnail race and Insert Media grid polish (#15)

Housekeeping

  • Remaining backlog cleared (#16)
  • Production and self-host DEV cleanup (#17)
  • Public self-host repo hygiene (#18)

Images

ghcr.io/gkhankinay/postqueen-app:v3.2.0
ghcr.io/gkhankinay/postqueen-app:latest

Multi-arch, linux/amd64 and linux/arm64.

PostQueen v3.0.4

Choose a tag to compare

@GkhanKINAY GkhanKINAY released this 22 Jul 20:53

The current cloud release of PostQueen, the open-source, AI-native social media scheduler. Schedule to 30+ networks from a visual calendar, the CLI, the public API, the MCP server, or your own AI agent. Get started: https://postqueen.ai/pricing (7-day free trial) or self-host with https://github.com/GkhanKINAY/postqueen-docker-compose. Docs: https://docs.postqueen.ai