Social publishing
Purpose
Post the launch content on a schedule from one place, through each platform's official API, without losing a personal account to the brand or the brand to a personal account. The tool here is Postiz (open source, self-hosted); the platform steps apply to any scheduler.
Prerequisites
- Brand accounts already created: Instagram professional account, Threads profile (comes from Instagram), X account, optionally TikTok.
- A domain on Cloudflare (for the tunnel hostname), Docker Desktop, a machine that stays on at posting times (or a small VPS).
- The
console-automationsetup if consoles are driven from a browser. - A content calendar (see "Calendar" below).
Platform reality (check again before you start; these change)
| Platform | Official route | What blocks you |
|---|---|---|
| Meta app + Facebook Login, IG professional account linked to a Facebook Page | New Meta apps get the "Unified login" Instagram setup, which requires a business portfolio; the older Instagram-Login-only setup is "not available for newer apps". Stories publish as plain media: no poll, link or countdown stickers via API. | |
| Threads | Meta app with the Threads use case | Until App Review, only accounts with the Threads Tester role can connect; the tester accepts in Threads › Settings › Account › Website permissions › Invites. |
| X | Developer account with paid API access (check current X API pricing) | Needs a payment method; set a spend cap. |
| TikTok | Content Posting API | Unaudited apps post private only until the audit passes (the audit takes time). New accounts need phone verification, and one number can belong to only one account. |
| YouTube | Data API upload | Unverified API projects upload as private until a compliance audit. |
Plan around the blockers: start audits early, post TikTok/YouTube natively (TikTok Studio, YouTube Studio schedulers) until audits pass, or drop the platform.
Procedure
1. Self-host Postiz (Docker)
- Start from the official
postiz-docker-composefile. Beforeup:- Remove services you don't need and any that bind ports already in use on the machine (the upstream file binds the Temporal UI to
127.0.0.1:8080). - Drop Elasticsearch: set Temporal
ENABLE_ES=falseandSKIP_ADD_CUSTOM_SEARCH_ATTRIBUTES=true(SQL visibility). Postiz only registers two keyword attributes. If Temporal was first started with ES, its database still holds the default text attributes and Postiz fails with "cannot have more than 3 search attribute of type Text": recreate the Temporal database volume before anything is scheduled. - Bind Postiz to
127.0.0.1only; setMAIN_URL,FRONTEND_URL,NEXT_PUBLIC_BACKEND_URLto the public HTTPS hostname. - Put secrets (
JWT_SECRET, tunnel token, app keys) in a git-ignored.env, referenced as${VAR}in the compose file. - Add
mem_limit/cpusper service. Postiz runs frontend, backend and orchestrator in one container; we observed about 1.5 GB use, so a 2 GB limit avoided hitting the cap. On Windows also cap the WSL VM in%USERPROFILE%\.wslconfig(memory=4GB,processors=4,autoMemoryReclaim=gradual), thenwsl --shutdownand restart Docker Desktop.
- Remove services you don't need and any that bind ports already in use on the machine (the upstream file binds the Temporal UI to
- Create the first account in the browser after the page has hydrated (an unhydrated form submits as GET and puts the password in the URL). Then set
DISABLE_REGISTRATION=trueand restart. - Keep
restart: alwaysand enable "start Docker Desktop at sign-in"; disable sleep if the machine is the scheduler.
2. Public hostname (Cloudflare Tunnel)
- Create a remotely managed tunnel (API:
POST /accounts/{id}/cfd_tunnelwithconfig_src: cloudflare,PUT .../configurationswith the ingress rule, a proxiedCNAMEto<tunnel-id>.cfargotunnel.com,GET .../token). - Run
cloudflaredas a service in the same compose network and point the ingress athttp://postiz:5000, notlocalhost. - APPROVAL: a tunnel exposes a local service to the internet; ask before creating it. Optionally add Cloudflare Access in front (needs Zero Trust enabled on the account; keep
/uploadsreachable for Meta's media fetch).
3. Meta (Instagram + Threads)
- Create a separate Meta app for publishing (don't add use cases to an app that already powers an in-app feature such as Instagram Stories sharing). Use cases: "Manage messaging & content on Instagram" and "Access the Threads API".
- Instagram:
- Create a business portfolio when the use case asks for one (free; name, owner name, business email). Verification is only needed for other people's data and App Review.
- Create a Facebook Page for the brand (also required for Meta ads) and link the Instagram professional account to it: Page settings › Linked accounts › Instagram › Connect. The OAuth pop-up logs in with whatever Instagram account the browser holds: read the "Log in as …" name; if it is a personal account, switch accounts first (connecting a personal account converts it to professional).
- In the Instagram use case add exactly the scopes the scheduler requests (Postiz:
instagram_basic,pages_show_list,pages_read_engagement,business_management,instagram_content_publish,instagram_manage_comments,instagram_manage_insights). Set the Facebook Login for Business redirect URI to the scheduler's callback (Postiz:/integrations/social/instagram). - App ID + secret (App settings › Basic; showing the secret asks for the Facebook password) →
FACEBOOK_APP_ID/SECRET.
- Threads: add
threads_basic,threads_content_publish,threads_manage_replies,threads_manage_insights; set redirect (/integrations/social/threads) plus uninstall/delete callback URLs; take the separate Threads app ID/secret; add the brand profile as Threads Tester and accept the invite on the phone. - If the Threads consent URL lands on the Threads home page instead of the consent screen, the browser session lives on
threads.comwhile the scheduler usesthreads.net: open the same URL onthreads.com. - If the browser is currently acting as the Page (after editing it), the Facebook consent first shows "You need to switch to "; continue as the person, not the Page.
- During the Facebook consent, Meta defaults to "all current and future Pages/Businesses/Instagram accounts". On an account that manages other brands, choose "current only" and pick the brand's assets.
4. X
- Developer Console onboarding: account name, a precise use-case description (own account only, no data resale, no automated likes/follows/DMs), accept terms.
- Buy the smallest credit pack; leave auto-recharge off and set a billing cycle cap. The dashboard total can lag behind the Credits page.
- App › User authentication settings: Read and write, "Web App, Automated App or Bot", callback = scheduler callback (Postiz:
/integrations/social/x), website, terms and privacy URLs. Save shows the OAuth 2.0 client secret once. - Postiz uses OAuth 1.0a: regenerate the Consumer Key and copy key + "Consumer Key Secret" (that exact label) once →
X_API_KEY/X_API_SECRET. - Authorizing: tick "I trust this app", confirm the screen shows the brand handle, then Authorize.
5. TikTok (optional)
Account:
- New accounts require phone verification and a number can sit on one account only; a web sign-up may end on "continue in the app". Options: a second number for brand accounts, or converting an existing personal account (new username and display name, profile photo, bio; archive/privatize old personal videos).
- In some regions the one-tap "Switch to Business Account" is gone; the menu offers "Become a Verified Business Account", which asks for company documents. A personal account is enough: TikTok Studio scheduling, analytics, Spark Ads and the Content Posting API all work with it. The business music library is the main loss.
- Username can change once per 30 days, display name once per 7 days. Web profile (name, bio, photo, counts) can lag the app by a day or more; trust the app.
- Account region is set at sign-up from SIM/IP and could not be changed in settings.
Developer app (for the scheduler):
- developers.tiktok.com sign-up shows only "Enterprise developer" (email + code + password), but when creating the app you can pick Individual ownership, so no company is needed to start.
- Create the app, then a Sandbox: basic info (icon 1024², category, description ≤120 chars, terms/privacy URLs, Web platform URL), products Login Kit (Web redirect URI = scheduler callback; Postiz
/integrations/social/tiktok) and Content Posting API (turn on Direct Post), and all scopes the scheduler requests. Postiz asks foruser.info.basic,user.info.profile,user.info.stats,video.list,video.upload,video.publish; a missing one shows "Something went wrong … correct the following: scope" on the consent page. Click "Apply changes" after every edit. - Add the brand account as a Sandbox Target user (the login pop-up accepts a QR scan from the phone app). Use the sandbox client key/secret in the scheduler.
- Push-by-file needs no domain verification; pull-by-URL does.
- Before the audit:
UPLOADmode sends the video to the account's TikTok inbox as a draft (publish from the phone within 24 h; max 5 pending per 24 h).DIRECT_POSTworks but posts privately only. Submit the production app for review with a demo video for public direct posting. - The consent step can ask "Verify it's really you" with an email code. After many account changes in one day (email, name, bio, new logins) TikTok rate-limits these codes ("Maximum number of attempts reached"): stop and retry the next day instead of hammering it.
6. Verify
- Scheduler channel list shows each brand account (name/handle), none disabled.
- Post one private/low-stakes test per platform, or a draft, before the calendar starts.
- Resource usage after a day:
docker stats --no-stream.
Calendar
- Key the posting calendar to real launch events, not fixed dates: pre-launch (no download call to action, no price), then each store's launch, then both live. Store review dates move; re-anchor the calendar when they do.
- Check every claim in a post against the shipped build, and every call to action against what is actually live on each store.
- Stories with polls, links or countdowns cannot be published through the API; post them in the app.
Decision points needing human approval
Creating brand accounts, business portfolio, Facebook Page; exposing a local service through a tunnel; buying API credits and the spend cap; which personal accounts may never be connected; converting a personal account; deleting Docker volumes or data.
Common pitfalls
- Connecting the personal Instagram account because the OAuth pop-up reused the browser's session.
- Assuming the old Instagram-Login setup exists on a new Meta app.
- Adding publishing use cases to the Meta app that ships inside the mobile app.
- Elasticsearch left in the stack when Temporal SQL visibility is enough.
- The upstream compose file grabbing a port already used by another local service.
- X key saved from the wrong label ("Consumer Key Secret", not the dialog title).
- Calendar written as if the app were live on both stores on day 1.
- Doing all TikTok account changes and the developer connection on the same day, which trips the verification-code rate limit.