Skip to content
Development

The config change that was already done (Postiz public API)

By Victor Da Luz
postizself-hostingrubydev-logblog-manager

I picked up a small prerequisite issue this week. The whole thing was one line: “enable the public API on our self-hosted Postiz so we can generate an API key.” Postiz is the social scheduler I’m wiring blog-manager into, and the cross-posting build downstream needs that key. Easy ticket. Flip a flag, generate a key, move on.

Except there was no flag to flip. It was already on. Finding that out took longer than the change would have, and it turned into the actually useful part of the day.

What the issue assumed

The ticket I’d written for myself a few days earlier said to enable the public_api tier flag, then grab a key from the Developers tab in settings. That framing assumed Postiz works like the hosted SaaS version: pick a plan, the plan unlocks the API. So I went looking for where that plan lives in our deployment.

Our Postiz runs from an Ansible role, pinned to ghcr.io/gitroomhq/postiz-app:v2.21.8. Before touching anything I wanted to know exactly what gates that Developers tab. So I read the source. The frontend shows the tab when:

user?.tier?.public_api && isGeneral

Two conditions. isGeneral comes straight from the IS_GENERAL env var, which our compose template already sets to "true". The tier is the interesting one. On the backend:

tier: organization?.subscription?.subscriptionTier
  || (!process.env.STRIPE_PUBLISHABLE_KEY ? 'ULTIMATE' : 'FREE')

No Stripe key, no billing, so there’s no subscription object. The fallback kicks in, and with no STRIPE_PUBLISHABLE_KEY set it resolves to ULTIMATE. And in the pricing table, ULTIMATE.public_api is true (only the FREE tier is false). Self-hosting with billing turned off doesn’t drop you to a free tier. It hands you the top one.

So the chain was already complete: IS_GENERAL=true + no Stripe key, therefore ULTIMATE, therefore public_api true, therefore the tab renders. The “enable the API” task was a no-op. The right move was to log in and confirm the tab was there, which it was, generate the key, and close the ticket. No config change at all.

The part I almost got wrong

My first instinct was to read the gating logic off the main branch on GitHub, because that’s what the search results surface first. We don’t run main. We run v2.21.8. The tier logic probably hasn’t changed, but “probably” is how you end up reasoning about code that isn’t deployed. GitHub’s API takes a ?ref=v2.21.8 on file reads, so there’s no excuse. I re-read every file at the actual tag. Same conclusion, but now it was about the thing actually running.

This is a habit I keep having to re-learn. The deployed tag is the source of truth, not the default branch.

Two corrections that fell out of checking

Because I was reading instead of assuming, two other things in the ticket turned out to be wrong.

First, the ticket said to store the generated key in config/credentials.yml.enc. But that’s not where blog-manager keeps third-party keys. Every blog-publishing token in this app lives in an encrypted database column, surfaced in the UI. The one instance-wide key it already has, Pexels, sits on an AppSetting singleton with a field in the settings page. The Postiz key is the same shape, so it belongs there too, next to Pexels, not in the credentials file. The credentials file holds app infrastructure secrets, not integration keys. Following the ticket literally would have put it in the wrong place and broken the pattern.

Second, the ticket bundled “connect our social accounts” into the same prerequisite. But connecting an account in Postiz needs per-platform OAuth apps - a client ID and secret for X, for Bluesky, for LinkedIn, each registered and approved on that platform’s developer portal, each added as env vars on the Postiz container. None of that is configured, and none of it is blog-manager’s job. It’s infrastructure work on the homelab side. So I split it into its own issue over there and left this ticket as what it actually was: generate one key.

What I’d take away

The ticket described three tasks. Reading the source turned all three into something smaller or different: one was already done, one pointed at the wrong file, and one didn’t belong in this repo. The thirty minutes of reading saved a config change that would have done nothing, a credential stored in the wrong place, and a pile of OAuth setup filed under the wrong project.

I write tickets ahead of time so future-me has a plan. The catch is that past-me was guessing about how a tool I hadn’t deployed yet would behave. Self-hosted software keeps surprising me by being more generous than the hosted tier chart implies - turn off billing and you often get everything, because the gating exists to sell plans, not to restrict the code. Worth checking before you assume you’re on the cheap tier.

Related reading

Development

Add a feature, or move a responsibility?

Adding Postiz social cross-posting looked done until a blunt question exposed a double-post bug, and a full audit of every posting path in the app found two more like it.

Read