tweets-2-bsky#
Crosspost posts from Twitter/X to Bluesky with thread support, media handling, account mapping, and a web dashboard.
Quick Start (Easy Mode)#
If you are comfortable with terminal basics but do not want to manage PM2 manually, use the installer script.
1) Clone the repo#
git clone https://github.com/j4ckxyz/tweets-2-bsky
cd tweets-2-bsky
2) Run install + background start#
chmod +x install.sh
./install.sh
What this does by default:
- installs dependencies
- builds server + web dashboard
- creates/updates
.envwith sensible defaults (PORT=3000, generatedJWT_SECRETif missing) - starts in the background
- uses PM2 if installed
- otherwise uses
nohup
- prints your local web URL (for example
http://localhost:3000)
3) Open the dashboard#
Open the printed URL in your browser, then:
- Register the first user (this user becomes admin).
- Add Twitter cookies in Settings.
- Add at least one mapping.
- Click
Run now.
Useful installer commands#
./install.sh --no-start
./install.sh --start-only
./install.sh --stop
./install.sh --status
./install.sh --port 3100
./install.sh --host 127.0.0.1
./install.sh --skip-native-rebuild
If you prefer full manual setup, skip to Manual Setup.
Linux VPS Without Domain (Secure HTTPS via Tailscale)#
If you host on a public VPS (Linux) and do not own a domain, use the server installer:
chmod +x install-server.sh
./install-server.sh
What this does:
- runs the normal app install/build/start flow
- auto-selects a free local app port if your chosen/default port is already in use
- forces the app to bind locally only (
HOST=127.0.0.1) - installs and starts Tailscale if needed
- configures
tailscale serveon a free HTTPS port so your dashboard is reachable over Tailnet HTTPS - prints the final Tailnet URL to open from any device authenticated on your Tailscale account
Optional non-interactive login:
./install-server.sh --auth-key <TS_AUTHKEY>
Optional fixed Tailscale HTTPS port:
./install-server.sh --https-port 443
Optional public exposure (internet) with Funnel:
./install-server.sh --funnel
Notes:
- this does not replace or delete
install.sh; it wraps server-hardening around it - normal updates still use
./update.shand keep your local.envvalues - if you already installed manually, this is still safe to run later
What This Project Does#
- crossposts tweets and threads to Bluesky
- handles images, videos, GIFs, quote tweets, and link cards
- stores processed history in SQLite to avoid reposting
- supports multiple Twitter source usernames per Bluesky target
- provides both:
- web dashboard workflows
- CLI workflows (including cron-friendly mode)
Requirements#
- Node.js 22+
- npm
- git
Optional but recommended:
- PM2 (for managed background runtime)
- Chrome/Chromium (used for some quote-tweet screenshot fallbacks)
- build tools for native modules (
better-sqlite3) if your platform needs source compilation
Manual Setup (Technical)#
Standard run (foreground)#
git clone https://github.com/j4ckxyz/tweets-2-bsky
cd tweets-2-bsky
npm install
npm run build
npm start
Open: http://localhost:3000
Set environment values explicitly#
cat > .env <<'EOF'
PORT=3000
JWT_SECRET=replace-with-a-strong-random-secret
EOF
Rebuild native modules after Node version changes#
npm run rebuild:native
npm run build
First-Time Setup via CLI (Alternative to Web Forms)#
npm run cli -- setup-twitter
npm run cli -- add-mapping
npm run cli -- run-now
Recommended Command Examples#
Always invoke CLI commands as:
npm run cli -- <command>
Status and basic operations#
npm run cli -- status
npm run cli -- list
npm run cli -- recent-activity --limit 20
Credentials and configuration#
npm run cli -- setup-twitter
npm run cli -- setup-ai
npm run cli -- set-interval 5
Mapping management#
npm run cli -- add-mapping
npm run cli -- edit-mapping <mapping-id-or-handle>
npm run cli -- remove <mapping-id-or-handle>
Running syncs#
npm run cli -- run-now
npm run cli -- run-now --dry-run
npm run cli -- run-now --web
Backfill and history import#
npm run cli -- backfill <mapping-id-or-handle> --limit 50
npm run cli -- import-history <mapping-id-or-handle> --limit 100
npm run cli -- clear-cache <mapping-id-or-handle>
Dangerous operation (admin workflow)#
npm run cli -- delete-all-posts <mapping-id-or-handle>
Config export/import#
npm run cli -- config-export ./tweets-2-bsky-config.json
npm run cli -- config-import ./tweets-2-bsky-config.json
Mapping references accept:
- mapping ID
- Bluesky handle/identifier
- Twitter username
Cron / CLI-Only Operation#
Run every 5 minutes:
*/5 * * * * cd /path/to/tweets-2-bsky && /usr/bin/npm run cli -- run-now >> /tmp/tweets-2-bsky.log 2>&1
Run one backfill once:
npm run cli -- backfill <mapping-id-or-handle> --limit 50
Background Runtime Options#
Option A: use install.sh (recommended)#
./install.sh
./install.sh --status
./install.sh --stop
Option B: manage PM2 directly#
pm2 start dist/index.js --name tweets-2-bsky
pm2 logs tweets-2-bsky
pm2 restart tweets-2-bsky --update-env
pm2 save
Option C: no PM2 (nohup)#
mkdir -p data/runtime
nohup npm start > data/runtime/tweets-2-bsky.log 2>&1 &
echo $! > data/runtime/tweets-2-bsky.pid
Stop nohup process:
kill "$(cat data/runtime/tweets-2-bsky.pid)"
Updating#
Use:
./update.sh
update.sh:
- stashes local uncommitted changes before pull and restores them after update
- pulls latest code (supports non-
originremotes and detached-head recovery) - installs dependencies
- rebuilds native modules when Node ABI changed
- builds server + web dashboard
- restarts existing runtime for PM2 or nohup mode
- preserves local
config.jsonand.envwith backup/restore
Useful update flags:
./update.sh --no-restart
./update.sh --skip-install --skip-build
./update.sh --remote origin --branch main
Data, Config, and Security#
Local files:
config.json: mappings, credentials, users, app settings (sensitive; do not share)data/database.sqlite: processed tweet history and metadata.env: runtime environment variables (PORT,JWT_SECRET, optional overrides)
Security notes:
- first registered dashboard user is admin
- after bootstrap, only admins can create additional dashboard users
- users can sign in with username or email
- non-admin users only see mappings they created by default
- admins can grant fine-grained permissions (view all mappings, manage groups, queue backfills, run-now, etc.)
- only admins can view or edit Twitter/AI provider credentials
- admin user management never exposes other users' password hashes in the UI
- if
JWT_SECRETis missing, server falls back to an insecure default; set your own secret in.env - prefer Bluesky app passwords (not your full account password)
Multi-User Access Control#
- bootstrap account:
- the first account created through the web UI becomes admin
- open registration is automatically disabled after this
- admin capabilities:
- create, edit, reset password, and delete dashboard users
- assign role (
adminoruser) and per-user permissions - filter the Accounts page by creator to review each user's mappings
- deleting a user:
- disables that user's mappings so crossposting stops
- leaves already-published Bluesky posts untouched
- self-service security:
- every user can change their own password
- users can change their own email after password verification
Development#
Start backend/scheduler from source#
npm run dev
Start Vite web dev server#
npm run dev:web
Build and quality checks#
npm run build
npm run typecheck
npm run lint
Troubleshooting#
See: TROUBLESHOOTING.md
Common recovery after changing Node versions:
npm run rebuild:native
npm run build
npm start
License#
MIT