- Python 64.9%
- HTML 35.1%
|
|
||
|---|---|---|
| app | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| compose.yaml | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
poster
A self-hosted web interface for composing and publishing posts to multiple platforms simultaneously. Replaces auto-crossposters with intentional, per-post control over where content goes.
Supported platforms
| Platform | Notes |
|---|---|
| GoToSocial | Mastodon-compatible API; supports visibility, content warnings, polls, media |
| Bluesky | AT Protocol; long posts split into threads automatically (300 grapheme limit) |
| Station | Gemini protocol; media embedded as Gemini link lines using GTS-hosted URLs |
Platforms are enabled by presence of their credentials in .env. Any combination works.
Features
- Compose and post to one or more platforms in a single action
- Per-platform visibility and content warning options
- Image/video attachments with alt text (up to 10 on Bluesky)
- Poll builder (GoToSocial only; other platforms skipped automatically when a poll is set)
- Save drafts, schedule posts, edit and delete sent posts
- Post history with per-platform status
- REST API for programmatic use (
/api/v1/) - Mobile-first responsive UI (works on iPhone, iPad, desktop)
- Dark/light theme toggle
Stack
- Python 3.14, FastAPI, Jinja2, HTMX 2, Pico CSS v2
- SQLite (WAL mode) for drafts, history, and results
- uv for dependency management
- No build step — pure server-side rendering with HTMX partials
Setup
1. Configure credentials
Copy .env.example to .env and fill in the platforms you want to use:
# GoToSocial — token needs read + write scope
GTS_BASE_URL=https://your.gotosocial.instance
GTS_ACCESS_TOKEN=your_token
# Bluesky
BSKY_HANDLE=you.bsky.social
BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
# Station (Gemini) — paths inside the container
STATION_CERT_PATH=/app/data/station.pem
STATION_KEY_PATH=/app/data/station.key
# Optional: public base URL used as fallback for media links when GTS is not selected
BASE_URL=https://poster.example.com
# Storage and logging
DATA_DIR=/app/data
LOG_LEVEL=INFO
2. Run with Docker Compose
docker compose up -d
The app listens on port 8000.
⚠️ No authentication is built in. Anyone who can reach the app can post, edit, and delete content on your behalf. Always place it behind a reverse proxy (nginx, Caddy, etc.) that enforces access control — e.g. HTTP basic auth, IP allowlist, or a VPN — and never expose it directly to the internet.
3. Run locally (development)
uv run uvicorn app.main:app --reload
REST API
The API lives at /api/v1/. Interactive docs at /api/docs.
POST /api/v1/posts # Post immediately
POST /api/v1/drafts # Save as draft
GET /api/v1/drafts # List drafts + scheduled
PUT /api/v1/drafts/{id} # Update draft
DELETE /api/v1/drafts/{id} # Delete draft
POST /api/v1/drafts/{id}/send # Send a draft now
GET /api/v1/posts # Post history
GET /api/v1/posts/{id} # Single post + results
PUT /api/v1/posts/{id} # Edit a sent post
DELETE /api/v1/posts/{id} # Delete from all platforms
POST /api/v1/media # Upload a media file
GET /api/v1/platforms # List configured platforms
Data
All persistent data lives in data/ (gitignored):
data/poster.db— SQLite databasedata/uploads/— uploaded media filesdata/station.pem/data/station.key— Station TLS client cert (if using Station)