⚠️ Disclaimer: postflow is in active development. It is not tested enough yet to guarantee correct behavior in all scenarios. Use at your own risk.
postflow is a lightweight social publishing service with:
- Web UI
- HTTP API
- MCP endpoint (Streamable HTTP)
- CLI (
postflow)
This README is a basic setup guide.
- Go 1.26.1+
- Optional: Homebrew (for installing CLI binary)
git clone https://github.com/antoniolg/postflow.git
cd postflow
cp .env.example .envGenerate required secrets:
# 32-byte base64 key (required)
openssl rand -base64 32
# API token (recommended)
openssl rand -hex 32Put those values in .env:
POSTFLOW_MASTER_KEY=<base64-from-openssl>
API_TOKEN=<hex-token>
PUBLIC_BASE_URL=http://localhost:8080
OWNER_EMAIL=owner@example.com
OWNER_PASSWORD_HASH=<bcrypt-hash>
POSTFLOW_DRIVER=mockGenerate OWNER_PASSWORD_HASH with the helper script in this repo:
go run ./scripts/hash-password.go 'replace-with-your-password'If you store it in a local .env, quote the value because bcrypt hashes contain $:
OWNER_PASSWORD_HASH='$2a$10$...'Run:
go run ./cmd/postflow-serverOpen:
- UI:
http://localhost:8080 - MCP:
http://localhost:8080/mcp
Use .env.example as template. These are the key ones:
| Variable | Required | Where it comes from |
|---|---|---|
POSTFLOW_MASTER_KEY |
Yes | Generate locally: openssl rand -base64 32 |
API_TOKEN |
Recommended | Generate locally (random token), kept for API/MCP auth for CLI, Codex, Claude, and other legacy clients |
OWNER_EMAIL |
Recommended for UI/ChatGPT | Owner email for the single-user local login |
OWNER_PASSWORD_HASH |
Recommended for UI/ChatGPT | Bcrypt hash for the owner password |
PUBLIC_BASE_URL |
Yes for OAuth and Instagram media URLs | Your app URL (http://localhost:8080 locally, your public HTTPS domain in prod) |
UI_BASIC_USER / UI_BASIC_PASS |
Temporary compatibility only | Optional legacy Basic Auth fallback for the UI |
| Variable | Default | Notes |
|---|---|---|
PORT |
8080 |
HTTP port |
DATABASE_PATH |
postflow.db |
SQLite DB path |
DATA_DIR |
data |
Uploaded media path |
| Network | Variables | Where to get them |
|---|---|---|
| X | X_CLIENT_ID, X_CLIENT_SECRET |
X Developer Portal OAuth 2.0 app credentials for account connection |
LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET |
LinkedIn Developer app with member posting enabled. To connect company pages, the app must also have organization posting/admin scopes approved. | |
| Facebook/Instagram | META_APP_ID, META_APP_SECRET |
Meta Developers app |
Important:
- If you want real publishing, set
POSTFLOW_DRIVER=live. - For local testing without real publishing, keep
POSTFLOW_DRIVER=mock. - OAuth account connection is available for X, LinkedIn, Facebook, and Instagram.
- LinkedIn OAuth connects the personal profile and, when available, any organization pages the user administers.
- In the web UI, if an OAuth provider returns multiple accounts, PostFlow shows a selection step before saving them.
- In production (Coolify), set secrets in the platform UI, not in committed files.
- In Coolify, mark
OWNER_PASSWORD_HASHas a literal/secret value so$is not interpolated.
Endpoint:
http://localhost:8080/mcp
For Codex, Claude, CLI, and other legacy clients, if API_TOKEN is set, send:
Authorization: Bearer <API_TOKEN>
For ChatGPT / remote MCP clients with OAuth:
- Authorization metadata:
http://localhost:8080/.well-known/oauth-authorization-server - Protected resource metadata:
http://localhost:8080/.well-known/oauth-protected-resource - The login page is
http://localhost:8080/login - Dynamic client registration is available at
POST /oauth/register - PostFlow allows MCP discovery requests without auth (
initialize,notifications/initialized,ping, andtools/list) so ChatGPT can complete the handshake cleanly. - Actual MCP tool execution (
tools/call) remains protected and requires OAuth bearer auth (or the legacyAPI_TOKENflow for non-OAuth clients).
Main MCP tools available:
postflow_healthpostflow_list_schedulepostflow_list_draftspostflow_list_accountspostflow_create_static_accountpostflow_connect_accountpostflow_disconnect_accountpostflow_set_x_premiumpostflow_delete_accountpostflow_list_failedpostflow_create_postpostflow_cancel_postpostflow_schedule_postpostflow_edit_postpostflow_delete_postpostflow_validate_postpostflow_upload_mediapostflow_list_mediapostflow_delete_mediapostflow_requeue_failedpostflow_delete_failedpostflow_set_timezone
Thread payload support (same shape in API/MCP/CLI):
segments:[{ "text": "...", "media_ids": ["med_x"] }]- If
segmentsis present, step1is the root post and steps2..Nare follow-ups. - Publishing semantics: X follow-ups are chained as replies; other supported thread platforms publish follow-ups as comments on the root post.
- Backward compatibility is preserved for legacy
text+media_ids. postflow_edit_postaccepts optionalmedia_idsto replace media on editable posts ([]clears all media).- Editing without
intentand withoutscheduled_atpreserves the current scheduling state.
codex mcp add postflow --url http://localhost:8080/mcp~/.codex/config.toml example:
[mcp_servers.postflow]
url = "http://localhost:8080/mcp"
bearer_token_env_var = "POSTFLOW_API_TOKEN"Then:
export POSTFLOW_API_TOKEN="<same-value-as-API_TOKEN>"claude mcp add -t http postflow http://localhost:8080/mcp --header "Authorization: Bearer <API_TOKEN>"Tip: in the app UI (settings) you can copy ready-to-use MCP snippets for Claude and Codex.
brew tap antoniolg/tap
brew install antoniolg/tap/postflow
postflow --helpgo run ./cmd/postflow --helpConfigure CLI env:
export POSTFLOW_BASE_URL="http://localhost:8080"
export POSTFLOW_API_TOKEN="<API_TOKEN>"Common commands:
postflow health
postflow schedule list --from 2026-03-01T00:00:00Z --to 2026-03-31T23:59:59Z
postflow schedule list --view posts --from 2026-03-01T00:00:00Z --to 2026-03-31T23:59:59Z
postflow drafts list --limit 20
postflow posts validate --account-id acc_xxx --text "hello"
postflow posts validate --account-id acc_xxx --segments-json '[{"text":"root"},{"text":"reply 1"}]'
postflow posts create --account-id acc_xxx --segments-json '[{"text":"root"},{"text":"reply 1","media_ids":["med_x"]}]' --scheduled-at 2026-03-01T10:00:00Z
postflow posts schedule --id pst_xxx --scheduled-at 2026-03-01T10:00:00Z
postflow posts edit --id pst_xxx --text "copy updated" --intent schedule --scheduled-at 2026-03-01T10:30:00Z
postflow posts edit --id pst_xxx --segments-json '[{"text":"root updated"},{"text":"reply updated"}]'
postflow posts edit --id pst_xxx --text "copy + media" --replace-media --media-id med_a --media-id med_b
postflow posts delete --id pst_xxx
postflow posts cancel --id pst_xxx
postflow accounts list
postflow settings set-timezone --timezone Europe/Madrid
postflow media list --limit 20--text and --segments-json are mutually exclusive on posts create, posts validate, and posts edit.
schedule list returns grouped publications by default. Use --view posts to inspect the raw per-post/thread rows.
You can deploy from prebuilt image (no build on server):
ghcr.io/antoniolg/postflow:latest
or pinned:
ghcr.io/antoniolg/postflow:vX.Y.Z
Full production runbook:
401 unauthorized:- check
API_TOKEN - check
Authorization: Bearer ...in MCP/API clients
- check
- OAuth callback errors:
- verify
PUBLIC_BASE_URLmatches your real public domain - for X, verify
X_CLIENT_IDis set and the callback URL is registered in the X app settings
- verify
- Instagram media create errors (
code=9004,error_subcode=2207052):- verify
PUBLIC_BASE_URLis public/reachable from the internet (notlocalhostin production) - for image posts, upload JPEG or PNG (
.jpg/.jpeg/.png) - for video posts, use MP4 or MOV
- verify
- CLI auth errors:
- verify
POSTFLOW_API_TOKENmatches serverAPI_TOKEN
- verify
- API contract: docs/specs/openapi.yaml
- Deployment guide: docs/coolify-deploy.md