Open-source social media management for creators, agencies, and SMBs.
BrightBean Studio is an open-source, self-hostable social media management platform built for creators, agencies and SMBs. It does what Sendible, SocialPilot, or ContentStudio do, but free and without per-seat, per-channel, or per-workspace limits. Plan, compose, schedule, approve, publish, and monitor content across Facebook, Instagram, LinkedIn, TikTok, YouTube, Pinterest, Threads, Bluesky, Google Business Profile, and Mastodon from a single multi-workspace dashboard.
It's for people managing many client accounts under one roof who'd rather own their social stack than pay $100–300/month to a SaaS vendor. Every feature is available to every user. No paid tier, no feature gate, no upsell.
You can deploy it with a one-click button on Heroku, Render, or Railway, run it on your own VPS via Docker, or run it locally. All platform integrations talk directly to the official first-party APIs using your own developer credentials, so there's no aggregator middleman, no vendor lock-in, and no third party sitting between you and your data.
| Multi-workspace & teams | Unlimited orgs → workspaces → members. Granular RBAC with custom roles, invitations, and a separate Client role for external collaborators. |
| Content composer | Rich editor with per-platform caption/media overrides, version history, reusable templates, content categories & tags, a Kanban idea board. |
| Calendar & scheduling | Visual calendar with recurring weekly posting slots per account and named queues that auto-assign posts to the next available slot. |
| Publishing engine | Direct first-party API integrations (no aggregator), automatic retries, per-account rate-limit tracking, and a 90-day publish audit log. |
| Approval workflows | Configurable stages (none / optional / internal / internal + client), threaded internal & external comments, reminders, and a full audit trail. |
| Unified social inbox | Comments, mentions, DMs, and reviews from every connected platform in one place, with sentiment analysis, assignments, threaded replies, and historical backfill. |
| Media library | Org- and workspace-scoped libraries with nested folders, auto-generated platform-optimized variants, and alt text. |
| Client portal | Passwordless 30-day magic-link access so clients can approve or reject posts without creating an account. |
| Notifications | In-app, email, and webhook delivery with per-user preferences for every event type. |
| Security & ops | Encrypted token & credential storage, optional 2FA (TOTP), Google/GitHub SSO, Sentry support, and a 7-day reversible org-deletion grace period. |
| White-label friendly | Per-workspace branding (logo, colors) and workspace defaults for hashtags, first comments, and posting templates. |
After deploying, set these environment variables in your platform's dashboard:
| Variable | Required | Description |
|---|---|---|
SECRET_KEY |
Auto-generated | Django secret key. Set automatically by the deploy button. |
ENCRYPTION_KEY_SALT |
Auto-generated | Encryption salt. Set automatically by the deploy button. |
DATABASE_URL |
Auto-provisioned | PostgreSQL connection string. Set automatically. |
ALLOWED_HOSTS |
Yes | Your app's domain, e.g. your-app.herokuapp.com |
APP_URL |
Yes | Full public URL, e.g. https://your-app.herokuapp.com |
STORAGE_BACKEND |
No | Set to s3 for S3/R2 storage. Default: local. Heroku, Render, and Railway have ephemeral filesystems, so uploaded files are lost on redeploy without S3. |
S3_ENDPOINT_URL |
If using S3 | S3-compatible endpoint URL |
S3_ACCESS_KEY_ID |
If using S3 | S3 access key |
S3_SECRET_ACCESS_KEY |
If using S3 | S3 secret key |
S3_BUCKET_NAME |
If using S3 | S3 bucket name |
EMAIL_HOST |
No | SMTP server for sending invitations and password resets |
EMAIL_PORT |
No | SMTP port (default: 587) |
EMAIL_HOST_USER |
No | SMTP username |
EMAIL_HOST_PASSWORD |
No | SMTP password |
GOOGLE_AUTH_CLIENT_ID |
No | For Google OAuth login. Get from Google Cloud Console → Credentials. |
GOOGLE_AUTH_CLIENT_SECRET |
No | Google OAuth secret |
For social media API keys, see Platform Credentials. Full variable reference: .env.example.
git clone https://github.com/brightbeanxyz/brightbean-studio.git
cd brightbean-studio
cp .env.example .envEdit .env - change DATABASE_URL to point to the Docker service name:
DATABASE_URL=postgres://postgres:postgres@postgres:5432/brightbean
Then start everything:
docker compose up -d
docker compose exec app python manage.py migrate
docker compose exec app python manage.py createsuperuserOpen http://localhost:8000 - you're running.
Run everything natively - no Docker, no PostgreSQL install. Uses SQLite for the database.
1. Clone and configure
git clone https://github.com/brightbeanxyz/brightbean-studio.git
cd brightbean-studio
cp .env.example .env2. Switch to SQLite
Open .env and replace the DATABASE_URL line:
DATABASE_URL=sqlite:///db.sqlite3
That's it - no database server to install or manage.
3. Set up Python
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt4. Set up Tailwind CSS
cd theme/static_src
npm install
cd ../..5. Run database migrations
6. Create your admin account
python manage.py createsuperuser7. Start the app (3 terminal tabs)
Tab 1 - Tailwind watcher:
cd theme/static_src && npm run startTab 2 - Django dev server:
source .venv/bin/activate
python manage.py runserverTab 3 - Background worker:
source .venv/bin/activate
python manage.py process_tasksOpen http://localhost:8000 and log in with the superuser you created.
source .venv/bin/activate # activate Python env
python manage.py runserver # start web server
# (open another tab)
python manage.py process_tasks # start workerNote: SQLite is fine for local development and small deployments. For production or heavy concurrent usage, switch to PostgreSQL.
With coverage:
pytest --cov=apps --cov-report=term-missingruff check . # lint
ruff format --check . # format check
mypy apps/ config/ --ignore-missing-imports # type checkAuto-fix lint issues:
ruff check --fix .
ruff format .# On your server:
git clone https://github.com/brightbeanxyz/brightbean-studio.git
cd brightbean-studio
cp .env.example .env
# Edit .env:
# SECRET_KEY=<generate a random 50+ char string>
# DEBUG=false
# ALLOWED_HOSTS=yourdomain.com
# APP_URL=https://yourdomain.com
# DATABASE_URL=postgres://postgres:<strong-password>@postgres:5432/brightbean
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
docker compose exec app python manage.py migrate
docker compose exec app python manage.py createsuperuserThis starts 4 containers: app (Gunicorn), worker, PostgreSQL, and Caddy (auto-HTTPS). Edit the Caddyfile with your domain.
To update:
git pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
docker compose exec app python manage.py migrate| Platform | Config file | Notes |
|---|---|---|
| Heroku | Procfile + app.json |
Deploy-button ready. Must use Basic+ dynos (Eco dynos break the worker). |
| Railway | railway.toml |
Three services: web, worker, managed PostgreSQL. |
| Render | render.yaml |
Blueprint with web, worker, PostgreSQL. Must use paid tier. |
All platforms with ephemeral filesystems require STORAGE_BACKEND=s3 - see .env.example for S3 configuration.
See architecture.md for detailed per-platform instructions and cost breakdowns.
brightbean-studio/
├── config/
│ ├── settings/
│ │ ├── base.py # Shared settings
│ │ ├── development.py # Local dev overrides
│ │ ├── production.py # Production hardening
│ │ └── test.py # Test overrides
│ ├── urls.py # Root URL configuration
│ ├── wsgi.py
│ └── asgi.py
├── apps/
│ ├── accounts/ # Custom User model, auth, OAuth, sessions
│ ├── organizations/ # Organization management
│ ├── workspaces/ # Workspace CRUD
│ ├── members/ # RBAC, invitations, middleware, decorators
│ ├── settings_manager/ # Configurable defaults with cascade logic
│ ├── credentials/ # Platform API credential storage (encrypted)
│ └── common/ # Shared: encrypted fields, scoped model managers
├── providers/ # Social platform API modules (one file per platform)
├── templates/ # Django templates
│ ├── base.html # Layout with sidebar + nav
│ └── components/ # Reusable HTMX partials
├── static/
│ └── js/ # Vendored HTMX + Alpine.js
├── theme/ # django-tailwind theme app
│ └── static_src/
│ ├── src/styles.css # Tailwind directives
│ └── tailwind.config.js
├── Dockerfile
├── docker-compose.yml # Dev: app + worker + postgres
├── docker-compose.prod.yml # Prod override: adds Caddy, uses Gunicorn
├── Caddyfile # Reverse proxy + auto-HTTPS config
├── .env.example # All environment variables
├── Procfile # Heroku
├── app.json # Heroku deploy button
├── railway.toml # Railway config
└── render.yaml # Render blueprint
All configuration is via environment variables. See .env.example for the full list.
Key variables for local development:
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
(required) | Django secret key. Any random string for dev. |
DEBUG |
false |
Set to true for local development. |
DATABASE_URL |
- | PostgreSQL connection string. |
STORAGE_BACKEND |
local |
local for filesystem, s3 for S3-compatible storage. |
EMAIL_BACKEND_TYPE |
smtp |
Set to smtp for SMTP or leave default (console in dev). |
To connect social media accounts, you need API credentials from each platform's developer portal. You can set these via environment variables in .env (see .env.example) or through the admin UI at Settings → Platform Credentials.
Redirect URI: When registering your app on any platform, set the OAuth redirect URI to:
{APP_URL}/social-accounts/callback/{platform}/
For example, if your APP_URL is https://brightbean.example.com, the Facebook redirect URI would be https://brightbean.example.com/social-accounts/callback/facebook/.
Facebook, Instagram, and Threads all use the same Meta app credentials.
-
Go to Meta for Developers and create a new app (type: Business)
-
Under App Settings → Basic, copy your App ID and App Secret
-
In the App Dashboard, go to Use cases and add the following four use cases. For each use case, click into it and go to Permissions and features to add the required optional permissions:
Use case: "Manage everything on your Page" (Facebook)
- This use case auto-includes
business_management,pages_show_list, andpublic_profile - Add these optional permissions:
pages_manage_posts,pages_read_engagement,pages_read_user_content,pages_manage_metadata
Use case: "Messenger from Meta" (Facebook Messaging)
- Required to enable the
pages_messagingpermission, which is not available under the "Manage Pages" use case - Add the optional permission:
pages_messaging
Use case: "Manage messaging & content on Instagram" (Instagram)
- Add these permissions:
instagram_basic,instagram_content_publish,instagram_manage_comments,instagram_manage_insights
Use case: "Access the Threads API" (Threads)
- This use case auto-includes
threads_basic - Add these optional permissions:
threads_content_publish,threads_manage_insights,threads_manage_replies
- This use case auto-includes
-
Under Facebook Login → Settings → Valid OAuth Redirect URIs, add the following redirect URIs:
{APP_URL}/social-accounts/callback/facebook/ {APP_URL}/social-accounts/callback/instagram/ {APP_URL}/social-accounts/callback/threads/ -
Set the environment variables:
PLATFORM_FACEBOOK_APP_ID=your-app-id PLATFORM_FACEBOOK_APP_SECRET=your-app-secret
The Instagram (Personal) connector uses the Instagram API with Instagram Login - a separate OAuth flow from the Facebook Login-based Instagram connector above. This supports personal, creator, and business Instagram accounts without requiring a linked Facebook Page.
- In the same Meta app, go to Use cases and add the "Instagram API" use case
- Under API setup with Instagram Login, note your Instagram App ID and Instagram App Secret (these are different from your Facebook App ID/Secret)
- Go to Permissions and features and add the required permissions:
instagram_business_basic,instagram_business_content_publish,instagram_business_manage_comments,instagram_business_manage_messages
- Under API setup with Instagram Login → Step 4: Set up Instagram business login, click Set up and add the redirect URI:
{APP_URL}/social-accounts/callback/instagram_personal/ - Set the environment variables:
PLATFORM_INSTAGRAM_APP_ID=your-instagram-app-id PLATFORM_INSTAGRAM_APP_SECRET=your-instagram-app-secret
Brightbean uses a single LinkedIn app (with Community Management API) for both personal profile and Company Page connections - each flow simply requests different OAuth scopes. The connect page shows two cards: LinkedIn (Personal Profile) and LinkedIn (Company Page).
- Go to the LinkedIn Developer Portal and create a new app
- Verify your app's association with a LinkedIn Company Page
- Under Products, request access to:
- Community Management API (restricted - requires LinkedIn review)
- Under Auth, add both redirect URIs:
{APP_URL}/social-accounts/callback/linkedin_personal/ {APP_URL}/social-accounts/callback/linkedin_company/ - Scopes are requested per connection type:
- Personal profile:
r_basicprofile,w_member_social,r_member_social - Company page:
r_basicprofile,w_member_social,w_organization_social,r_organization_social,rw_organization_admin
- Personal profile:
- Set the environment variables:
PLATFORM_LINKEDIN_CLIENT_ID=your-client-id PLATFORM_LINKEDIN_CLIENT_SECRET=your-client-secret
Refresh tokens: Community Management API provides refresh tokens natively (365-day lifetime, access tokens last 60 days). No Advertising API product is needed. Note: the Share on LinkedIn and Community Management API products are mutually exclusive on the same app - use Community Management API as it covers both personal and organization scopes.
- Go to the TikTok Developer Portal and create a new app
- Add the products Login Kit, Content Posting API, and Comment API
- Configure the redirect URI under your app's settings:
{APP_URL}/social-accounts/callback/tiktok/ - Required scopes:
user.info.basic,video.publish,video.upload,comment.list,comment.list.manage - Note: TikTok uses Client Key (not Client ID). Copy the Client Key and Client Secret from your app dashboard
- Set the environment variables:
PLATFORM_TIKTOK_CLIENT_KEY=your-client-key PLATFORM_TIKTOK_CLIENT_SECRET=your-client-secret
YouTube and Google Business Profile share the same Google Cloud credentials.
- Go to the Google Cloud Console and create a new project (or select an existing one)
- Enable the following APIs under APIs & Services → Library:
- YouTube Data API v3 (for YouTube)
- My Business Account Management API, My Business Business Information API, and Google My Business API (for Google Business Profile)
- Go to APIs & Services → Credentials and create an OAuth 2.0 Client ID (type: Web application)
- Add the following redirect URIs under Authorized redirect URIs:
{APP_URL}/social-accounts/callback/youtube/ {APP_URL}/social-accounts/callback/google_business/ - Copy the Client ID and Client Secret
- Required scopes:
- YouTube:
https://www.googleapis.com/auth/youtube.upload,https://www.googleapis.com/auth/youtube.readonly,https://www.googleapis.com/auth/youtube.force-ssl - Google Business Profile:
https://www.googleapis.com/auth/business.manage
- YouTube:
- Set the environment variables:
PLATFORM_GOOGLE_CLIENT_ID=your-client-id PLATFORM_GOOGLE_CLIENT_SECRET=your-client-secret
- Go to the Pinterest Developer Portal and create a new app
- Under your app settings, add the redirect URI:
{APP_URL}/social-accounts/callback/pinterest/ - Copy the App ID and App Secret
- Required scopes:
boards:read,pins:read,pins:write - Set the environment variables:
PLATFORM_PINTEREST_APP_ID=your-app-id PLATFORM_PINTEREST_APP_SECRET=your-app-secret
No developer app registration needed. Users connect by entering their Bluesky handle and an App Password:
- Log in to Bluesky
- Go to Settings → App Passwords
- Create a new app password and use it when connecting your account in Brightbean
No developer app registration needed. Brightbean automatically registers an OAuth application on each Mastodon instance when a user connects their account. Users just need to enter their instance URL (e.g., mastodon.social).
See the Supported Platforms matrix above for per-platform inbox capabilities.
To import historical messages (e.g., from the last 7 days):
python manage.py backfill_inbox --days 7Options:
--days N- Number of days to backfill (default: 7)--platform NAME- Only backfill a specific platform (e.g.,youtube,linkedin,tiktok)--account-id UUID- Only backfill a specific account
| Layer | Technology |
|---|---|
| Backend | Django 5.x |
| Frontend | Django templates, HTMX, Alpine.js |
| CSS | Tailwind CSS 4 via django-tailwind |
| Database | PostgreSQL 16+ |
| Background jobs | django-background-tasks (no Redis required) |
| Auth | django-allauth (email + Google OAuth) |
| Media | Pillow (images), FFmpeg (video) |
| Deployment | Docker, Gunicorn, Caddy |
Docker: postgres container is unhealthy
Wait 10-15 seconds after docker compose up for the health check to pass, then retry your command. Check logs with docker compose logs postgres.
python manage.py migrate fails with connection errors
Make sure PostgreSQL is running and healthy. For Docker: docker compose ps should show postgres as "healthy". For local: verify the DATABASE_URL in .env matches your setup.
Tailwind CSS changes not appearing
Make sure the Tailwind watcher is running: cd theme/static_src && npm run start. If styles still don't update, try npm run build for a full rebuild.
OAuth callback errors ("redirect URI mismatch")
The redirect URI registered on the platform must exactly match {APP_URL}/social-accounts/callback/{platform}/. Check that APP_URL in .env matches the URL you're accessing (including http vs https and port number).
Background tasks not running (posts not publishing)
Make sure the worker is running: python manage.py process_tasks. In Docker: check docker compose logs worker.
See CONTRIBUTING.md for development setup, coding guidelines, and how to submit pull requests.
To report a security vulnerability, see SECURITY.md. Do not open a public issue.
AGPL-3.0 - see LICENSE for details.