DEV Community

Cover image for Markette: an open-source, self-hosted tool to write once and publish everywhere
Marco Montanari
Marco Montanari

Posted on Originally published at markette.org

Markette: an open-source, self-hosted tool to write once and publish everywhere

I publish to a lot of places: Bluesky, Mastodon, X, LinkedIn, a couple of Telegram channels, a blog. I also help a few organizations that do the same, each with its own accounts and its own people. The existing tools were either SaaS with per-channel pricing, or self-hosted but limited to a handful of networks with a scheduler that was little more than a cron job.

So I built Markette: write a post once, adapt it for each network, schedule it at the right moment, and see what worked. It is open source (AGPL-3.0) and you can run it on your own server with one docker compose up.

This article is a tour of what it does and how it is built.

What it publishes to

24 destinations, behind a single provider contract:

  • Social: X, Bluesky, Threads, Mastodon (any instance), LinkedIn (people and company pages), Facebook Pages, Instagram, YouTube, Reddit
  • Messaging: Telegram, Slack, Discord, WhatsApp, WeChat, QQ
  • Blogs and sites: WordPress, Drupal, Ghost, Medium, DEV (and any Forem community), Hugo (it commits a page bundle to your git repo)
  • Newsletters: Brevo and Listmonk, where each post becomes an email campaign
  • Anything else: a signed JSON webhook, for n8n, Make, Zapier or your own code

Yes, this article was written in Markette and published through its DEV integration.

Write once, tailor per channel

A post fans out into one target per channel. Every target can override the text, media and options: a short version for X, a longer one with a first comment for LinkedIn, a title and tags for the blog. Character counters count the way each network does (X weighs links as 23 characters, Bluesky counts graphemes), and every target is validated against its network's rules before anything is scheduled.

A scheduler you can trust

Scheduling is the part I cared about most. Each target gets its own time, chosen by one of these modes:

  • now or an exact time
  • queue: the next free weekly slot
  • best time: the best-scoring free hour for that channel, keeping a minimum gap from other posts

A Celery beat process ticks every 15 seconds and claims due targets with SELECT … FOR UPDATE SKIP LOCKED, so several workers never publish the same thing twice. Errors are classified as retryable (rate limits, 5xx, network: exponential back-off, honouring Retry-After), auth (the channel is flagged for reconnection) or permanent. A worker that dies mid-publish leaves the target as "failed, check before retrying" instead of retrying blindly and risking a duplicate post.

Best time to post, from your own data

The best-time engine keeps a 7×24 grid (weekday × hour) of scores for every channel. It starts from a per-platform prior, so a new channel gets sensible defaults, then blends in the engagement rate of your own posts from the last 180 days. The more posts land in an hour, the more your data wins over the prior. The UI tells you whether a grid comes from defaults, a blend, or your data alone.

Metrics are pulled 1 h, 6 h, 24 h, 3 d, 7 d and 30 d after publishing, plus a daily audience count.

Optional AI, with any provider

AI is optional and goes through OpenRouter or any OpenAI-compatible API (OpenAI, Azure, vLLM, Ollama…). It can suggest times (checked against the engine and the calendar), plan a campaign over a period, and rewrite one text for each network's length and tone. Every call is logged with its tokens and cost, and organizations can have monthly limits.

Built for teams and agencies

  • Organizations contain workspaces, one per brand or client, with roles from owner to viewer
  • Channels belong to the organization and can be shared across workspaces
  • Teams can restrict channels and require approval before posts are scheduled
  • Single sign-on with any OpenID Connect provider, per organization
  • Optional Stripe billing if you host it for others

More ways in

  • MCP server: connect Claude, or any MCP client, and ask it to draft, validate and schedule posts. Every call runs as you, with your roles.
  • Chrome extension: share the page, a selection, a link or an image straight to the composer.
  • Android and iOS apps, with a share sheet.
  • A documented REST API with personal tokens.

The stack

  • Backend: Django 6 + Django REST Framework, Celery, PostgreSQL, Redis
  • Frontend: Angular 21, served by nginx or GitHub Pages
  • Mobile: Capacitor
  • Channel credentials are encrypted at rest (Fernet), and secrets are write-only in the API

Try it

git clone https://github.com/markette-org/markette-deploy.git
cd markette-deploy && cp .env.example .env   # set the keys and your domain
docker compose up -d
docker compose exec api python manage.py createsuperuser
Enter fullscreen mode Exit fullscreen mode

The self-hosting guide explains where to find the keys for every network.

Issues, pull requests and new providers are very welcome. I'd love to hear which network you'd want next, and how you handle publishing to many places today.

Top comments (3)

Collapse
 
launchgatecheck profile image
Launch Gate •

The worker-death "check before retrying" state is a useful boundary. I'd test the less obvious version too: the provider creates the post, but its response is lost and the worker remains alive. Does that network error become a retryable target, or an unknown-delivery state that needs reconciliation? Row locking prevents two workers claiming one target, but doesn't resolve whether the external post landed. A provider-specific fixture with a lost response after creation would make that contract clear. For a multi-channel post, I'd also keep confirmed targets untouched while only the uncertain target is reviewed. Article-based suggestions, not results from running Markette.

Collapse
 
sirmmo profile image
Marco Montanari •

thank you or the idea, will look into it!

Collapse
 
sirmmo profile image
Marco Montanari •

done :D