An open-source, self-hostable alternative to Buffer, Typefully & Hootsuite.
Write once, publish everywhere. Schedule posts to X, Bluesky, LinkedIn, Facebook, Instagram, Threads, and Discord from one calendar β on your own server, with your own data.
This repository is the Apache-2.0-licensed iot.EraX downstream distribution of coollabsio/shoutrrr. It preserves upstream attribution while providing the minimal, vulnerability-gated runtime used by
iot.EraX Shout.
Shoutrrr is a social media scheduling tool you run yourself. Connect your accounts, draft a post once, and send it to every network at the same time β or queue it to go out on a recurring schedule. No monthly seat fees, no third party holding your tokens or your data.
It's built for individuals and teams: invite collaborators into a shared workspace, keep clients or brands separated, and see how your posts perform β all from a single, fast interface.
- You own everything β your posts, your audience tokens, your analytics. Self-hosted on your infrastructure.
- One post, every platform β compose once and publish to multiple accounts, tweaking the text per network when you want.
- Plan ahead β a posting queue with recurring time slots and a month calendar, so your feed stays consistent without you babysitting it.
- Made for teams β workspaces, roles, and email invites keep clients and collaborators tidy.
- No vendor lock-in β open source under the Apache 2.0 license, runs anywhere Docker does.
| Platform | Connect with | Publishing | Threads | Analytics |
|---|---|---|---|---|
| X (Twitter) | OAuth 2.0 | β (Free: β€280 chars / 2m20 video; Premium tiers: β€25,000 chars / up to 4h video, up to 4 media) | β | likes, reposts, replies, impressions |
| Bluesky | ATProto OAuth or app passwords | β (β€300 graphemes, up to 4 images or 1 video) | β | likes, reposts, replies |
| OAuth 2.0 (OIDC) | β (β€3000 chars, up to 9 images or 1 video) | β (single post) | not available for personal accounts | |
| Facebook (Pages) | OAuth 2.0 (Facebook Login) | β (β€63,206 chars, up to 10 images or 1 video) | β (single post) | likes, comments, shares, impressions |
| OAuth 2.0 (Facebook Login) | β (media required, β€2,200 chars, up to 10 images or 1 video/Reel) | β (single post) | likes, comments, shares, views | |
| Threads | OAuth 2.0 | β (β€500 chars, up to 10 images or 1 video) | β | likes, replies, reposts, views |
| Discord | Channel webhook URL | β (β€2000 chars, up to 10 files β€10 MiB) | β | reactions |
- π Composer β draft with media and alt text, see live per-account text and video limits (including detected X subscription limits), and automatically split long posts into threads where the platform supports it.
- π Multi-account publishing β fan one post out to many accounts at once, with optional per-platform overrides. Each target publishes independently and retries on failure.
- ποΈ Queue & calendar β set recurring posting slots (in your workspace's timezone), drop drafts into the queue, and review everything on a month calendar. Publish instantly whenever you like.
- π Analytics β follower and post-count trends per account, plus per-post engagement (likes, reposts, replies, impressions) where the provider API supports it.
- π Connected accounts β link accounts via OAuth (X, LinkedIn) or app password (Bluesky), group them into reusable sets, and get nudged when one needs reconnecting. Tokens are stored encrypted and refreshed automatically; a successful authenticated X tier refresh also restores a stale attention state.
- π₯ Workspaces & team β multiple workspaces with role-based memberships, email invitations, and ownership transfer. Every bit of data is scoped to its workspace.
- π Notifications β in-app alerts when a post publishes or fails, or when an account needs attention.
- π Secure by default β email/password with verification, two-factor (TOTP), passkeys (WebAuthn), and optional social login (Google, X, LinkedIn).
The hardened iot.EraX production image is private in Google Artifact Registry;
public image distribution is deferred. It contains PHP 8.5, FrankenPHP,
ffmpeg, production Composer dependencies, and compiled browser assets. Bun,
Node.js, Yarn, npm, Composer, Git, SSR assets, development dependencies, and
node_modules are absent from the final runtime.
Each release is built for linux/amd64 and linux/arm64, scanned for operating
system and language vulnerabilities before and after publication, signed with
keyless Cosign, and accompanied by provenance and CycloneDX SBOM attestations.
No mutable latest tag is published.
Self-hosters choose an explicit immutable image reference. It may be an image built locally from this checkout or a digest in a registry they control. The Compose contract does not assume Docker Hub, GHCR, or access to the private iot.EraX registry.
Build the runtime locally, for example:
docker build --tag shoutrrr-local:1.4.4-ioterax.1 .The bundled self-hosting stack uses a digest-pinned PostgreSQL 18 base and runs
migration, web, queue, and scheduler as separate containers. Its small local
database companion removes the unused vulnerable gosu binary and is scanned
by the same container gate. Copy the example environment, set the explicit
image reference and local pull policy, generate an application key, and set a
strong database password:
git clone https://github.com/ioterax/shoutrrr.git
cd shoutrrr
cp .env.example.prod .env
# Set SHOUTRRR_IMAGE=shoutrrr-local:1.4.4-ioterax.1,
# SHOUTRRR_PULL_POLICY=never, and POSTGRES_PASSWORD in .env first.
docker run --rm shoutrrr-local:1.4.4-ioterax.1 php artisan key:generate --show
# Copy the generated key into APP_KEY in .env.
docker compose -f docker-compose.production.yaml up -d --buildOpen http://localhost:8080 and create the first account. For a public HTTPS
deployment, set the final APP_URL, OCTANE_HTTPS=true,
SESSION_SECURE_COOKIE=true, and a precise trusted-proxy range. Use * only
when the application cannot be reached except through that proxy.
The application image itself does not run migrations or background processes implicitly. Compose expresses their lifecycle explicitly, making the same image suitable for orchestrators that separate web services from batch Jobs.
docker-compose.production.yamlis a public self-hosting reference. The official iot.EraX deployment does not use its PostgreSQL container: Cloud Run connects privately to the existing Hub/Foundation PostgreSQL 16 instance and runs migrations, scheduler dispatch, and queue draining as terminating Cloud Run Jobs. See the production delivery contract.
docker-compose.development.yaml builds exactly the minimal production runtime
from the current checkout. It includes an isolated PostgreSQL container and can
optionally load the upstream demo account:
cp .env.example.prod .env
# Set APP_KEY and POSTGRES_PASSWORD in .env.
docker compose -f docker-compose.development.yaml --profile demo up -d --buildThe demo credentials are test@example.com / password. Omit
--profile demo when you do not want seeded data.
The source-development workflow remains Bun-based, as defined by upstream. Bun is a build dependency only and does not enter either runtime image.
The image accepts videos up to Shoutrrr's 1 GiB application ceiling by default. For large or public deployments, configure appropriate proxy limits and prefer object storage so uploads do not traverse the application process.
Outside local, Shoutrrr sends a strict, nonce-based Content-Security-Policy along with X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and (in production) Strict-Transport-Security. This is deliberate hardening β but if you customise the frontend or front the app with an unusual proxy/CDN, it's the first place to look when something renders wrong.
If the UI loads unstyled or a feature is broken, open your browser's dev console and check for CSP violations. Common causes and fixes (all in app/Http/Middleware/SecurityHeaders.php):
- Assets served from a different origin than
APP_URL(e.g. a CDN host) are blocked bydefault-src 'self'. Serve built assets from the app origin, or add the host toscript-src/style-src/img-src. - Third-party embeds or analytics scripts are blocked β
script-srconly trusts the app's own nonced scripts ('strict-dynamic'). Add the source explicitly if you need it. - Images/avatars from arbitrary hosts are allowed (
img-srcpermitshttps:); tighten this if you prefer.
The CSP is intentionally not sent in local (APP_ENV=local) because it is incompatible with the Vite dev server's hot-reload. To verify the production policy locally, run a build and serve with a non-local env (bun run build && APP_ENV=production php artisan serve). Note that Strict-Transport-Security requires the app to be served over HTTPS.
Bluesky connects two ways, and neither needs you to register a developer app:
- OAuth (recommended) β users sign in on Bluesky and authorize Shoutrrr without handing over a password. It's zero-config: Shoutrrr publishes an ATProto OAuth client-metadata document at
${APP_URL}/oauth/bluesky/client-metadata.json(with keys at${APP_URL}/oauth/bluesky/jwks.json), and Bluesky's authorization server fetches those to identify your instance. The signing key is generated once and stored encrypted β there's nothing to add to.env. The one requirement:APP_URLmust be a public HTTPS URL, because Bluesky has to reach those two documents over the internet. (Inlocaldev, Shoutrrr falls back to a loopback client so OAuth still works onlocalhost.) - App password β users paste a Bluesky app password. No setup, and it works anywhere β including private or LAN deployments Bluesky can't reach for OAuth.
X, LinkedIn, and the Meta platforms (Facebook, Instagram, Threads) publish through your own developer app, so you'll register one with each provider and add the credentials to .env. The redirect URIs must match what you register (they default to ${APP_URL}/...):
# X β https://developer.x.com
X_CLIENT_ID=
X_CLIENT_SECRET=
X_REDIRECT_URI="${APP_URL}/accounts/callback/x"
# LinkedIn β https://www.linkedin.com/developers
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
LINKEDIN_REDIRECT_URI="${APP_URL}/accounts/callback/linkedin"
# Facebook + Instagram β one Meta app (https://developers.facebook.com).
# Instagram accounts are discovered via their linked Facebook Pages, so both
# platforms share these credentials. Publishing to non-test accounts requires
# Meta App Review + Business Verification.
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
FACEBOOK_REDIRECT_URI="${APP_URL}/accounts/callback/meta"
# Threads β a separate Meta app (https://developers.facebook.com).
THREADS_CLIENT_ID=
THREADS_CLIENT_SECRET=
THREADS_REDIRECT_URI="${APP_URL}/accounts/callback/threads"Optionally, let people sign in with a social account instead of a password:
SOCIALITE_ENABLED=true
SOCIALITE_PROVIDERS=google # comma-separated: google,x,linkedin
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI="${APP_URL}/auth/google/callback"Heads up: publishing, scheduling, engagement polling, and analytics capture rely on a running queue worker and scheduler. The provided Docker setups start both for you. Metrics and engagement are enabled by default and can be disabled with
METRICS_ENABLED=false/ENGAGEMENT_ENABLED=false(seeconfig/metrics.phpandconfig/engagement.php). X accounts read the authenticated account's subscription type when connected or when Refresh tier is used in Connected Accounts; Free accounts use a 280-character / 140-second-video limit, while Basic, Premium, and Premium+ accounts use 25,000 characters and can upload videos up to four hours. When a post targets more than one X account, the composer enforces the strictest selected account limit.
Shoutrrr is a Laravel 13 (PHP 8.5) app with a React 19 + TypeScript frontend on Inertia v3, Tailwind v4, and shadcn/ui. It runs on Laravel Octane (FrankenPHP), with typed routes generated by Wayfinder.
composer setup # install deps, copy .env, generate app + Passport keys, bun install, build assets
composer dev # serve + queue + scheduler + logs + Vite, all at onceUses Bun for the frontend (
bun install,bun run β¦) β not npm/pnpm.
To test SMTP locally during development, run Mailpit:
docker run --rm --name mail --pull always -p 1025:1025 -p 8025:8025 axllent/mailpit:latestThen set these mail values in your .env file:
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_SCHEME=null
MAIL_FROM_ADDRESS="hello@shoutrrr.local"
MAIL_FROM_NAME="${APP_NAME}"A post is composed once, then split into one target per connected account. The scheduler dispatches due posts every minute; a queued PublishPostTarget job then publishes each target independently, with retries, idempotency, and a per-attempt audit trail. Scheduled jobs refresh OAuth tokens before they expire, fetch replies, and check for due metrics captures every 15 minutes. Metrics refresh cadence is controlled in config/metrics.php.
The REST API and MCP integration authenticate with bearer tokens minted by Laravel Passport, which signs and verifies every token with an RSA keypair. You don't need to provision these keys β the first time a workspace issues an API key, Shoutrrr generates the pair automatically (ApiKeyManager::ensureEncryptionKeysExist() runs passport:keys) and stores it in storage/oauth-private.key / oauth-public.key. The bundled Docker setups persist storage on a named volume, so the keys survive redeploys.
If you'd rather provision them explicitly β for example to share one keypair across multiple app instances behind a load balancer β do either of the following before issuing keys:
- Run
php artisan passport:keysonce and keepstoragepersistent, or - Set the
PASSPORT_PRIVATE_KEYandPASSPORT_PUBLIC_KEYenv vars to the key contents (the auto-generation step is skipped when both are present).
To turn auto-generation off entirely, set PASSPORT_AUTO_GENERATE_KEYS=false. Issuing an API key without keys present then fails loudly instead of writing new ones β useful when the keypair is managed externally and Shoutrrr must never mint its own.
| Concern | Tool | Command |
|---|---|---|
| Tests | Pest | composer test |
| PHP style | Pint | composer lint |
| PHP static analysis | Larastan (level 7) | composer types:check |
| PHP refactoring | Rector | composer refactor:check / composer refactor |
| JS lint / format | oxlint / oxfmt | bun run lint:check / bun run format:check |
Run the full local gate (lint, format, type-check, refactor check, Pest suite) with composer ci:check.
![]() Andras Bacsai |
![]() Aditya Tripathi |
See all the people who have contributed in the contributors list.
Open-source software licensed under the Apache 2.0 license.

