A self-hosted web interface for composing and publishing posts to multiple platforms simultaneously.
  • CSS 95.7%
  • Python 2.8%
  • HTML 1.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Erick Ruiz de Chavez 896616f1dd Fix CRLF line endings breaking Bluesky splits and Station remove URL extraction
HTML form textareas submit \r\n line endings, causing split_post to miss
paragraph breaks (it searched for \n\n only) and falling back to mid-sentence
splits. Station's remove URL fallback also relied on a "now" timestamp that
Station no longer emits (it uses "X seconds ago" instead).

- Normalize \r\n→\n at form input time (compose, drafts) and defensively in
  split_post and _build_gemtext for existing DB data
- Replace the dead "now" timestamp fallback in _extract_root_remove_url with a
  feed-post pattern: Station now redirects to /username after posting, so the
  first =>/username/[32-char-hex] link in the profile body is the just-created post

Assisted-By: Claude <noreply@anthropic.com>
2026-07-02 11:11:54 +00:00
app Fix CRLF line endings breaking Bluesky splits and Station remove URL extraction 2026-07-02 11:11:54 +00:00
.env.example Initial implementation of multi-platform posting web app 2026-06-24 23:20:02 +00:00
.gitignore Initial implementation of multi-platform posting web app 2026-06-24 23:20:02 +00:00
CLAUDE.md Add README.md and CLAUDE.md 2026-06-25 10:12:28 +00:00
compose.yaml Initial implementation of multi-platform posting web app 2026-06-24 23:20:02 +00:00
pyproject.toml Initial implementation of multi-platform posting web app 2026-06-24 23:20:02 +00:00
README.md README: make the no-auth warning more prominent 2026-06-25 10:14:59 +00:00
uv.lock Initial implementation of multi-platform posting web app 2026-06-24 23:20:02 +00:00

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 database
  • data/uploads/ — uploaded media files
  • data/station.pem / data/station.key — Station TLS client cert (if using Station)