Lossless Changelog
fleet

Postiz Lands on reach-edu — Five Services, Three Gotchas, One Social Command Center

The stack's Hootsuite-killer is live at postiz.reach-edu.didi.sh: Postiz v2.22.1 deployed to the reach-edu Railway project with a full Temporal orchestration layer, after fighting through a search-attribute quota, a soft-deleted volume, Railway's injected PORT, and a session cookie that browsers refused to store.

Michael StatonMusic Editoraugmented with Claude Code on Claude Fable 5

Why Care?

Postiz has sat on the README's replacement roster (Hootsuite $1.2k–3k/yr, Buffer $2.4k/yr, Sprout $199+/seat/mo → $0) since the beginning, with the upstream repo pinned under core/postiz-app. Today it went from roster row to running software: reach-edu now has a self-hosted social command center at https://postiz-production-d863.up.railway.app, verified by browser drive down to the register screen and a live backend API.

What's New?

  • Five-service bundle in the reach-edu Railway project: postiz (pinned ghcr.io/gitroomhq/postiz-app:v2.22.1, volume at /uploads), postiz-postgres (17-alpine), postiz-redis (volumeless, Twenty pattern), temporal (temporalio/auto-setup:1.28.1), and temporal-postgres (16). Postiz v2 schedules posts as Temporal workflows, so the orchestrator is load-bearing, not optional — but we run it with Postgres visibility (ENABLE_ES=false), dropping upstream's Elasticsearch, temporal-ui, and admin-tools services entirely.

  • docs/postiz/setup.md — the per-client runbook, born with a four-entry gotcha log from this first execution:

    1. SKIP_ADD_CUSTOM_SEARCH_ATTRIBUTES=true is mandatory — auto-setup's demo attributes eat 2 of SQL visibility's 3 Text slots, and Postiz's backend crashloops registering its own organizationId/postId.

    2. Railway volume deletion is a 2-day soft-delete with no public recover mutation and staged-change limbo on detach; recreating the whole postgres service is faster than fighting it.

    3. Railway injects PORT=8080; Postiz's backend honors it while the container's internal nginx expects 3000 → every API call 502s until PORT=3000 is pinned.

    4. ${{service.RAILWAY_PRIVATE_DOMAIN}} references resolve to EMPTY (silently) after the referenced service is deleted.

    5. Auth is impossible on *.up.railway.app — Postiz scopes its session cookie to the registrable domain of FRONTEND_URL, which computes to Domain=.railway.app, a Public Suffix List apex every browser rejects. Registration returns 200, the account exists, and the user stays logged out forever with zero backend errors. The custom domain (postiz.reach-edu.didi.sh, cut over same-day via the custom-domain-cutover playbook) is therefore load-bearing, not cosmetic; the cookie now lands as Domain=.didi.sh and sticks. Verified end-to-end with a curl login on the new domain.

Scope Note

Postiz publishes; it does not follow. The companion ambition — following every person and org with profile info in the SurrealDB canonical layer — is a per-platform job (feasible via AT Protocol/ActivityPub for Bluesky/Mastodon, not offered at accessible API tiers by LinkedIn/X) and is explicitly out of this deploy's scope, documented in the runbook.

Next

Operator gate: sign in at postiz.reach-edu.didi.sh/auth/login (the account created during the cookie saga is intact), then flip DISABLE_REGISTRATION=true. Per-platform OAuth credentials get collected as channels are connected. Hub card follows the usual path; the domain is already done.