A single-user bridge service that automatically cross-posts from your Bluesky account to your Mastodon account. Built specifically for Val.town with TypeScript and Deno.
- Connects your accounts: Securely links your Bluesky and Mastodon accounts using OAuth
- Automatic syncing: Checks for new Bluesky posts every 15 minutes and cross-posts them to Mastodon
- Smart transformations: Converts Bluesky mentions (@handle.bsky.social) to profile links since they don't exist on Mastodon
- Media support: Uploads images and videos from Bluesky to your Mastodon instance
- Duplicate prevention: Tracks synced posts to avoid posting the same content twice
- Error handling: Retries failed posts and logs errors for troubleshooting
✅ OAuth authentication for both Bluesky and Mastodon
✅ Media cross-posting (images & videos)
✅ Mention transformation (handles → profile links)
✅ Duplicate prevention via content hashing
✅ Retry mechanism with exponential backoff
✅ Error logging and sync tracking
✅ Setup wizard for easy configuration
Copy all files from this repository into your Val.town account. The project structure should look like:
├── backend/
│ ├── index.ts # Main HTTP handler
│ ├── database/
│ ├── routes/
│ └── services/
├── cronjob.ts # Cron job for syncing
├── frontend/
│ └── index.html # Setup wizard UI
└── shared/
└── types.ts # Shared interfaces
HTTP Trigger (for the web interface and API):
- Set
backend/index.ts
as an HTTP val - This serves the setup wizard and handles OAuth callbacks
Cron Trigger (for automatic syncing):
- Set
cronjob.ts
as a Cron val - Schedule:
*/15 * * * *
(every 15 minutes) - For paid accounts, you can use shorter intervals like
*/5 * * * *
(every 5 minutes)
Set these environment variables in your Val.town account:
ATPROTO_CLIENT_ID=https://your-val-url.web.val.run/client-metadata.json
You'll also need to create a client-metadata.json
file (see OAuth setup
below).
Create a client metadata file at the root of your val that's accessible via HTTP:
{ "client_id": "https://your-val-url.web.val.run/client-metadata.json", "client_name": "ATProto to Fediverse Bridge", "redirect_uris": [ "https://your-val-url.web.val.run/api/oauth/atproto/callback" ], "scope": "atproto", "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "token_endpoint_auth_method": "none", "dpop_bound_access_tokens": true }
The service automatically registers with your Mastodon instance during setup - no manual configuration needed.
- Visit your HTTP val URL (e.g.,
https://your-val-url.web.val.run
) - Click "Get Started" to begin the setup wizard
- Connect your Bluesky account by entering your handle
- Connect your Mastodon account by entering your instance URL
- Complete the setup
Once configured, the service runs automatically:
- Every 15 minutes (or your configured interval), the cron job runs
- It fetches new posts from your Bluesky account
- Transforms the content (mentions become profile links)
- Uploads any media to your Mastodon instance
- Creates the cross-post on Mastodon
- Logs the result for your review
✅ Included:
- Regular posts with text
- Posts with images/videos
- Posts with links and hashtags
- Posts with mentions (converted to profile links)
❌ Excluded:
- Replies to other posts
- Empty posts
- Posts you've already cross-posted
Visit your val's dashboard to see:
- Setup status and account connections
- Recent sync activity and statistics
- Error logs for troubleshooting
- Post sync history
- Free Val.town accounts: 15-minute minimum sync interval
- Bluesky media limits: 1MB images, 100MB videos
- Mastodon compatibility: Works with all Mastodon instances
- Single user: Designed for personal use (one Bluesky → one Mastodon)
OAuth failures: Check your client metadata URL is publicly accessible
Sync not working: Verify both accounts are connected in the dashboard
Missing posts: Check the sync logs for specific error messages
Media upload fails: Bluesky/Mastodon may have different file size limits
- All tokens are stored encrypted in SQLite
- No data leaves your Val.town instance
- OAuth follows security best practices
- You can revoke access anytime from your account settings
This is an open-source project. For issues or feature requests, check the repository or Val.town community forums.
Built with ❤️ for the decentralized social web