|
|
||
|---|---|---|
| .vscode | Latest snapshot | |
| app | Latest snapshot | |
| components | Latest snapshot | |
| hooks | Latest snapshot | |
| lib | Latest snapshot | |
| public | Latest snapshot | |
| scripts | Latest snapshot | |
| styles | Latest snapshot | |
| supabase/migrations | Latest snapshot | |
| .gitignore | Latest snapshot | |
| components.json | Latest snapshot | |
| LICENSE | Latest snapshot | |
| middleware.ts | Latest snapshot | |
| next-env.d.ts | Latest snapshot | |
| next.config.mjs | Latest snapshot | |
| package-lock.json | Latest snapshot | |
| package.json | Latest snapshot | |
| pnpm-lock.yaml | Latest snapshot | |
| postcss.config.mjs | Latest snapshot | |
| README.md | Latest snapshot | |
| tsconfig.json | Latest snapshot | |
| vercel.json | Latest snapshot | |
TootPulse
Open-source social media management for your team. Schedule, approve, and publish posts to Mastodon, LinkedIn, and more from a single dashboard.
Features
- Multi-platform publishing -- Connect multiple accounts per project across Mastodon, LinkedIn, Bluesky, Threads, Reddit, Facebook Pages, Instagram, and more — including email newsletters via Buttondown, Resend, Mailgun, Brevo, Mailchimp, SendGrid, ConvertKit, and Beehiiv. Choose which connections each post publishes to.
- Team collaboration -- Invite members by email. Assign admin or member roles. Pending invites auto-accept on signup.
- Approval workflows -- Optional sequential or parallel approval groups. Admins can reorder groups and toggle the entire workflow on or off per project.
- Scheduling -- Schedule posts for future dates. A cron job (
/api/cron/publish) automatically publishes approved posts when their scheduled time arrives. - Calendar and timeline views -- Visualise scheduled posts in a monthly calendar grid or a chronological timeline list.
- Edit history -- Every content change is tracked with diffs and author attribution.
- Invite-only mode -- Public sign-up can be disabled via an environment variable, restricting access to email-invited users only.
Tech stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| Database & Auth | Supabase (PostgreSQL + Row Level Security + Auth) |
| UI | shadcn/ui, Tailwind CSS v4, Radix primitives |
| Deployment | Vercel |
Getting started
Prerequisites
1. Clone and install
git clone https://github.com/joramulmke/TootPulse.git
cd TootPulse
npm install
2. Environment variables
Create a .env.local file (or set these in your Vercel project settings):
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# NEXT_PUBLIC_APP_URL=your-url
# instead
# Go to your Supabase project dashboard: **Authentication > URL Configuration**
# Set **Site URL** to your deployed app URL (e.g. `https://tootpulse.app`)
# Under **Redirect URLs**, add your deployed URL with a wildcard pattern: `https://yourdomain.com/**`
# Sign-up control
# Set to "false" to disable public registration (invite-only mode)
NEXT_PUBLIC_SIGNUP_ENABLED=false
# App name (optional) — customize the displayed app name in the UI
# Defaults to "TootPulse" if not set
NEXT_PUBLIC_APP_NAME=TootPulse
# App tagline (optional) — customize the tagline shown in the footer
# Defaults to "Open-source social media management for your team." if not set
NEXT_PUBLIC_APP_TAGLINE=Open-source social media management for your team.
# Legal
NEXT_PUBLIC_IMPRESSUM_URL=your-legal-url
# Donation link (optional) — shown in the user dropdown menu above Sign out
NEXT_PUBLIC_DONATION_URL=your-donation-url
# Cron secret (protects the /api/cron/publish endpoint)
CRON_SECRET=your-cron-secret
# * * * * * curl -s -H "Authorization: Bearer YOUR_CRON_SECRET" https://your-app.com/api/cron/publish
# OAuth (optional — enables one-click OAuth for LinkedIn, Threads, and Reddit)
# Without these, users enter credentials manually. Both modes coexist.
# See "OAuth setup" below for how to obtain these values.
LINKEDIN_CLIENT_ID=your-linkedin-client-id
LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret
THREADS_CLIENT_ID=your-threads-client-id
THREADS_CLIENT_SECRET=your-threads-client-secret
REDDIT_CLIENT_ID=your-reddit-client-id
REDDIT_CLIENT_SECRET=your-reddit-client-secret
# Shared for Facebook Page and Instagram OAuth
FACEBOOK_APP_ID=your-facebook-app-id
FACEBOOK_APP_SECRET=your-facebook-app-secret
3. Database setup
The database schema is managed through Supabase migrations. The key tables are:
profiles-- User display names and avatars (synced from auth.users)projects-- Workspace containers with approval and schedule settingsproject_members-- User-to-project membership with rolesproject_invites-- Email-based invitations with auto-accept on signupsocial_connections-- Platform credentials (Mastodon, LinkedIn) per projectposts-- Content with status, scheduling, and language metadatapost_connections-- Many-to-many link between posts and social connections, tracking per-connection publish statusapproval_groups-- Ordered approval stages per projectpost_approvals-- Per-group approval decisions on each postpost_edits-- Full edit history with diffs
Row Level Security (RLS) is enabled on all tables, scoped through is_project_member() and is_project_admin() helper functions.
4. Run locally
npm run dev
Open http://localhost:3000.
Project structure
app/
page.tsx # Landing page
auth/ # Login, sign-up, sign-up success
dashboard/
page.tsx # Project list
calendar/page.tsx # Cross-project calendar
project/[projectId]/
page.tsx # Project detail (posts, schedule, settings)
posts/new/page.tsx # Create post
posts/[postId]/page.tsx # Post detail with approvals
posts/[postId]/edit/page.tsx # Edit post
api/cron/publish/route.ts # Scheduled publishing cron
api/oauth/
linkedin/route.ts # LinkedIn OAuth initiation
linkedin/callback/route.ts # LinkedIn OAuth callback
threads/route.ts # Threads OAuth initiation
threads/callback/route.ts # Threads OAuth callback
reddit/route.ts # Reddit OAuth initiation
reddit/callback/route.ts # Reddit OAuth callback
facebook/route.ts # Facebook Page OAuth initiation
facebook/callback/route.ts # Facebook Page OAuth callback
instagram/route.ts # Instagram OAuth initiation
instagram/callback/route.ts # Instagram OAuth callback
components/
dashboard/ # Dashboard shell, project list
project/ # Post form, post detail, settings,
# connection manager, member manager,
# approval groups, calendar, timeline
footer.tsx # Global footer
ui/ # shadcn/ui primitives
lib/
types.ts # Shared TypeScript types
oauth-state.ts # Nonce-based CSRF state for OAuth flows
char-limits.ts # Per-platform character limits
csv-import.ts # Bulk post import from CSV
approval.ts # Approval workflow helpers
utils.ts # General utilities
{platform}.ts # One file per platform (mastodon, linkedin,
# bluesky, pixelfed, threads, tumblr, reddit,
# telegram, discord, webhook, pinterest,
# facebook, instagram, wordpress, httprequest,
# ntfy, matrix, unifiedpush, slack, teams,
# buttondown, resend, mailgun, brevo,
# mailchimp, sendgrid, convertkit, beehiiv)
actions/
connection-actions.ts # CRUD for social connections
post-actions.ts # CRUD for posts
project-actions.ts # Projects, members, invites, approval groups
publish-actions.ts # Multi-platform publishing logic
OAuth setup
If you configure OAuth credentials for LinkedIn, Threads, Reddit, Facebook, or Instagram, users see a "Connect with [Platform]" button instead of manually entering tokens. Both flows always coexist — a "enter manually" toggle is available as a fallback.
- Go to developers.linkedin.com and create an app.
- Under Auth, add
{APP_URL}/api/oauth/linkedin/callbackas an authorized redirect URL. - Under Products, request access to Sign In with LinkedIn using OpenID Connect (grants
openid profile) and Share on LinkedIn (grantsw_member_social). - For company page posting, also enable the Advertising API product (grants
w_organization_social). - Copy the Client ID and Client Secret into your env vars.
Threads
- Go to developers.facebook.com and create a Meta app with the Threads API product.
- Under Threads API > Settings, add
{APP_URL}/api/oauth/threads/callbackas a valid OAuth redirect URI. - Copy the App ID (as
THREADS_CLIENT_ID) and App Secret (asTHREADS_CLIENT_SECRET) into your env vars.
- Go to reddit.com/prefs/apps and create a web app (not script).
- Set the redirect URI to
{APP_URL}/api/oauth/reddit/callback. - Copy the client ID (shown below the app name) and secret into your env vars.
Reddit OAuth tokens expire after 1 hour. TootPulse automatically refreshes them using the stored refresh token when a publish fails with a 401 — no action needed.
Facebook Page
- Go to developers.facebook.com and create a Meta app.
- Add the Facebook Login product and under its settings add
{APP_URL}/api/oauth/facebook/callbackas a valid OAuth redirect URI. - Request the permissions
pages_show_list,pages_read_engagement, andpages_manage_posts. - Copy the App ID (as
FACEBOOK_APP_ID) and App Secret (asFACEBOOK_APP_SECRET) into your env vars. - When connecting, users enter their Page ID first (found in Facebook Page Settings → About), then authorize via OAuth. The Page access token is fetched automatically.
Facebook Page access tokens obtained via a long-lived user token do not expire as long as the user keeps the app authorized. If publishing fails with a token error, reconnect the page via OAuth.
- Use the same Meta app as Facebook (same
FACEBOOK_APP_ID/FACEBOOK_APP_SECRET). - Add the Instagram Graph API product to your Meta app.
- Under Instagram Graph API > Settings, add
{APP_URL}/api/oauth/instagram/callbackas a valid OAuth redirect URI. - Request the permissions
instagram_basic,instagram_content_publish, andpages_show_list. - The connected Instagram account must be a Business or Creator account linked to a Facebook Page.
Instagram requires an image on every post — text-only posts will not publish to Instagram. Tokens expire after ~60 days; reconnect if publishing fails.
Email newsletters
No OAuth is required for any email platform. Credentials are entered manually in the connection form (API key plus sender details). The post title is used as the email subject line (falls back to the first line of content if no title is set).
Platform-specific notes:
- Mailchimp — the API key must include the server suffix (e.g.
abc123-us1) - SendGrid — requires a
sender_id(the numeric ID of a verified sender in your SendGrid account) - Resend / ConvertKit — two-step flow: create draft, then publish/send
- Brevo — two-step flow: create campaign, then send
- Mailchimp — three-step flow: create campaign → set content → send
- Buttondown / Beehiiv — single-step send
Adding a social connection
- Open a project and go to Settings > Connections.
- Click Add Connection and choose a platform.
- For Mastodon: enter the instance URL (e.g.
https://mastodon.social) and an access token generated from your Mastodon account settings. - For LinkedIn / Threads / Reddit / Facebook / Instagram: click Connect with [Platform] if OAuth is configured, or use the manual token form.
- Click Test to verify the credentials, then Save.
Scheduled publishing
Posts with a scheduled_at date are automatically published by the cron endpoint at /api/cron/publish. Configure a Vercel Cron Job or an external scheduler to call this endpoint periodically:
// vercel.json
{
"crons": [
{
"path": "/api/cron/publish",
"schedule": "*/5 * * * *"
}
]
}
The endpoint is protected by the CRON_SECRET environment variable.
License
This project is open source. See the repository for license details.