# Open-Dispatch — adapter credentials # Copy to .env and fill what you actually use. The API only complains when a target # references a platform whose env vars aren't set. # # Account-scoped credentials: target "twitter:work" → TWITTER_ACCESS_TOKEN_WORK. # ── Storage layout ──────────────────────────────────────────────────────── # OPEN_DISPATCH_DATA=data # queue, profiles.json, media root # OPEN_DISPATCH_MEDIA_DIR= # confine all media reads/writes to # # this directory. Strongly recommended: # # when unset, media paths are NOT # # restricted to a root directory. # ── API access control (read before exposing the port) ───────────────────── # When set, every route except /healthz requires an Authorization header with a bearer token. # When unset, the API is fully open. The Docker image binds 0.0.0.0:8000, so # set this to anything reachable from outside your machine. # Generate with: openssl rand -hex 32 # OPEN_DISPATCH_API_TOKEN= # ── Telegram ─────────────────────────────────────────────────────────────── TELEGRAM_BOT_TOKEN= TELEGRAM_CHAT_ID= # TELEGRAM_CHAT_ID_BROADCAST= # ── Twitter / X ──────────────────────────────────────────────────────────── TWITTER_API_KEY= TWITTER_API_SECRET= TWITTER_ACCESS_TOKEN= TWITTER_ACCESS_SECRET= # ── Instagram (Graph API; needs public media URLs) ───────────────────────── IG_USER_ID= IG_TOKEN= # ── Bluesky (use App Password, not your main login password) ─────────────── BLUESKY_HANDLE= BLUESKY_APP_PASSWORD= # ── LinkedIn (3-legged OAuth token with w_member_social) ─────────────────── LINKEDIN_ACCESS_TOKEN= LINKEDIN_AUTHOR_URN= # ── Threads (Meta Threads Graph API) ─────────────────────────────────────── # Long-lived token with threads_basic + threads_content_publish scopes. # Get via https://developers.facebook.com/docs/threads/getting-started THREADS_USER_ID= THREADS_ACCESS_TOKEN= # THREADS_SETTLE_SECONDS=30 # delay before publishing the created container # ── YouTube Shorts (Data API v3, resumable upload) ───────────────────────── # Setup: # 1. console.cloud.google.com → enable "YouTube Data API v3" # 2. Create OAuth Client (Desktop or Web) → get YOUTUBE_CLIENT_ID/SECRET # 3. Run an OAuth flow with scope https://www.googleapis.com/auth/youtube.upload # to obtain a refresh_token → YOUTUBE_REFRESH_TOKEN YOUTUBE_CLIENT_ID= YOUTUBE_CLIENT_SECRET= YOUTUBE_REFRESH_TOKEN= # ── Worker tuning ────────────────────────────────────────────────────────── WORKER_POLL_INTERVAL=5 # How many rows one worker publishes in parallel (bounded concurrency). # Claims stay atomic per backend, so rows are never double-published, but # each concurrent slot multiplies load on the platform API — keep this at or # below what your platform rate limits allow (default 1 = old serial behaviour). WORKER_CONCURRENCY=1 WORKER_MAX_ATTEMPTS=3 WORKER_BACKOFF_BASE=30 # Re-queue rows stuck in "publishing" longer than this (hard-crash recovery). WORKER_PUBLISHING_LEASE=900 # ── Data dir (queue JSONL lives here when REDIS_URL is unset) ────────────── # Do NOT set this here! It would override the location the wrappers and # Docker already choose (install.sh: ~/.open-dispatch; Docker: /data), and # compose would silently split api and worker onto different queues. # Set it in your shell/service environment only if you want a custom path. # ── Queue backend (optional — opt in for multi-worker setups) ───────────── # Priority: DATABASE_URL > REDIS_URL > JSONL fallback. # Same API across all three — set one of these and the worker just works. # # Redis: fast, simple, single-region multi-worker # Spin up with: `docker compose --profile redis up -d` # Bare metal / host install: redis://localhost:6379/0 # Inside docker compose: redis://redis:6379/0 (service name, not localhost) # REDIS_URL=redis://localhost:6379/0 # # Postgres: cross-region multi-worker with proper ACID + SKIP LOCKED claims # `pip install psycopg2-binary` then set: # DATABASE_URL=postgresql://user:pass@host:5432/dispatch # ── AI caption adapter (optional) ────────────────────────────────────────── # Rewrites one source caption into platform-native posts (Twitter punchy, # LinkedIn formal, Instagram hashtags, etc). If neither key is set, falls # back to a heuristic adapter that trims to limits — no LLM needed. # # Prefers Ollama when OLLAMA_HOST is set (free, local), else OpenRouter. OPENROUTER_API_KEY= OPENROUTER_MODEL=openai/gpt-4o-mini OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # OPENROUTER_REFERER= # optional Referer header for some OpenRouter routes # OLLAMA_HOST=http://localhost:11434 # OLLAMA_MODEL=qwen2.5:latest # AI_CAPTION_TEMPERATURE=0.4 # AI_CAPTION_TIMEOUT_SECONDS=30 # ── TikTok (Content Posting API v2) ──────────────────────────────────────── # Apply at developers.tiktok.com — needs scope: video.publish # Format: tiktok_post { video_url, caption, privacy } TIKTOK_ACCESS_TOKEN= # TIKTOK_PRIVACY_LEVEL=PUBLIC_TO_EVERYONE # or MUTUAL_FOLLOW_FRIENDS / SELF_ONLY # ── Facebook (Meta Graph API v19, Page token) ─────────────────────────────── # Create a Facebook App → Pages API → generate a Page access token # Format: facebook_post { text, image_url?, video_url?, link? } FACEBOOK_PAGE_ID= FACEBOOK_ACCESS_TOKEN= # ── Discord (Webhook — no bot required) ──────────────────────────────────── # Server → Channel settings → Integrations → Webhooks → Copy Webhook URL # Format: discord_message { content, username?, avatar_url?, embeds? } DISCORD_WEBHOOK_URL= # DISCORD_WEBHOOK_URL_ANNOUNCE= # per-account webhooks: discord:announce # ── Testing (optional) ───────────────────────────────────────────────────── # DSN for the live-PostgreSQL integration test: tests/test_postgres_integration.py # OPEN_DISPATCH_TEST_POSTGRES_DSN=host=127.0.0.1 port=5499 user=postgres dbname=od_audit