Posthive Documentation

Posthive is an open-source social media scheduling SaaS. Write once, publish to Bluesky, Threads, Instagram, LinkedIn, Mastodon, and YouTube — all from a single clean interface. Self-hostable under AGPL-3.0.

Quick start

Get Posthive running locally in under five minutes.

1. Clone the repository

git clone https://github.com/posthive/posthive.git cd posthive

2. Copy environment files

cp apps/api/.env.example apps/api/.env # Edit apps/api/.env and fill in the required values

3. Run database migrations

cd apps/api pnpm db:migrate

4. Start the dev server

# From the repo root pnpm dev

This starts both the API on http://localhost:3001 and the web app on http://localhost:3000 in parallel.

App running in browser — compose page

Installation

Prerequisites

  • Node.js ≥ 20
  • pnpm ≥ 9 (npm i -g pnpm)
  • Redis — Upstash free tier or Railway Redis (required for BullMQ job queue)

Clone and install

git clone https://github.com/posthive/posthive.git cd posthive pnpm install

The monorepo uses pnpm workspaces. Running pnpm install at the root installs dependencies for both apps/api and apps/web.

Environment variables

All configuration lives in apps/api/.env. Copy .env.example and fill in the values below.

VariableRequiredDescription
DATABASE_URLYesSQLite path (file:./dev.db) for dev, or a Postgres connection string for prod.
ENCRYPTION_KEYYes64-char hex string. Used for AES-256-GCM encryption of stored credentials. Never change after data is written.
REDIS_URLYesRedis connection string (e.g. rediss://...) for BullMQ job queue.
JWT_ACCESS_SECRETYes64-char hex. Signs short-lived access tokens.
JWT_REFRESH_SECRETYes64-char hex. Signs long-lived refresh tokens.
WEB_URLYesURL of the web app. Use http://localhost:3000 in dev so auth cookies stay stable across tunnel restarts.
PUBLIC_API_URLMeta onlyPublic HTTPS URL of the API. Meta fetches images from this URL, so it must be reachable from the internet.
ENABLE_BILLINGNoSet to true to enable Dodo Payments billing. Defaults to off for self-hosters.
AUTH_PROVIDERNolocal (default) or supabase. Switches the auth backend.
Warning: ENCRYPTION_KEY must never be changed after connected accounts have been saved. Changing it makes all stored credentials permanently unreadable.

Bluesky

Bluesky uses app passwords — no OAuth flow required. Connection is straightforward and does not need a public callback URL.

How to connect

  1. Go to bsky.app → Settings → Privacy and Security → App Passwords.
  2. Create a new app password and copy it.
  3. Open the Accounts page in Posthive, click Connect Bluesky.
  4. Enter your Bluesky handle (e.g. yourname.bsky.social) and the app password.

Posthive stores the app password encrypted with AES-256-GCM. Your main account password is never used or stored.

Bluesky connect dialog

Threads

Threads uses Meta OAuth 2.0. You need a Meta Developer app with the Threads use case enabled.

Setup

  1. Go to developers.facebook.com and create an app.
  2. Add the Threads API use case to your app.
  3. Add a redirect URI matching THREADS_REDIRECT_URI in your env — must be a public HTTPS URL.
  4. Copy your App ID and App Secret into THREADS_APP_ID and THREADS_APP_SECRET.
  5. Click Connect Threads on the Accounts page and complete the OAuth flow.

Threads tokens expire every 60 days. Posthive automatically refreshes them before each scheduled post.

Threads OAuth authorization screen

Instagram

Instagram publishing requires a Professional (Business or Creator) account linked to a Facebook Page, and a Meta Developer app with the Instagram product enabled.

Important: PUBLIC_API_URL must be a public HTTPS URL. Meta fetches your uploaded images directly from the API server when creating carousel containers — a localhost URL will not work.

Supported media types

  • Post — single image or carousel (up to 10 images on Pro/Team)
  • Reel — short video (Pro/Team plans only)
  • Story — image or video story (Pro/Team plans only)

Setup

  1. Create a Meta Developer app and add the Instagram product.
  2. Set your redirect URI to INSTAGRAM_REDIRECT_URI (must be public HTTPS).
  3. Fill in INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET, and PUBLIC_API_URL.
  4. Click Connect Instagram on the Accounts page.
Instagram connect dialog and media type selector

LinkedIn

LinkedIn uses OAuth 2.0 via the LinkedIn Developer platform.

Note: Image and video uploads require elevated API access (Marketing Developer Platform approval). Without it, Posthive will publish text-only posts to LinkedIn.

Setup

  1. Create an app at developer.linkedin.com.
  2. Add the Share on LinkedIn and Sign In with LinkedIn using OpenID Connect products.
  3. Add your callback URL under AuthAuthorized redirect URLs.
  4. Set LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET in your env.
  5. Click Connect LinkedIn on the Accounts page.

Mastodon

Posthive works with any Mastodon instance. You register an application within your own instance and paste the credentials into Posthive.

How to connect

  1. Log in to your Mastodon instance (e.g. mastodon.social).
  2. Go to Settings → Development → New application.
  3. Give it a name, set the redirect URI to your Posthive callback URL, and enable the write:statuses and write:media scopes.
  4. Copy the Client key and Client secret.
  5. Click Connect Mastodon in Posthive, enter your instance URL and the credentials.
Mastodon app settings — Development tab

YouTube

Posthive publishes to YouTube as Shorts (or regular videos — your choice per post) using Google OAuth 2.0 and the YouTube Data API v3. Every post requires a video attached.

How to connect

  1. Create a project at console.cloud.google.com and enable the YouTube Data API v3.
  2. Configure the OAuth consent screen — add the youtube.upload, youtube.readonly, and youtube.force-ssl scopes, and add your own Google account under Audience → Test users while the app is unverified.
  3. Create an OAuth client (Web application) and copy the Client ID and Secret into YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET.
  4. Click Connect YouTube on the Accounts page and authorize.
Important: Google requires OAuth redirect domains to be owned and verified — shared tunnel domains (devtunnels.ms, ngrok, etc.) are rejected outright with Error 403: access_denied. Use http://localhost:<API_PORT>/auth/youtube/callback for YOUTUBE_REDIRECT_URI instead — Google exempts localhost from domain verification. This means connecting YouTube only works from a browser on the same machine as your API server (everything else in Posthive works fine over a tunnel).

Shorts vs. regular video

In Compose, the YouTube section has a Short / Video toggle. YouTube classifies Shorts by the video file itself — vertical (9:16) and 60 seconds or under is the reliable threshold. Posthive auto-appends #Shorts to the description when "Short" is selected, but that tag alone does nothing if the video doesn't already qualify by aspect ratio and duration. Posthive checks the attached video's dimensions and warns you in the UI if it won't actually classify as a Short.

Title and description are separate dedicated fields (not the shared Post box other platforms use) — title is capped at 100 characters, description at 5,000. Selecting only YouTube accounts hides the shared Post box entirely since it isn't used.

While the Google app stays in "Testing" publishing status, refresh tokens expire after 7 days regardless of activity — scheduled YouTube posts will start failing until you manually reconnect. Submit the app for Google verification to remove this limit and the 100-test-user cap.
Compose page — YouTube Title/Description fields and Short/Video toggle

Scheduling posts

The Compose page is where you write and schedule posts. Everything happens in a single panel — no multi-step wizard.

Steps

  1. Select one or more connected accounts from the account picker at the top.
  2. Write your post in the text area. The character counter updates per-platform.
  3. Optionally attach images or video using the media button.
  4. Pick a scheduled time using the date-time picker. You can also post immediately.
  5. Click Schedule. The post is enqueued and will fire at the chosen time.
Compose page — account picker, editor, and schedule button

Calendar view

The Posts page (/jobs) has both a list view and a calendar view. Toggle between them with the view switcher in the top-right corner.

The calendar supports month, week, and day modes. Pending posts can be dragged to a new time slot to reschedule them without opening the edit dialog.

Calendar view showing scheduled posts across the month

First comment

The first comment field lets you attach a reply that is posted immediately after the main post goes live. This is commonly used to add hashtags without cluttering the main post body, or to add a thread continuation.

The comment is published per-platform — each connected account gets its own first comment published to that platform.

First comment field below the main editor

Per-platform overrides

Available on Pro and Team plans. Per-platform overrides let you customise the post text for individual accounts without creating separate posts.

Click the Customize button next to any selected account in the Compose page. A dialog opens where you can edit the content independently for that account. Accounts without an override use the main post body.

Per-platform customize dialog with independent text editor

Media uploads

Posthive supports image and video attachments. Images can be attached by clicking the media button, dragging files into the compose area, or pasting from the clipboard.

  • Creator plan: up to 4 images per post (carousel).
  • Pro / Team plans: up to 10 images per carousel.
  • Alt text is supported — click any thumbnail to add descriptive text for accessibility.
  • In development, files are stored on local disk. In production, set STORAGE_PROVIDER=supabase and configure your bucket.

Docker setup

Posthive can be self-hosted on any platform that runs Docker containers — Railway, Render, Fly.io, or your own VPS.

Recommended stack

  • Deploy the API container and point DATABASE_URL to a managed Postgres instance.
  • Set REDIS_URL to an Upstash Redis or Railway Redis URL.
  • Update the Prisma provider to postgresql in apps/api/prisma/schema.prisma.
  • Run pnpm db:migrate as part of your release command.
# Example Railway / Dockerfile release command pnpm db:migrate && node dist/index.js

Database

Posthive uses Prisma 5 with SQLite in development and Postgres in production. Switching is a one-line change in the schema.

// apps/api/prisma/schema.prisma datasource db { provider = "postgresql" // change from "sqlite" url = env("DATABASE_URL") }

Run migrations after any schema change:

cd apps/api pnpm db:migrate
Warning: ENCRYPTION_KEY must never change after accounts are saved. All stored OAuth tokens and app passwords are encrypted with this key — changing it renders all connected accounts permanently unusable.

Redis

Redis is used exclusively for the BullMQ job queue that powers scheduled post delivery. No application state is stored in Redis — it is safe to flush between deploys as long as no posts are currently queued.

Options

  • Upstash — free tier is sufficient for most self-hosters. Use the rediss:// TLS URL.
  • Railway Redis — add the Redis plugin to your Railway project and copy the connection string.
  • Self-hosted — any Redis 6+ instance works.
REDIS_URL="rediss://default:<password>@<host>:<port>"

Storage

Uploaded media can be stored locally (dev) or in Supabase Storage (prod). Switch with the STORAGE_PROVIDER env variable.

Local storage (default)

STORAGE_PROVIDER=local

Files are written to apps/api/uploads/. Not recommended for production — files are lost on redeploy.

Supabase Storage

  1. Create a Supabase project.
  2. In Storage, create a public bucket named media.
  3. Copy your project URL and service role key.
STORAGE_PROVIDER=supabase SUPABASE_URL="https://your-project.supabase.co" SUPABASE_SERVICE_KEY="eyJ..."

Plans & pricing

Posthive has four tiers. The trial is available immediately after sign-up with no card required.

PlanAccountsPosts / monthReels & StoriesOverridesImages / post
Trialing330NoNo4
Creator5400NoNo4
Pro15UnlimitedYesYes10
Team50UnlimitedYesYes10

Billing is handled through Dodo Payments. Set ENABLE_BILLING=true and configure your Dodo API key and product IDs to activate billing.

Webhooks

Posthive listens for Dodo Payments webhook events to update subscription status in real time. Configure the webhook endpoint in your Dodo dashboard.

Webhook URL

POST https://your-api-url/billing/webhook

Webhook secret

Important: Dodo Payments webhook secrets are prefixed with whsec_. Strip this prefix before setting DODO_WEBHOOK_SECRET — the verification code base64-decodes the raw secret and will fail if the prefix is included.
# Dodo dashboard shows: whsec_abc123... # Set in .env: DODO_WEBHOOK_SECRET="abc123..."

Handled events

EventEffect
payment.succeededSets the user's plan to active for the purchased tier.
subscription.cancelledMarks the subscription as cancelled. Access continues until the end of the billing period.