Run your social media on autopilot, on your own server.
A free, MIT-licensed, self-hosted social media scheduler and AI autopilot for X, LinkedIn, Reddit, Bluesky and webhooks.
It's one Node.js process with one SQLite file. No Redis, no Temporal, no paid tier.
Website Β· Quickstart Β· Docs / Wiki Β· API Β· Compare vs Postiz Β· FAQ
- Why Open Post?
- Features
- Quickstart
- Your first post in five minutes
- Connecting real channels
- Autopilot
- AI providers
- Configuration reference
- REST API
- How it works
- Deployment
- Security
- Open Post vs Postiz
- Roadmap
- FAQ
- Contributing
- License
Most social media schedulers fall into one of two camps. Hosted SaaS tools charge per channel or per seat and keep your content on their servers. The open-source alternatives are often full platforms that need PostgreSQL, Redis, a workflow engine and several containers before you can schedule a single post.
Open Post is for people who want just enough:
- Write once, publish everywhere. Write a post once, tweak it per platform if you like, and pick a time. Open Post publishes it to every channel you selected and logs exactly what happened on each one.
- Autopilot when you're busy. Give it a list of topics and a few daily time slots. It drafts posts, with an LLM or its offline template engine, and keeps your calendar filled.
- Own everything. Your posts live in a single SQLite file on your machine, and your API keys live in environment variables. There are no accounts to create and nothing phones home.
- Small enough to understand. The whole backend is a handful of files and has one runtime dependency (Express). You can read it in an afternoon and change anything.
It runs happily on a small VPS, a Raspberry Pi, a home server or your laptop. We measured about 70 MB RSS at idle on Node 20.
Status: v1.1 is a young project. Everything described in this README is implemented and covered by tests. Planned features are listed in the roadmap and labelled as such. Nothing in the app is mocked. See AUDIT.md for how the original prototype's mock data was removed.
- Universal composer with optional per-platform versions for X, LinkedIn, Reddit (title and body) and Bluesky
- Live character counters for X (280), Bluesky (300) and LinkedIn (3000). Posts over a limit are rejected, never silently cut
- X threads: put a line containing
---between parts and they're posted as a reply chain - Queue and calendar views: filter by status, edit, reschedule, publish now, or click a calendar day to schedule
- Delivery log per channel, with remote IDs and links to the live post
- Automatic retries with exponential backoff (1 β 2 β 4 minutes). Only the channels that failed are retried
- Clear post states:
draft,scheduled,publishing,published,partial,failed
| Network | How Open Post publishes | Auth |
|---|---|---|
| X (Twitter) | API v2 POST /2/tweets, threads as reply chains |
OAuth 1.0a user context (HMAC-SHA1 signed), or an OAuth 2.0 user token |
| UGC Posts API, for members or organization pages | OAuth 2.0 access token plus author URN | |
/api/submit self posts to any subreddit |
Script-app password grant, or an access token | |
| Bluesky | AT Protocol createRecord, with link and hashtag facets |
Handle plus App Password |
| Webhook | JSON POST, signed with HMAC-SHA256 |
Optional shared secret |
| Sandbox | Records a delivery without any network call | None |
You can run multiple accounts per network by giving each channel its own env prefix (for example X_BRAND2_API_KEY).
- Cadence slots (e.g.
09:00, 13:00, 17:30), planned 1β14 days ahead without double-booking - Topic queue with content pillars. Evergreen topics are recycled when the one-off ideas run out
- Five personas: Thought Leader, Indie Hacker / Builder in Public, Technical Architect, Contrarian Analyst, Viral Storyteller
- Platform-tailored output: one idea becomes an X post, a LinkedIn post, a Reddit title and body, and a Bluesky post
- Transparent: every generated post records which engine wrote it and, if a provider failed, why it fell back
- OpenAI (and any OpenAI-compatible endpoint), Anthropic, Google Gemini, and Ollama for fully local models
- Built-in heuristic engine that needs no API key and no network, and gives deterministic output
- Storage: built-in
node:sqliteon Node 22+,better-sqlite3, or an atomic JSON file. Detected automatically - Sandbox mode for safe end-to-end testing (
SANDBOX_MODE=true) - Optional API token that protects the API and dashboard
- Docker image runs as a non-root user with a healthcheck
cli.js tickfor cron or serverless-style runs- OpenAPI 3.1 spec, llms.txt and CITATION.cff for tools and agents
![]() |
![]() |
| Composer | Autopilot |
![]() |
![]() |
| Calendar | Channels: shows which env vars are missing, never the values |
git clone https://github.com/CodesbyFebin/Open-Post.git
cd Open-Post
cp .env.example .env
# for a first, risk-free run:
sed -i 's/^SANDBOX_MODE=false/SANDBOX_MODE=true/' .env
docker compose up -d --buildOpen http://localhost:3000.
git clone https://github.com/CodesbyFebin/Open-Post.git
cd Open-Post
npm install
cp .env.example .env
npm startcurl http://localhost:3000/api/health
# {"status":"ok","app":"open-post","version":"1.1.0","store":"node:sqlite","sandboxMode":true,...}The store field shows which storage engine was picked: node:sqlite, better-sqlite3 or json.
- Start in sandbox mode (
SANDBOX_MODE=true). The header showsSANDBOX MODEso you always know nothing is going out. - Add a channel. Open Channels, pick
sandbox(or any real type) and give it a name. - Compose. Open Compose, write something, tick the channel and pick a time a minute or two from now. Then click Schedule.
- Watch it go out. The scheduler checks every 30 seconds. In Queue, the post changes to
publishedand the delivery log showssandbox (not sent). - Go live. Add the credentials for a real network to
.env(see below), setSANDBOX_MODE=false, restart, and publish for real.
Prefer the terminal? Here's the same flow with curl:
B=http://localhost:3000
CH=$(curl -s -X POST $B/api/channels -H 'Content-Type: application/json' \
-d '{"type":"sandbox","name":"Try it"}' | node -pe 'JSON.parse(require("fs").readFileSync(0)).id')
curl -s -X POST $B/api/posts -H 'Content-Type: application/json' \
-d "{\"content\":\"Hello from Open Post\",\"channelIds\":[\"$CH\"]}" > /tmp/post.json
curl -s -X POST $B/api/posts/$(node -pe 'require("/tmp/post.json").id')/publish-nowOpen Post never stores secrets in its database. Each channel reads its credentials from environment variables named <PREFIX>_<KEY>. The dashboard shows which variables are still missing. It shows only the names, never the values.
| Type | Default prefix | Required variables |
|---|---|---|
x |
X |
X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_SECRET, or X_OAUTH2_USER_TOKEN |
linkedin |
LINKEDIN |
LINKEDIN_ACCESS_TOKEN, LINKEDIN_AUTHOR_URN |
reddit |
REDDIT |
REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USERNAME, REDDIT_PASSWORD or REDDIT_ACCESS_TOKEN, plus a subreddit on the channel |
bluesky |
BLUESKY |
BLUESKY_IDENTIFIER, BLUESKY_APP_PASSWORD (optional BLUESKY_SERVICE) |
webhook |
WEBHOOK |
URL on the channel or WEBHOOK_URL, optional WEBHOOK_SECRET |
sandbox |
β | none |
Quick notes for each network (the Channels wiki page has step-by-step instructions):
- X: your app needs Read and write permission. Regenerate the access token after changing permissions. An app-only Bearer token can't create posts. X's API tiers and pricing change, so check that your plan allows writes.
- LinkedIn: use a token with
w_member_social(personal) orw_organization_social(company page). The author isurn:li:person:<id>orurn:li:organization:<id>. Tokens usually expire after about 60 days. - Reddit: create a script app at reddit.com/prefs/apps, and set a descriptive
REDDIT_USER_AGENT. Each subreddit's rules still apply. - Bluesky: use an App Password, never your main password. Links and
#hashtagsbecome clickable automatically. - Webhooks: perfect for n8n, Make, Zapier or your own service. Each request carries
X-Open-Post-Signature: sha256=<hmac>, so you can verify it came from you.
Multiple accounts: create a second X channel with env prefix X_BRAND2 and set X_BRAND2_API_KEY, X_BRAND2_API_SECRET, X_BRAND2_ACCESS_TOKEN and X_BRAND2_ACCESS_SECRET.
Autopilot turns a list of ideas into a steady posting schedule.
- Add topics in the Autopilot tab, e.g. "What we learned migrating to SQLite" (pillar: Engineering). Tick evergreen for topics that are fine to revisit.
- Pick a persona to set the tone of voice.
- Choose target channels and cadence slots, e.g.
09:00, 18:00. Slots use the server's local time, so setTZ. - Enable it. Every five minutes Open Post looks
horizonDaysahead, finds empty slots, picks the least-used eligible topic, drafts a post and schedules it.
Autopilot posts are normal scheduled posts. You can review, edit or delete them before they're due. If you'd rather approve everything by hand, leave autopilot disabled and press Fill slots now or Generate one now when you want drafts.
When the queue is empty, autopilot simply stops. It never makes up topics.
AI_PROVIDER |
You need | Notes |
|---|---|---|
heuristic (default) |
nothing | Offline, deterministic templates. A good fallback and fine for testing |
openai |
OPENAI_API_KEY |
JSON mode, default model gpt-4o-mini. OPENAI_BASE_URL lets you use any compatible gateway |
anthropic |
ANTHROPIC_API_KEY |
Messages API, default claude-3-5-haiku-latest |
gemini |
GEMINI_API_KEY |
generateContent with JSON output, default gemini-1.5-flash |
ollama |
a running Ollama | Fully local, default llama3.1 (ollama pull llama3.1) |
Override the model with AI_MODEL. The prompt asks for a core post plus variants for X, LinkedIn, Reddit and Bluesky, and tells the model not to invent statistics, customers or testimonials. If a provider errors or returns invalid JSON, Open Post uses the heuristic engine and saves the reason in meta.fallbackReason. The UI shows it too, so silent failures are impossible.
Configuration lives in environment variables, usually a .env file. .env.example lists all of them with comments. The most important ones:
| Variable | Default | What it does |
|---|---|---|
PORT / HOST |
3000 / 0.0.0.0 |
Where the server listens |
DATA_DIR |
./data |
Where the database file lives |
STORE_DRIVER |
auto |
auto, sqlite or json |
TZ |
system | Time zone for cadence slots |
API_TOKEN |
β | Require Authorization: Bearer β¦ on the API |
SANDBOX_MODE |
false |
Send every channel to the sandbox |
SCHEDULER_INTERVAL_MS |
30000 |
How often due posts are checked |
SCHEDULER_DISABLED |
false |
Turn the in-process scheduler off (use cli.js tick) |
MAX_PUBLISH_ATTEMPTS |
3 |
Retries before failed / partial |
AI_PROVIDER / AI_MODEL |
heuristic |
Content engine |
CORS_ORIGIN |
β | Allow a separate front-end origin |
The dashboard uses the same API you do. The full spec is in openapi.yaml, and you can load it into Swagger UI, Postman, Insomnia or an AI agent.
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Status, version, storage engine, sandbox flag |
GET |
/api/config |
Channel types, personas, AI provider |
GET POST |
/api/channels |
List or create channels |
GET PUT DELETE |
/api/channels/:id |
Manage one channel |
GET POST |
/api/posts |
List (?status=&channelId=&from=&to=) or create posts |
GET PUT DELETE |
/api/posts/:id |
Manage one post |
POST |
/api/posts/:id/publish-now |
Publish immediately |
GET PUT |
/api/autopilot |
Read or update autopilot settings |
POST |
/api/autopilot/generate-now |
Generate and schedule one post |
POST |
/api/autopilot/fill |
Fill every empty cadence slot |
POST |
/api/generate |
Draft content without saving it |
GET |
/api/analytics |
Counts computed from your database |
Example: schedule a post that goes out as a two-part thread on X and a single post elsewhere:
curl -X POST http://localhost:3000/api/posts -H 'Content-Type: application/json' -d '{
"content": "We just shipped Open Post 1.1: real drivers, SQLite and autopilot.",
"platformOverrides": {
"x": { "content": "We just shipped Open Post 1.1 π\n---\nReal X, LinkedIn, Reddit and Bluesky drivers, SQLite storage and an autopilot that fills your calendar." },
"reddit": { "title": "Open Post 1.1: a tiny self-hosted social scheduler" }
},
"channelIds": ["ch_x...", "ch_bsky...", "ch_reddit..."],
"scheduledFor": "2026-11-01T09:00:00+05:30",
"tags": ["release"]
}'βββββββββββββββββ one Node.js process βββββββββββββββββ
β β
β Dashboard (public/) βββΊ REST API (src/app.js) β
β β β
β Service (src/service.js) β
β posts Β· channels Β· retries Β· autopilot β
β β β β β
β Store (src/store.js) Drivers LLM β
β SQLite / JSON file X Β· LinkedIn OpenAI Β· β
β Reddit Β· Anthropicβ
β Scheduler (setInterval) Bluesky Β· Gemini Β· β
β fill slots β dispatch due Webhook Β· Ollama Β· β
β Sandbox Heuristicβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- No queue server. A single timer ticks every 30 seconds. A guard stops two ticks from overlapping, and an in-flight lock stops a post from being published twice.
- No ORM. Records are JSON documents in a single SQLite table keyed by
(collection, id), so the same code runs on SQLite or a JSON file. - Drivers are plain functions of
(channel, post, { fetch, env }). The tests inject a fakefetchand check the exact HTTP request each network receives.
- Any VPS or home server: Docker Compose, or Node with systemd. See the Installation and Deployment wiki pages.
- Behind HTTPS: put Caddy or Nginx in front, set
HOST=127.0.0.1and setAPI_TOKEN. - Backups:
sqlite3 data/open-post.db ".backup backup.db", or copydata/open-post.json. - One instance per data directory. The scheduler is in-process.
- Hosts that sleep idle containers will pause the scheduler too. Use an always-on host, or trigger
node cli.js tickfrom cron.
- Secrets come only from environment variables. The API rejects
credentialsin channel payloads and never returns secret values. - Setting
API_TOKENprotects the API and dashboard with a constant-time token check. - Webhooks are HMAC-signed when
WEBHOOK_SECRETis set. - Open Post is single-tenant. Anyone with API access can post to every connected channel, so keep it private or token-protected.
- Report vulnerabilities privately. See SECURITY.md.
Postiz is a mature, full-featured open-source scheduler, and if you need what it offers, it's a great choice. Open Post makes different trade-offs:
| Open Post | Postiz | |
|---|---|---|
| License | MIT | AGPL-3.0 |
| Services to run | 1 Node process plus a SQLite file | App, PostgreSQL 14+, Redis 6+, Temporal (required since v2.12.0) |
| Hardware | ~70 MB RSS measured at idle | 2 GB RAM floor, 8 GB recommended (per Postiz docs) |
| Networks | X, LinkedIn, Reddit, Bluesky, webhooks | Many more, including Instagram, Facebook, TikTok, YouTube, Threads and Mastodon |
| Teams and multiple users | No (single-tenant) | Yes |
| Media uploads | Not yet | Yes |
| Hosted option | No | Yes (Postiz Cloud) |
| AI | OpenAI, Anthropic, Gemini, Ollama, offline heuristic | Built-in AI features |
The Postiz column comes from its self-hosting documentation (System Requirements and Configuration Reference), checked in October 2026. If something has changed, please open an issue.
Choose Open Post if you post to a few networks as one person or a small team, want tiny resource usage, or want code you can read end to end. Choose Postiz if you need many networks, media, team workflows or a hosted service.
These are planned and not implemented yet:
- Image and video uploads (X, LinkedIn, Bluesky blobs)
- OAuth "Connect" buttons as an alternative to env vars
- Mastodon and Threads drivers
- Engagement metrics pulled back from each network
- Per-slot channel targeting and per-day cadences
- Approval queue for autopilot posts
- Prebuilt container image on GHCR
Want one of these? Comment on or open an issue, or send a PR.
Is it really free?
Yes. It's MIT licensed with no tiers, seats, telemetry or license keys. You pay only for your own hosting, and for any paid platform APIs or LLMs you choose to use.
Why env vars instead of a "Connect account" button?
A connect button needs a registered OAuth app per network and a public callback URL on your server. Env vars work everywhere, keep tokens out of the database, and are easy to rotate. Built-in OAuth flows are on the roadmap.
The channel says "missing env vars". What now?
The card lists the exact variable names. Add them to .env and restart the server. The check shows only names, never values.
My X post fails with 401/403.
Give your X app Read and write permission, regenerate the access token and secret afterwards, and use OAuth 1.0a user keys or an OAuth 2.0 user token. App-only Bearer tokens can't create posts. Also check that your API plan allows writes.
How do I test without posting anything?
Set SANDBOX_MODE=true, or create a sandbox channel. Sandbox deliveries are labelled clearly and never include fake links.
Can I run it fully offline or with a local model?
Yes. The heuristic engine needs no network, and AI_PROVIDER=ollama uses a local model. Publishing itself obviously needs to reach the social networks.
Which time zone do cadence slots use?
The server's local time. Set TZ, for example TZ=Asia/Kolkata or TZ=America/New_York.
What happens if a network is down?
The failed channels are retried after 1, 2 and 4 minutes (configurable with MAX_PUBLISH_ATTEMPTS). Channels that already succeeded aren't posted again. After the last attempt, the post is marked partial or failed, and the error is shown in the queue.
Can I run several instances for redundancy?
Not against the same data directory. The scheduler lock is in-process. Run one instance under a supervisor that restarts it.
Does it post images?
Not yet. It posts text only. Media support is the top roadmap item.
Where is my data stored?
In DATA_DIR (default ./data), as open-post.db (SQLite) or open-post.json.
Is the original single-file HTML app still available?
It was replaced by the API-backed dashboard in public/, because it kept everything in localStorage and never actually published. It's still in the git history (commit 6dceaef). See AUDIT.md.
Contributions of all sizes are welcome: bug reports, docs fixes, new drivers and UI polish.
npm install
npm test # node --test
npm run lint # eslint
npm run dev # auto-restart on changesPlease read CONTRIBUTING.md and follow the Code of Conduct. Good first contributions: a Mastodon driver, media uploads for Bluesky, translations for the dashboard, and more tests.
If Open Post saves you time, starring the repo helps other people find it.
MIT. Free for personal and commercial use.



