Automate your social media posting across multiple platforms with a single message to a Telegram bot. Posts are queued and published via a cron-triggered schedule.
- Send media to a Telegram bot to trigger automated posting
- Owner-only — only your Telegram account can use the bot
- Cron-based queue — Cloudflare Worker wakes the server hourly; background loop also processes every 60s
- Telegram credential management — set platform credentials via bot commands, stored in Cloudflare KV
- Multi-platform support:
- Telegram Channel
- Bluesky
- Mastodon
- Threads
- Twitter/X
- YouTube
- Automatic retry on failures
- Turso-backed persistent job queue (with local SQLite fallback)
- Cloudinary media storage (survives container restarts)
- Cloudflare Worker webhook receiver
- Deployed on Render.com free tier
┌───────────────────────┐
│ Cloudflare Worker │
│ │
[Telegram Bot] ──webhook──▶ • Owner-only filter │
│ • Bot commands │──────▶ Cloudflare KV
│ (/setcred, /help…) │ (credentials)
│ • Forward media │
│ • Cron trigger │
└──────┬────────────┬────┘
│ │
/webhook /process-queue
│ │
┌──────▼────────────▼────┐
│ FastAPI on Render.com │
│ │
│ • Download media │
│ • Upload to Cloudinary │──▶ Cloudinary (media CDN)
│ • Queue jobs (Turso) │──▶ Turso (persistent DB)
│ • Background loop 60s │
│ • Post to platforms │
└────────────────────────┘
Flow:
- You send a photo/video/text to the Telegram bot
- Cloudflare Worker checks you're the owner, forwards to Render
- FastAPI downloads the media, uploads it to Cloudinary, and queues jobs in Turso for all enabled platforms
- The background loop (every 60s) or CF Worker cron processes due jobs
- At processing time, media is fetched from Cloudinary if the local file is gone (ephemeral container)
- After all platforms finish posting for a submission, Cloudinary media is deleted automatically
- Python 3.11+
- Cloudflare account (free tier)
- Render.com account (free tier)
- API credentials for each platform you want to use
git clone <your-repo-url>
cd forwardrpython -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activatepip install -r requirements.txtcp .env.example .envEdit .env and fill in your credentials. At minimum, you need:
TELEGRAM_BOT_TOKEN- Create a bot via @BotFatherTELEGRAM_OWNER_ID- Your Telegram user ID (send/startto @userinfobot)API_SECRET_KEY- Generate a secure random stringCLOUDFLARE_WORKER_URL- Your deployed worker URL
# Install Turso CLI
curl -sSfL https://get.tur.so/install.sh | bash
turso auth login
# Create database and get credentials
turso db create forwardr
turso db show forwardr --url # → TURSO_DATABASE_URL
turso db tokens create forwardr # → TURSO_AUTH_TOKENAdd TURSO_DATABASE_URL and TURSO_AUTH_TOKEN to your Render environment variables. Without them, the app falls back to local SQLite.
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000The API will be available at http://localhost:8000
cd cloudflare-worker
npm install -g wrangler
wrangler login
# Create the KV namespaces
wrangler kv namespace create FAILED_UPDATES
wrangler kv namespace create CREDENTIALS
# Update wrangler.toml with the returned KV IDs, then:
wrangler deploySet secrets (not stored in wrangler.toml):
wrangler secret put RENDER_URL # Your Render service URL
wrangler secret put API_KEY # Same as API_SECRET_KEY
wrangler secret put TELEGRAM_OWNER_ID # Your numeric Telegram user ID
wrangler secret put TELEGRAM_BOT_TOKEN # Your bot token- Create a new Web Service on Render.com
- Connect your GitHub repository
- Configure the service:
- Environment: Python 3
- Build Command:
pip install -r requirements.txt - Start Command:
uvicorn app.main:app --host 0.0.0.0 --port $PORT
- Add all environment variables from
.envto Render's Environment Variables - Deploy
After deploying, point the Telegram webhook to your CF Worker:
curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook?url=<YOUR_CLOUDFLARE_WORKER_URL>"Once the bot is running, send these commands via Telegram:
| Command | Description |
|---|---|
/setcred <platform> <key> <value> |
Save a platform credential |
/delcred <platform> <key> |
Delete a credential |
/getcreds |
List configured platforms & missing fields |
/status |
Show server & queue status |
/help |
Show all commands |
Example — setting up Bluesky:
/setcred bluesky handle your-handle.bsky.social
/setcred bluesky password xxxx-xxxx-xxxx-xxxx
Credentials are stored in Cloudflare KV (persistent, encrypted at rest by Cloudflare). Environment variables in .env or Render always take precedence over KV values.
- Create a bot: @BotFather
- Get your API credentials: my.telegram.org
- Create a channel and add your bot as an administrator
- Create an account at bsky.app
- Generate an app password: Settings → App Passwords → Add App Password
- Create an account on any Mastodon instance
- Go to Settings → Development → New Application
- Grant required permissions and create the app
- Copy the access token
- Convert your Instagram account to a Professional Account (if not already):
- Go to Settings → Account → Switch to Professional Account
- Create a Facebook Page and link it to your Instagram Professional Account
- Set up a Facebook App in Facebook Developers:
- Create a new app → Business type
- Add Instagram Graph API product
- Configure Instagram Basic Display
- Generate a long-lived access token for the Instagram Graph API
- Get your Instagram Business Account ID
Official Meta API Integration (Safe & Recommended)
- Create a Meta Developer app at developers.facebook.com
- Add Threads API product to your app
- Generate a long-lived access token with
threads_basicandthreads_content_publishpermissions - Get your Threads User ID
- Sign up → https://cloudinary.com/
- Dashboard → Copy credentials
- Add to
.env:
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_key
CLOUDINARY_API_SECRET=your_secretRate Limits: 250 posts per 24 hours, 500 character limit per post
- Apply for developer access: developer.twitter.com
- Create a new app and generate API keys
- Ensure you have read/write permissions
- Create an app: reddit.com/prefs/apps
- Select "script" as the app type
- Copy the client ID and secret
- Create a project in Google Cloud Console
- Enable YouTube Data API v3
- Create OAuth 2.0 credentials
- Download the client configuration
- Send a photo, video, or document to your Telegram bot
- Include a caption (optional) — this will be the post text
- The bot will queue the post for all configured platforms
- The cron trigger processes one queued post every 5 hours
POST /webhook- Receives webhooks from Cloudflare WorkerPOST /process-queue- Process the oldest pending job (cron-triggered)GET /health- Health check endpointGET /queue- View current job queueDELETE /queue/{job_id}- Cancel a pending job
id- Unique job identifierstatus- Job status (pending, processing, completed, failed)platform- Target platformmedia_info- Serialised media info JSONscheduled_time- When to publishcreated_at- Job creation timestampattempts- Number of retry attemptserror_log- Error messages from failed attemptsfile_id- Telegram file ID (for cleanup tracking)post_url- URL of the published post
Edit app/config.py to customize:
- Post delay duration
- Retry attempts
- Enabled platforms
- Media file size limits
forwardr/
├── app/ # Main application code
│ ├── api/ # API endpoints
│ ├── models/ # Database models
│ ├── services/ # Business logic
│ │ └── platforms/ # Platform-specific integrations
│ ├── utils/ # Utility functions
│ └── workers/ # Background workers
├── cloudflare-worker/ # Cloudflare Worker code
├── scripts/ # Database and utility scripts
├── tests/ # Test files
├── .env.example # Environment variables template
├── requirements.txt # Python dependencies
└── README.md # This file
any sort of help is welcome :)
MIT License - feel free to use this project for personal or commercial purposes.