User’s guide

ActivityPub

Using microblog.pub efficiently requires knowing a bit about how ActivityPub works.

Skimming over the Overview section of the ActivityPub specification should be enough.

Also, you should know that the Fediverse is a common name used to describe all the interconnected/federated instances of servers supporting ActivityPub (like Mastodon, Pleroma, PeerTube, PixelFed…).

Mastodon client apps

Besides the built-in web UI, you can also use your instance from any Mastodon client app (Tusky, Fedilab, Ivory, the official Mastodon app…): enter your domain where the app asks for an instance, then log in with your admin password. See the dedicated Mastodon client apps page for what’s supported, what isn’t, and troubleshooting tips.

Configuration

Profile

You initial profile configuration is generated via the setup wizard.

You can manually edit the configuration file stored in data/profile.toml (TOML), note that the following config items cannot be updated (without breaking federation):

  • domain

  • username

As these two config items define your ActivityPub handle @handle@domain.

You can tweak your profile by tweaking these items:

  • name: The name shown with your profile.

  • summary: The summary or ‘bio’ part of your profile, written in Markdown.

  • icon_url: Your profile image or avatar.

  • image_url: This provides a ‘header’ or ‘banner’ image. Note that it is not shown by the default Microblog.pub templates. It will be used by Mastodon (which uses a 3:1 ratio image) and Pleroma. Pixelfed and Peertube, for example, don’t show these images by default.

Whenever one of these config items is updated, an Update activity will be sent to all known servers to update your remote profile.

The server will need to be restarted for taking changes into account.

Before restarting the server, you can ensure you haven’t made any mistakes by running the configuration checking task.

Note that currently image_url is not used anywhere in microblog.pub itself, but other clients/servers do occasionally use it when showing remote profiles as a background image. Also, this image can be used in microblog.pub - just add this:

<img src="{{ local_actor.image_url | media_proxy_url }}">

to an appropriate place of your template (most likely, header.html). For more information, see a section about custom templates further in this document.

Local avatar/header files

If you’d rather not host your avatar/header elsewhere and reference it with an absolute URL, you can drop image files directly into the instance and let it serve them. The avatar and header are resolved in this priority order:

  1. icon_url / image_url in data/profile.toml — an explicit URL here always wins. If set, the files below are ignored.

  2. data/avatar.jpg / data/profile.image.jpg — your own files, kept with the rest of your mutable data (and therefore preserved across upgrades and Docker image rebuilds).

  3. app/static/avatar.jpg / app/static/profile.image.jpg — packaged defaults shipped with the instance; a data/ file of the same name overrides them.

When one of these files is used, it is served from /img/avatar.jpg (or /img/profile.image.jpg) on your own domain, and that URL is what gets advertised as your icon/image over ActivityPub and as the og:image meta tag. Only those two exact filenames are served this way. As with any profile change, restart the server for it to take effect (an Update activity is then federated to known servers).

Profile metadata

You can add metadata to your profile with the metadata config item.

Markdown is supported in the value field.

Be aware that most other software like Mastodon will limit the number of key/value to 4.

metadata = [
  {key = "Documentation", value = "[https://docs.microblog.pub](https://docs.microblog.pub)"},
  {key = "Source code", value = "[https://github.com/toniher/microblog.pub](https://github.com/toniher/microblog.pub)"},
]

Discoverability

These two profile.toml items are published on your actor, so remote servers know what you want done with your profile and posts. Both default to true, matching what the public web profile and the shipped robots.txt already allow:

  • discoverable — may other servers list your profile in directories, suggestions and “who to follow” features?

  • indexable — may they include your posts in full-text search?

discoverable = false
indexable = false

They are requests, not enforcement: a remote server is free to ignore them, and nothing here stops a public page from being read. Set them to say what you want; keep posts non-public if you need it guaranteed. Both are also reported to Mastodon clients (as discoverable and noindex on the account), and microblog.pub honours the same properties on remote actors when they publish them.

Followers and following

These profile.toml items control how followers are handled and what’s shown publicly (all default to false):

  • manually_approves_followers — require manual approval of new followers.

  • hides_followers — hide your followers list.

  • hides_following — hide who you are following.

manually_approves_followers = true
hides_followers = true
hides_following = true

Quote posts

When a remote server asks to quote one of your posts (FEP-044f), the quote_policy config item controls how the request is handled:

quote_policy = "public"
  • "public" (the default) — auto-accept a request targeting a public or unlisted post, reject anything else.

  • "followers" — like "public", but the requester must also be one of your followers.

  • "manual" — every request surfaces as a notification with accept/reject buttons, like an incoming follow request. This one can’t be turned off via disabled_notifications, for the same reason a pending follow request can’t be: it’s the only place to act on it.

  • "nobody" — always reject.

Quoting someone else’s post from here always sends that server a request first (except quoting your own post, which needs no request); the status stays pending until they respond. The handshake is FEP-044f — a QuoteRequest the other server answers with an Accept (carrying an authorization stamp) or a Reject. That stamp matters beyond the pending state: Mastodon 4.5 and later refuse to display a quote that doesn’t carry one.

A stamp you’ve granted for one of your own posts can be revoked from /admin — a revoke quote button next to the quoting post and next to its notification. That sends the quoting server a Delete of the stamp and stops the quote rendering here immediately, but reaching everyone who already saw the quote depends on that server forwarding the revocation to its own audience, as the FEP describes.

URL aliases

Every post is reachable at an opaque permalink like /o/ef0166f89a124e7d9d3d6305cfa5597a. From a post’s admin edit page, you can give it a human-readable alias instead — set it and the post becomes canonically reachable at /post/my-first-note; the old /o/… link keeps working, redirecting (301) to the alias. Clearing the alias field restores the original URL. This is local-only: changing only the alias does not federate an Update to followers.

Whatever you type is normalized: lowercased, spaces collapsed into hyphens, and anything that isn’t a letter, digit, hyphen or underscore is stripped — so My First Note! becomes my-first-note. Prefer hyphens over underscores for word separation (my-first-note, not my_first_note); hyphens are what the normalization produces automatically and are the convention used everywhere else in the app’s own URLs. Aliases are capped at 200 characters and must be unique across all posts, including deleted ones.

The path prefix (post by default) is configurable via the alias_url_prefix config item in profile.toml:

alias_url_prefix = "blog"

It must not collide with an existing top-level route (o, articles, admin, static, .well-known, and similar are rejected at startup). Treat it as a set-once setting: changing it after posts have already been aliased and federated invalidates the url already sent to remote servers for those posts (local rendering picks up the new prefix immediately, and /o/{public_id} — the ActivityPub id, which never changes — keeps resolving either way).

Default language

You can set a default language for your instance with the language_code config item in profile.toml, using a BCP 47 tag (e.g. en, fr, pt-BR):

language_code = "en"

The default value is en. It is used for the lang attribute of the generated HTML pages (which helps screen readers and translation tools) and as the suggested default when setting a per-note language.

It also acts as the fallback language the interface (buttons, labels, messages) is shown in: both the admin UI (/admin) and public pages use a visitor’s browser language when a translation is available for it, falling back to language_code otherwise (e.g. no Accept-Language header sent, or none of the requested languages are available). See the developer guide for how translations are added.

Schema.org microdata

By default, the generated HTML only carries microformats2 markup. You can additionally enable schema.org structured data (itemscope/itemtype/itemprop microdata attributes) with:

enable_microdata = true

The default value is false. When enabled, feeds are marked up as a Blog, and each note/article/video/event as the corresponding schema.org type (BlogPosting, SocialMediaPosting, VideoObject, Event…), with author, datePublished, url and media properties filled in alongside the existing microformats2 classes. This can help search engines and other tools that read schema.org structured data (e.g. rich results) understand your content.

Privacy replace

You can define domains to be rewritten to more “privacy friendly” alternatives, like Invidious or Nitter.

To do so, add these extra config items. This is a sample config that rewrite URLs for Twitter, Youtube, Reddit and Medium:

privacy_replace = [
    {domain = "youtube.com", replace_by  = "yewtu.be"},
    {domain = "youtu.be", replace_by  = "yewtu.be"},
    {domain = "twitter.com", replace_by = "nitter.fdn.fr"},
    {domain = "medium.com", replace_by = "scribe.rip"},
    {domain = "reddit.com", replace_by = "teddit.net"},
]

Disabling certain notification types

All notifications are enabled by default.

You can disabled specific notifications by adding them to the disabled_notifications list.

This example disables likes and shares notifications:

disabled_notifications = ["like", "announce"]

Available notification types

  • new_follower

  • rejected_follower

  • unfollow

  • follow_request_accepted

  • follow_request_rejected

  • move

  • like

  • undo_like

  • announce

  • undo_announce

  • mention

  • new_webmention

  • updated_webmention

  • deleted_webmention

  • blocked

  • unblocked

  • block

  • unblock

  • reported

  • quote

  • status

  • update

  • poll

Outgoing HTTP connections

Requests the server makes to other instances — fetching ActivityPub objects, delivering activities, webfinger lookups, and proxying remote media — go through shared HTTP clients that keep connections alive and reuse them, negotiating HTTP/2 where the remote end supports it.

This is enabled by default and needs no configuration. All four settings below are optional; omit them to keep the default behaviour.

http_client_pooling = true
http_client_http2 = true
http_client_max_connections = 100
http_client_max_keepalive_connections = 20
  • http_client_pooling — when false, no connection is kept open past the response that used it, so every request opens a fresh one. This also disables HTTP/2, since running several requests over a single multiplexed connection is a form of connection sharing as well.

  • http_client_http2 — when false, connections are still pooled and reused, but only HTTP/1.1 is negotiated. Has no effect if http_client_pooling is false.

  • http_client_max_connections — upper bound on connections open at once, across all remote hosts.

  • http_client_max_keepalive_connections — how many idle connections are kept around for reuse.

One visible side effect of pooling: because a page’s remote images usually come from a single host, they are now fetched over one shared connection and tend to finish together, rather than trickling in one by one as they did when every request opened its own connection. The page finishes loading sooner, but it renders in one go instead of progressively. Setting http_client_pooling = false restores the old behaviour at the cost of re-establishing a connection for every request.

Outgoing federation delivery

Activities queued for delivery (to followers, mentioned actors, webmention targets…) are sent out in batches, with several deliveries in flight at once instead of one at a time.

This is enabled by default and needs no configuration. All three settings below are optional; omit them to keep the default behaviour.

outgoing_delivery_batch_size = 20
outgoing_delivery_concurrency = 10
outgoing_delivery_per_host_concurrency = 2
  • outgoing_delivery_batch_size — how many due activities are fetched per poll.

  • outgoing_delivery_concurrency — how many deliveries run at once, across all recipients. Must not exceed http_client_max_connections.

  • outgoing_delivery_per_host_concurrency — how many deliveries run at once against a single remote host. Must not exceed outgoing_delivery_concurrency.

Delivery order to a given recipient inbox is always preserved regardless of these values — activities queued for the same inbox are still delivered one at a time, in order, so a boost can never overtake the post it boosts, for example. Only deliveries to different recipients run concurrently. Setting outgoing_delivery_concurrency = 1 restores fully serial delivery, one activity at a time.

Upload size limits

All settings below are optional; omit them to keep the defaults, which match what’s advertised to Mastodon clients out of the box.

max_image_upload_size = 10485760  # 10 MiB
max_video_upload_size = 41943040  # 40 MiB
max_image_pixels = 16777216       # 16 MP
  • max_image_upload_size — the largest an image upload may be, in bytes.

  • max_video_upload_size — the largest a video or audio upload may be, in bytes (audio shares this limit rather than getting a third setting, matching Mastodon).

  • max_image_pixels — the largest an image upload may be in pixels (width × height).

The pixel cap is not redundant with the byte cap: a 7 MB JPEG straight off a phone decodes to a 12 megapixel bitmap, and EXIF-stripping and thumbnailing both cost memory and CPU in proportion to pixel count, not file size. The limit is checked from the image header, before any pixel is decoded, so an oversized image costs nothing to reject.

A file over its limit is rejected before any of it is written to disk. If you’re behind a reverse proxy, make sure its own body-size cap (e.g. nginx’s client_max_body_size, see Reverse proxy) is at least as large, or it will reject a large-but-valid upload before the app ever sees it.

Web Push delivery

Mastodon client apps that support push notifications (see Mastodon client apps) are served by a separate push_worker process. Both settings below are optional; omit them to keep the defaults.

vapid_key_path = "data/vapid_key.pem"
push_delivery_batch_size = 10
  • vapid_key_path — where the instance’s VAPID (RFC 8292) keypair lives. Generated automatically on first use; back it up along with the rest of data/ — regenerating it invalidates every existing subscription, and clients must re-subscribe.

  • push_delivery_batch_size — how many subscriptions with pending notifications are fetched per poll by the push_worker process.

Streaming API

Mastodon client apps that support live timeline/notification updates (see Mastodon client apps) connect over wss://…/api/v1/streaming. Unlike Web Push, this needs no separate process — see the linked section for why. All settings below are optional; omit them to keep the defaults.

streaming_enabled = true
streaming_poll_interval = 1.0
streaming_max_connections = 32
  • streaming_enabled — set to false to disable the endpoint entirely; this also removes the streaming_api/configuration.urls.streaming advertisement, so clients never attempt a connection that would just fail.

  • streaming_poll_interval — how often (seconds) the in-process pump checks for new activity while at least one client is connected. Lower is more responsive and costs a few more cheap queries per second; has no cost at all when no client is connected.

  • streaming_max_connections — a cap on concurrent streaming sockets, since public/hashtag streams can be reached without authentication.

Customization

Default emoji

If you don’t like cats, or need more emoji, you can add your favorite emoji in profile.toml and it will replace the default ones:

emoji = "🙂🐹📌"

You can copy/paste them from getemoji.com.

Custom emoji

You can add custom emoji in the data/custom_emoji directory and they will be picked automatically. Do not use exotic characters in filename - only letters, numbers, and underscore symbol _ are allowed.

Custom CSS

The CSS is written with SCSS.

You can override colors and the font by editing data/_theme.scss:

$primary-color: #e14eea;                                                                            
$secondary-color: #32cd32;

The following variables are available to override (see app/scss/main.scss for the authoritative list and their default values):

Variable

Purpose

$font-stack

Font family used across the site

$background

Page background

$light-background

Alternate/secondary background (e.g. cards)

$text-color

Main body text

$primary-color

Primary accent (links, default favicon)

$secondary-color

Secondary accent

$muted-color

Muted/secondary text (e.g. timestamps)

$form-background-color

Form field background

$form-text-color

Form field text

$nav-button-background-color

Navigation button background

$nav-button-text-color

Navigation button text

$primary-button-text-color

Text on primary buttons

$code-highlight-background

Background behind highlighted code blocks

Only set the variables you want to change — anything you leave out keeps its default. If you need to go further than variables, you can add arbitrary SCSS after the variable definitions in data/_theme.scss.

The palette is exposed as CSS custom properties, so the site automatically follows the visitor’s system light/dark preference (prefers-color-scheme). Every variable above has a dark-mode counterpart prefixed with $dark- (e.g. $dark-background, $dark-text-color, $dark-primary-color) that you can override the same way. Leave them unset to keep the built-in dark theme:

$dark-background: #101418;
$dark-primary-color: #6fd06f;

You will need to recompile CSS after doing any CSS changes (for actual css files to be updates) and restart microblog.pub (for css link in HTML documents to be updated with a new checksum - otherwise, browsers that downloaded old CSS will keep using it).

Custom favicon

By default, microblog.pub favicon is a square of $primary-color CSS color (see above section on how to redefine CSS colors). You can change it to any icon you like - just save a desired file as data/favicon.ico. It is served the same way as the avatar/header images: a data/favicon.ico overrides the packaged app/static/favicon.ico default (the themed square) at serve time, so you only need to restart microblog.pub for it to take effect — no recompile step required. As with your data, it is preserved across upgrades and Docker image rebuilds.

Web analytics

You can add a self-hosted analytics tool such as Matomo, Plausible, GoatCounter or umami by pasting the provider’s tracking snippet into the analytics_html config item in profile.toml:

analytics_html = '''
<script>
  var _paq = window._paq = window._paq || [];
  _paq.push(['trackPageView']);
  _paq.push(['enableLinkTracking']);
  (function() {
    var u = "https://analytics.example.com/";
    _paq.push(['setTrackerUrl', u + 'matomo.php']);
    _paq.push(['setSiteId', '1']);
    var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
    g.async = true; g.src = u + 'matomo.js'; s.parentNode.insertBefore(g, s);
  })();
</script>
'''

The snippet is inserted verbatim (as raw HTML) in the <head> of public pages only — it is never shown on /admin pages, and never shown to you when you’re browsing your own instance logged in, so your own visits aren’t tracked.

By default the Content Security Policy only allows resources from your own domain (default-src 'self'), which will block a script loaded from another host. If your analytics provider is hosted on a different domain than your instance, you also need to set custom_content_security_policy to allow it, e.g.:

custom_content_security_policy = "default-src 'self'; style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com; img-src 'self' https://analytics.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"

Custom templates

If you’d like to customize your instance’s theme beyond CSS, you can modify the app’s HTML by placing templates in data/templates which overwrite the defaults in app/templates.

A template placed in data/templates/<name>.html fully replaces app/templates/<name>.html. The ones you are most likely to want to override are:

  • header.html — the site header shown at the top of every public page (name, avatar, navigation)

  • layout.html — the overall page skeleton (<head>, footer, common markup)

  • index.html — the public homepage

  • utils.html — shared macros (see the per-macro override trick below)

Templates are written using Jinja templating language. Moreover, utils.html has scoped blocks around the body of every macro. This allows macros to be overridden individually in data/templates/utils.html, without copying the whole file. For example, to only override the display of a specific actor’s name/icon, you can create data/templates/utils.html file with following content:

{% raw %}

{% extends "app/utils.html" %}
{% block display_actor %}
	{% if actor.ap_id == "https://me.example.com" %}
		<!-- custom actor display -->
	{% else %}
		{{ super() }}
	{% endif %}
{% endblock %}

{% endraw %}

Custom Content Security Policy (CSP)

You can override the default Content Security Policy by adding a line in data/profile.toml:

custom_content_security_policy = "default-src 'self'; style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"

This example will output the default CSP, note that {HIGHLIGHT_CSS_HASH} will be dynamically replaced by the correct value (the hash of the CSS needed for syntax highlighting).

Code highlighting theme

You can switch to one of the styles supported by Pygments by adding a line in data/profile.toml:

code_highlighting_theme = "solarized-dark"

Custom routes

Custom routes can be added in data/custom_routes.py (the file is imported automatically at startup if it exists).

The simplest option is to serve a static HTML page. The html_file is read from the data/ directory and its content is inserted verbatim (as raw HTML) into the page body, wrapped in the site layout:

from app.customization import register_html_page

register_html_page(
    "/testcustom",
    title="test html page",
    html_file="test.html",  # relative to data/
    show_in_navbar=True,
)

If you register a page at /, it replaces the homepage, and the default stream of notes is moved to /notes automatically.

For anything more dynamic, you can register a raw FastAPI handler instead:

from starlette.responses import PlainTextResponse
from app.customization import register_raw_handler

async def my_handler(request):
    return PlainTextResponse("hello from a custom route")

register_raw_handler(
    "/hello",
    title="Hello",
    handler=my_handler,
    show_in_navbar=False,
)

Routes registered with show_in_navbar=True (the default) get a link added to the navigation bar automatically.

Custom stream visibility

The main public stream (the homepage) shows, by default, non-reply notes from people you follow, plus anything mentioning you and replies within your own conversations.

You can override exactly what appears in the stream by defining a custom_stream_visibility_callback in data/stream.py. It receives an ObjectInfo for each incoming object and returns True to show it in the stream or False to hide it:

from app.customization import ObjectInfo

def custom_stream_visibility_callback(object_info: ObjectInfo) -> bool:
    # e.g. only show posts tagged #mycommunity from people you follow
    return (
        object_info.is_from_following
        and "mycommunity" in object_info.hashtags
    )

The ObjectInfo passed to the callback exposes:

Field

Description

is_reply

Whether the object is a reply

is_local_reply

Whether it’s a reply to one of your own objects

is_mention

Whether it mentions you (the local actor)

is_from_following

Whether it’s from someone you follow

hashtags

List of hashtags on the object, e.g. ["microblogpub"]

actor_handle

The author’s handle, e.g. @dev@microblog.pub

remote_object

The full remote object

Restart microblog.pub after adding or changing data/stream.py.

Blocking accounts

You can block a single actor from anywhere their profile is displayed in the admin interface (the block button next to their handle).

The Blocks page in the admin menu lists every account you have blocked, with an unblock button for each — it’s also where you can check whether someone is still blocked. Mastodon clients see the same list through GET /api/v1/blocks.

Note that the list is ordered by when each account was first seen by your instance, not by when you blocked it: blocks are stored as a flag on the account, without a timestamp of their own.

Muting accounts

Muting is the quieter alternative to a block: the account keeps following you and can still see your posts, nothing is federated (they can’t tell), but their posts stop appearing anywhere you read. That covers the stream, their boosts, and other people’s boosts of them, plus their notifications.

The mute button sits next to block wherever a profile is displayed in the admin interface, and the Mutes page in the admin menu lists everyone you’ve muted with an unmute button for each. Muting someone hides posts you already received rather than deleting them — unmuting brings them back.

Mastodon clients see the same list through GET /api/v1/mutes and can mute with the two options the API defines: whether notifications are hidden too (the admin button always hides them), and a duration after which the mute lapses on its own (the admin button mutes until you undo it). Like the blocks list, the page is ordered by when each account was first seen by your instance, not by when you muted it.

Blocking servers

In addition to blocking “single actors” via the admin interface, you can also prevent any communication with entire servers.

Add a blocked_servers config item into profile.toml.

The reason field is just there to help you document/remember why a server was blocked.

You should unfollow any account from a server before blocking it.

blocked_servers = [
    {hostname = "bad.tld", reason = "Bot spam"},
]

Public website

Public notes will be visible on the homepage.

Only the last 20 followers/follows you have will be shown on the public website.

And only the last 20 interactions (likes/shares/webmentions) will be displayed, to keep things simple/clean.

Admin section

You can login to the admin section by clicking on the Admin link in the footer or by visiting https://yourdomain.tld/admin/login. The password is the one set during the initial configuration.

By default, an admin session stays valid for 3 days. You can change this by setting session_timeout (in seconds) in profile.toml:

session_timeout = 86400  # stay logged in for 1 day

Lookup

The Lookup section allows you to interact with any remote remote objects/content on the Fediverse.

The lookup supports:

  • profile page, like https://testing.microblog.pub

  • content page, like https://testing.microblog.pub/o/4bccd2e31fad43a7896b5a33f0b8ded9

  • username handle like @testing@testing.microblog.pub

  • ActivityPub ID, like https://testing.microblog.pub/o/4bccd2e31fad43a7896b5a33f0b8ded9

Post actions

Each post shows a row of admin-only action buttons. Two of them apply to specific kinds of posts:

  • history — shown on your own posts once they have at least one edit. Opens a page listing every past revision, with the timestamp it was saved at, so you can see what a post looked like before each edit.

  • fetch replies — shown on posts from other instances that appear in your inbox (boosts and replies included). Triggers an on-demand fetch of that post’s replies collection from the remote server, for replies that were never delivered to you directly. It’s capped to a few seconds and a couple of new replies per click, so it’s meant to be pressed again if a thread has more replies than it pulled in on the first try, rather than left to fetch everything at once.

Authoring notes

Notes are authored in Markdown. There is no imposed characters limit.

If you fill the content warning, the note will be automatically marked as sensitive.

You can optionally set a language for the note (a BCP 47 tag such as en or pt-BR). When set, it is advertised to other servers through the ActivityPub contentMap, the same way Mastodon does, so remote clients can label and filter your post by language. Leave the field empty to omit it (no language is advertised). The field is pre-suggested with your instance’s default language.

You can add attachments/upload files. When attaching pictures, EXIF metadata (like GPS location) will be removed automatically before being stored.

Consider marking attachments as sensitive using the checkbox if needed.

Attaching video and audio

Video and audio attachments work the same way as pictures: pick a file and it uploads with the note. There’s no re-encoding — the file you upload is the file that gets served and federated, so it needs to already play in mainstream browsers (Firefox/Chrome). As soon as you pick a video or audio file, the compose form checks it in your browser and shows an instant preview (duration, dimensions) or a warning if this browser can’t play it back. That check only reflects your own browser though — Safari plays some formats (like HEVC from an iPhone) that Firefox and Chrome don’t — so the instance itself also checks the file’s actual codec/container when you submit, and rejects it with a specific reason if it’s confidently unsupported.

If a video or audio upload is rejected, the error names the actual problem (e.g. “hevc video is not playable in most browsers”). The fix is almost always the same: re-encode the file as H.264 video / AAC audio in an MP4 container, which plays everywhere. A QuickTime .mov container is rejected even if the video inside is otherwise fine — remux it to .mp4 (ffmpeg -i in.mov -c copy out.mp4 re-containers without re-encoding, so it’s fast and lossless).

Video gets a poster frame (a still pulled from partway through the clip) shown before playback starts, the same way an image gets a thumbnail. This requires ffmpeg to be installed on the server (see Installation) — without it, video/audio still uploads, just without a poster, duration, or compatibility warning at all (uploads are never rejected in that case).

Webmentions

Public notes that link to “Webmention-compatible” website will trigger an outgoing webmention. Most websites that support Webmention will display your profile on the mentioned page.

Fenced code blocks

You can include code blocks in notes, using the triple backtick syntax.

The code will be highlighted using Pygments.

Example:

Hello

```python
print("I will be highlighted")
```

Interactions

microblog.pub supports the most common interactions supported by the Fediverse.

Shares

Sharing (or announcing) an object will relay it to your followers and notify the author. It will also be displayed on the homepage.

Most receiving servers will increment the number of shares.

Receiving a share will trigger a notification, increment the shares counter on the object and the actor avatar will be displayed on the object permalink.

Likes

Liking an object will notify the author.

Unlike sharing, liked objects are not displayed on the homepage.

Most receiving servers will increment the number of likes.

Receiving a like will trigger a notification, increment the likes counter on the object and the actor avatar will be displayed on the object permalink.

Bookmarks

Bookmarks allow you to like objects without notifying the author.

It is basically a “private like”, and allows you to easily access them later.

It will also prevent objects to be pruned.

Webmentions

Sending webmentions to ping mentioned websites is done automatically once a public note is authored.

Receiving a webmention will trigger a notification, increment the webmentions counter on the object and the source page will be displayed on the object permalink.

Backup and restore

All the data generated by the server is located in the data/ directory:

  • Configuration files

  • Server secrets

  • SQLite3 database

  • Theme modifications

  • Custom emoji

  • Uploaded media

Restoring is as easy as adding your backed up data/ directory into a fresh deployment.

Moving from another instance

If you want to move followers from your existing account, ensure it is supported in your software documentation.

For Mastodon you can look at Moving or leaving accounts.

If you wish to move to another instance, see Moving to another instance.

First you need to grab the “ActivityPub actor URL” for your existing account:

Python edition

# For a Python install
poetry run inv webfinger username@instance-you-want-to-move-from.tld

Edit the config.

Docker edition

# For a Docker install
make account=username@instance-you-want-to-move-from.tld webfinger

Edit the config.

Edit the config

And add a reference to your old/existing account in profile.toml:

also_known_as = "https://instance-you-want-to-move-form.tld/users/username"

Restart the server, and you should be able to complete the move from your existing account.

Note that if you already have a redirect in place on Mastodon, you may have to remove it before initiating the migration.

Import follows from Mastodon

You can import the list of follows/following accounts from Mastodon.

It requires downloading the “Follows” CSV file from your Mastodon instance via “Settings” / “Import and export” / “Data export”.

Then you need to run the import task:

Python edition

# For a Python install
poetry run inv import-mastodon-following-accounts following_accounts.csv

Docker edition

# For a Docker install
make path=following_accounts.csv import-mastodon-following-accounts

Tasks

Configuration checking

You can confirm that your configuration file (data/profile.toml) is valid using the check-config

Python edition

poetry run inv check-config

Docker edition

make check-config

Recompiling CSS files

You can ensure your custom theme is valid by recompiling the CSS manually using the compile-scss task.

Python edition

poetry run inv compile-scss

Docker edition

make compile-scss

Password reset

If have lost your password, you can generate a new one using the reset-password task.

Python edition

# shutdown supervisord
poetry run inv reset-password
# edit data/profile.toml
# restart supervisord

Docker edition

docker compose stop
make reset-password
# edit data/profile.toml
docker compose up -d

Pruning old data

You should prune old data from time to time to free disk space.

The default retention for the inbox data is 15 days.

It’s configurable via the inbox_retention_days config item in profile.toml:

inbox_retention_days = 30

Data owned by the server will never be deleted (at least for now), along with:

  • bookmarked objects

  • liked objects

  • shared objects

  • inbox objects mentioning the local actor

  • objects related to local conversations (i.e. direct messages, replies)

For now, it’s recommended to make a backup before running the task in case it deletes unwanted data.

You should shutdown the server before running the task.

Python edition

# shutdown supervisord
cp -r data/microblogpub.db data/microblogpub.db.bak
poetry run inv prune-old-data
# relaunch supervisord and ensure it works as expected
rm data/microblogpub.db.bak

Docker edition

docker compose stop
cp -r data/microblogpub.db data/microblogpub.db.bak
make prune-old-data
docker compose up -d
rm data/microblogpub.db.bak

Reporting unattached uploads

Uploaded media (images, video, audio) is stored independently of the post it’s attached to, and in a few cases a row can end up unattached to any post — e.g. a rejected multi-file post, or media uploaded via the Mastodon API and never used. This is a read-only report: it lists unattached Upload rows and files in data/uploads with no matching row, but deletes nothing. Unattached does not mean deletable: it can be in-flight media for a post not submitted yet, or media queued in a scheduled post. The server doesn’t need to be stopped to run it.

Python edition

poetry run inv report-unattached-uploads

Docker edition

make report-unattached-uploads

Removing a confirmed orphan by hand

The report deliberately doesn’t delete anything – an unattached upload isn’t necessarily garbage (see above). If, after checking, you’re sure a row is safe to remove (e.g. an old, undated one with no (referenced by a scheduled status) note, and no ongoing draft it could belong to), remove it by hand. data/ is the same directory in both the Python and Docker editions, so this runs against it directly on the host either way:

# Back up first
cp -r data/microblogpub.db data/microblogpub.db.bak

# id and hash are the Upload#<id> and hash=... values the report printed
sqlite3 data/microblogpub.db "DELETE FROM upload WHERE id = <id>;"
rm -f "data/uploads/<hash>" "data/uploads/<hash>_resized"

# once you've confirmed the server still starts and behaves as expected
rm data/microblogpub.db.bak

For a file the report lists with no matching Upload row (the second half of the report), there’s nothing in the database to touch – just remove the file(s) directly.

Moving to another instance

If you want to migrate to another instance, you have the ability to move your existing followers to your new account.

Your new account should reference the existing one, refer to your software configuration (for example Moving or leaving accounts from the Mastodon doc).

If you wish to move from another instance, see Moving from another instance.

Execute the Move task:

Python edition

# For a Python install
poetry run inv move-to username@domain.tld

Docker edition

# For a Docker install
make account=username@domain.tld move-to

Deleting the instance

If you want to delete your instance, you can request other instances to delete your remote profile.

Note that this is a best-effort delete as some instances may not delete your data.

The command won’t remove any local data, it just broadcasts account deletion messages to all known servers.

After executing the command, you should let the server run until all the outgoing delete tasks are sent.

Once deleted, you won’t be able to use your instance anymore, but you will be able to perform a fresh re-install of any ActivityPub software.

Python edition

# For a Python install
poetry run inv self-destruct

Docker edition

# For a Docker install
make self-destruct

Troubleshooting

If the server is not (re)starting, you can: