An easy way to synchronize posts from 𝕏 (Twitter) to other social media platforms.
| Platform | Notes |
|---|---|
| ☁️ Bluesky | Full support: posts, threads, media, quotes, replies, profile sync |
| 🇩 Discord | Webhook embeds with engagement stats |
The quickest way in is the web dashboard. You configure everything in the browser, so there are no config files to edit by hand. Here's the whole thing start to finish.
You'll need a machine that can run Bun (macOS, Linux, or Windows under WSL). If you'd rather use containers, jump to the Docker section below.
You'll also need a few accounts and credentials:
- The handle of the X (Twitter) account you want to mirror. Just the public handle, e.g.
nasa. - An X (Twitter) login for the bot to read tweets with. You enter this once, and every bot shares it.
- At least one place to post to:
- For Bluesky, your handle and an app password. Create the app password under Bluesky Settings, App Passwords. Don't use your account password.
- For Discord, a webhook URL for the target channel. Create it under the channel's Edit, Integrations, Webhooks, New Webhook, then copy the URL.
Install Bun and grab the code:
# Install Bun (other options at https://bun.sh)
curl -fsSL https://bun.sh/install | bash
git clone https://github.com/netarcx/roccobots.git
cd roccobots
bun installPick a dashboard password and start the server with it:
WEB_ADMIN_PASSWORD="pick-a-strong-password" bun run start:webOn the first run, RoccoBots generates an encryption key for your saved credentials and writes it next to the database. That's fine for trying things out. On a real server you should set the key yourself so it survives rebuilds; see Web Environment Variables.
Now open http://localhost:3000 and log in with the password you just set.
Before adding a bot, go to Settings and enter the X (Twitter) login the bot will use to read tweets. You only do this once, and all bots use it.
Then click Add Bot and fill in the form:
- The source handle (the X account to mirror, without the
@). - A sync frequency. Thirty minutes is a reasonable starting point.
- One destination and its credentials (the Bluesky app password or Discord webhook URL from above).
Save the bot, then click Start on its card. New posts from the source account will start showing up on your destination. The Logs page shows what the bot is doing as it runs.
There's a more detailed version of this in the web dashboard guide.
RoccoBots runs in one of two modes. Pick whichever fits how you like to work.
| CLI Mode | Web Dashboard Mode | |
|---|---|---|
| Config | .env file |
Browser UI |
| Multi-bot | Numeric suffixes in .env |
Per-bot in the dashboard |
| Credentials | Plaintext in .env |
Encrypted in SQLite |
| Monitoring | Terminal logs | Live dashboard with log history |
| Analytics | Not available | Bluesky engagement stats |
| Entry point | bun src/index.ts |
bun src/web-index.ts |
Set the dashboard password and start the server:
WEB_ADMIN_PASSWORD=your_secure_password bun src/web-index.ts
# http://localhost:3000An encryption key is generated and saved beside the database on the first run. To pin a specific key (needed for reproducible deployments and migrations), generate one and add it to your environment:
bun run generate-key
# copy the printed value into ENCRYPTION_KEY
ENCRYPTION_KEY=<64-character hex string>Once the server is up, open the dashboard and:
- Go to Settings and configure the Twitter login that all bots share.
- Click Add Bot, enter the handle to mirror, and set up the destination platforms.
- Press Start on the bot card.
docker-compose.yml, using the pre-built image from the GitHub Container Registry:
services:
roccobots-web:
container_name: "roccobots-web"
image: ghcr.io/netarcx/roccobots:latest
restart: unless-stopped
env_file: ".env.web"
environment:
- DATABASE_PATH=/data/data.sqlite
ports:
- "3000:3000"
volumes:
- ./data:/data
command: ["bun", "./src/web-index.ts"].env.web:
WEB_ADMIN_PASSWORD=your_secure_password
# Optional. Pin the encryption key so credentials survive container rebuilds.
# Generate with: bun run generate-key
ENCRYPTION_KEY=your_64_char_hex_key
# Optional
WEB_PORT=3000
DATABASE_PATH=/data/data.sqliteIf you leave ENCRYPTION_KEY unset, a key file is generated at /data/.encryption.key. Mount the /data volume so it persists across restarts. Lose that key and every saved platform credential becomes unreadable.
- Bot cards showing status (running, stopped, error), last sync time, and enabled platforms
- Start and stop individual bots, or all of them at once
- Import bots from an existing
.envfile
- Click Add Bot
- Enter the source Twitter handle
- Set the sync frequency (default: 30 minutes)
- Toggle the sync options you want (posts, bio, profile picture, display name, header)
- Add one or more destination platforms and fill in their credentials
- Save. Credentials are encrypted at rest with AES-256-GCM.
- Twitter Authentication: the single Twitter login all bots use to read tweets
- Backup & Restore: export or import all bot configs, platform credentials, and sync state as JSON
The backup file contains plaintext credentials, so store it somewhere safe.
Each bot keeps a paginated log history with timestamps and platform tags (info, warn, error, success). Open it from the bot card or at /bots/:id/logs.
Tracks engagement on posts that have synced to Bluesky.
- Open Analytics and select a bot
- Click Refresh from Bluesky to pull current like, repost, reply, and quote counts
- Posts are ranked by total engagement
Analytics can be turned on or off per bot under Sync Options, Bluesky Analytics.
Per-bot rules that rewrite post text before it's sent to a platform:
| Rule | Effect |
|---|---|
prepend |
Add text before every post |
append |
Add text after every post |
regex_replace |
Find and replace with a regular expression |
strip_urls |
Remove URLs matching a pattern |
add_hashtags |
Append hashtags to every post |
Rules apply globally or only to specific platforms (Bluesky, for example). Configure them through the API at PUT /api/bots/:id/transforms.
Trusted Bluesky handles can control a bot by mentioning it with a command:
| Command | Action |
|---|---|
!sync |
Trigger an immediate sync |
!restart |
Restart the bot |
!source @handle |
Change the source Twitter account |
!status |
Report current bot status |
!frequency <min> |
Change the sync interval |
!posts on/off |
Toggle post syncing |
!bio on/off |
Toggle bio syncing |
!help |
List available commands |
Turn commands on per bot in the bot edit form under Bluesky Commands, where you also set the trusted handles and the poll interval (default: 60 seconds).
| Variable | Required | Default | Description |
|---|---|---|---|
WEB_ADMIN_PASSWORD |
Yes | none | Password for the web dashboard login |
ENCRYPTION_KEY |
No | auto-generated | 64-char hex key used to encrypt stored credentials |
WEB_PORT |
No | 3000 |
HTTP port the server listens on |
DATABASE_PATH |
No | data.sqlite |
Path to the SQLite database file |
LOG_RETENTION_DAYS |
No | 30 |
How many days of sync logs to keep |
docker-compose.cli.yml, using the pre-built image:
services:
roccobots:
container_name: "roccobots"
image: ghcr.io/netarcx/roccobots:latest
restart: unless-stopped
env_file: ".env"
environment:
- DATABASE_PATH=/data/data.sqlite
volumes:
- ./data:/dataRun it with:
docker compose -f docker-compose.cli.yml up -d# X (Twitter)
TWITTER_HANDLE=YourXHandle
TWITTER_USERNAME=your_x_email@example.com
TWITTER_PASSWORD=YourXPassword
# Bluesky (optional)
BLUESKY_INSTANCE=bsky.social # default: bsky.social
BLUESKY_IDENTIFIER=your-handle.bsky.social
BLUESKY_PASSWORD=xxxx-xxxx-xxxx-xxxx # use an app password
# Discord webhook (optional)
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890/abcdeEach account uses a numeric suffix. The first has no suffix, the second uses 1, the third uses 2, and so on. Accounts can target different platforms.
# ======= ACCOUNT 0 (no suffix) =======
TWITTER_HANDLE=FirstXHandle
BLUESKY_IDENTIFIER=first-handle.bsky.social
BLUESKY_PASSWORD=xxxx-xxxx-xxxx-xxxx
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/111111111/xxxxxxxxxx
# ======= ACCOUNT 1 =======
TWITTER_HANDLE1=SecondXHandle
BLUESKY_IDENTIFIER1=second-handle.bsky.social
BLUESKY_PASSWORD1=yyyy-yyyy-yyyy-yyyy
# ======= ACCOUNT 2 =======
TWITTER_HANDLE2=ThirdXHandle
DISCORD_WEBHOOK_URL2=https://discord.com/api/webhooks/222222222/zzzzzzzzzz| Variable | Default | Description |
|---|---|---|
SYNC_FREQUENCY_MIN |
30 |
Minutes between sync cycles |
DAEMON |
true |
Keep running on a loop. Set to false to run once and exit |
SYNC_POSTS |
true |
Sync posts |
SYNC_PROFILE_DESCRIPTION |
true |
Sync bio |
SYNC_PROFILE_PICTURE |
true |
Sync profile picture |
SYNC_PROFILE_NAME |
true |
Sync display name |
SYNC_PROFILE_HEADER |
true |
Sync header image |
BACKDATE_BLUESKY_POSTS |
true |
Use the original tweet timestamp on Bluesky |
DATABASE_PATH |
data.sqlite |
Path to the SQLite database |
FORCE_SYNC_POSTS |
false |
Re-sync already-synced posts |
# Install Bun: https://bun.sh
bun install
# CLI mode
bun src/index.ts
# Web dashboard mode
bun src/web-index.tsBluesky: @beastModeRocco.com