Skip to content

Bringing X Posts into my Mastodon timeline - with a small home server

I wanted a simple thing: to read posts from a handful of X accounts alongside the people I already follow on Mastodon.

Not in a separate dashboard. Not as a collection of website RSS feeds. And not through one giant bot account that makes every post look as though it came from the same person.

I wanted each source account to have its own recognisable profile, with its name and avatar, and for its posts to appear naturally in my existing Mastodon home timeline.

That turned into a surprisingly satisfying homelab project: a small bridge between X, Discord, and Mastodon, running on an existing home server.

The starting point

The server was a repurposed Intel Mac mini running Linux. It already handled network storage and several containerised applications, so this project needed to fit around a working system.

There was also an existing X-to-Discord relay, powered by the open-source Tweetcord project. A dedicated X account followed the accounts I wanted to keep up with, and Tweetcord collected their posts for a Discord channel.

That gave us a useful starting point: we already had a working source of actual X posts.

The challenge was getting those posts into Mastodon while preserving the identity of each source account-and without exposing the rest of the home server.

Why not just use RSS?

RSS initially looked like the obvious shortcut. But there is an important distinction between following a publication’s website feed and following its X account.

A website feed might contain articles. Its X account might also contain short updates, photographs, commentary, announcements, and conversations that never become articles.

I wanted the X posts themselves.

Mastodon also doesn’t natively turn an arbitrary RSS feed into an account you can follow. Something still has to translate those feed entries into ActivityPub posts.

Public bridges can solve parts of this problem, but availability and federation policies vary. A bridge is only useful if it supports the accounts you want and your Mastodon server will communicate with it.

We decided to host a small, dedicated relay instead.

The architecture

The setup separates collection, publishing, and reading:

Dedicated X account’s following list
                  │
                  ▼
              Tweetcord
                  │
          ┌───────┴────────┐
          ▼                ▼
    Existing Discord   Local event export
         relay              │
                            ▼
                     Publisher service
                            │
                            ▼
                       GoToSocial
                   One mirror per account
                            │
                         ActivityPub
                            │
                            ▼
                  Existing Mastodon account

The important detail is that we did not introduce a second X scraper.

Instead, we extended the existing collector to export the posts it was already seeing. A separate publisher reads those events and creates the corresponding posts on a lightweight ActivityPub server.

That avoids duplicating collection work and keeps the Discord and Mastodon publishing paths separate.

A small federation server, not a Mastodon migration

We used GoToSocial for the relay accounts.

It speaks ActivityPub, so people on Mastodon can follow its accounts without moving to it. That meant I could keep my existing Mastodon account, follows, and normal reading app.

The relay server has a dedicated subdomain. Each source account gets its own profile there, conceptually:

@sourceaccount@relay.example.net

Each profile has:

  • The source account’s display name and avatar.
  • A bot designation.
  • A clear description identifying it as an unofficial mirror.

The distinction matters. These profiles are automated mirrors, not accounts operated or endorsed by the original authors.

Once followed from Mastodon, their posts arrive alongside everything else in the home timeline.

Reusing the existing collector

We added two small export mechanisms to the existing Tweetcord deployment.

The first maintains a snapshot of the dedicated X account’s following list. The second exports newly observed posts from Tweetcord’s existing notification cache.

The exported events contain the information the publisher needs: source identity, post identifier, text, timestamp, and available media references.

Files are updated atomically, so the publisher doesn’t accidentally read half-written JSON. The event buffer is bounded rather than growing indefinitely.

Protected accounts are excluded from the relay.

This keeps the additional collection logic narrow: export what the existing collector already knows, then let another process handle federation.

Publishing without flooding the timeline

The publisher is a small Python service managed by systemd.

It checks the exported events every 30 seconds. That is in addition to the upstream collector’s polling interval, so it is near-real-time polling-not an instant push service.

Several details make it more useful than a basic “copy text and send” script:

It starts with new posts.
There is an initial cutoff rather than an automatic historical import. Turning the relay on shouldn’t dump months of old posts into the timeline.

It tracks completed deliveries.
A local SQLite database records which source posts have been published.

It uses idempotency keys.
Retries use a stable key derived from the source identity and post identifier, helping prevent duplicates if a request succeeds but its response is lost.

It avoids accidental mentions.
Source-style @mentions are neutralised so copied text doesn’t unexpectedly notify unrelated Fediverse users.

It treats media separately.
Supported images and videos are downloaded and uploaded to the relay, subject to size and file-type limits. Media handling remains deliberately bounded; not every possible X attachment is guaranteed to transfer.

It pauses when its following-list snapshot becomes stale.
That helps avoid continuing indefinitely with an outdated view of which accounts should be mirrored.

The service starts automatically after a reboot, without needing an interactive login.

Keeping the posts readable

We initially included an explicit source-post link at the end of every relayed post.

After seeing the results in the timeline, I preferred a cleaner presentation: the original text and supported media, without an automatically appended X URL.

Links already present in the original text remain. The mirror profile itself makes the source and unofficial nature of the account clear.

We updated the already-published posts as well, preserving their images.

That was a small change, but it made the relay feel much more natural to read.

The most important constraint: don’t expose the NAS

This server also stores personal files. Making an ActivityPub endpoint reachable from the internet must not mean making the entire machine’s services reachable.

We treated the public relay as a separate deployment.

Its containers have their own application data, and they do not mount personal storage directories or the Docker socket.

A Caddy gateway handles public HTTPS with a valid certificate. The application backend is not published directly, and administrative and account-management routes are blocked at the public gateway.

The public-facing containers also share a restricted network namespace. Firewall rules block connections from that environment to private LAN ranges, Tailscale addresses, and other internal destinations, while allowing the public HTTPS connections needed for federation.

We tested that separation: public federation destinations were reachable, while attempts to reach internal services were blocked.

This is defence in depth, not a claim of perfect isolation. The relay still shares a physical host with the NAS, and the local publisher is a trusted host process. But the internet-facing components are deliberately kept away from personal storage and internal services.

What happens when I follow someone new?

The dedicated X account acts as the source-selection list.

The following list is synchronised roughly every 15 minutes. When a new public account appears, the publisher can provision its mirror profile and begin relaying newly collected posts.

There is one remaining manual step:

I still need to follow that new mirror from my Mastodon account.

Creating a remote profile and subscribing my personal account to it are separate operations. Automating the second would require an additional authorised Mastodon integration.

For the initial setup, we followed all the current mirror profiles manually. Future automatic follows are a possible improvement, not something the system already does.

What we actually verified

We tested more than whether the containers started.

An initial real X post was published through the relay and appeared in the existing Mastodon home timeline.

Then the continuous publisher picked up and published two further posts automatically, including their images. All 18 initial mirror profiles were followed successfully.

We also checked that the existing server applications remained running and that the publisher was completing cycles without errors.

That establishes a working end-to-end path. It does not prove that every type of X post, attachment, outage, or future API change will be handled perfectly.

The trade-offs

This project solves a specific problem, but it still needs maintenance.

The upstream X collection mechanism can break if authentication requirements or platform behaviour change. Federation depends on the receiving server’s policies. A home-hosted endpoint depends on power, connectivity, and correct DNS; a changing residential public IP needs attention.

Edits and deletions are another important boundary: this implementation focuses on publishing newly collected posts. It should not be described as a complete, bidirectional synchronisation of an X account.

Why this was a worthwhile homelab project

The satisfying part wasn’t installing another application. It was connecting existing components to make something personally useful.

A modest Linux machine now collects selected X posts once, continues delivering them to Discord, and publishes them through individual ActivityPub profiles-all while letting me keep my existing Mastodon account.

There is no new timeline app to learn and no account migration.

The result is exactly the experience I was aiming for: recognisable source accounts appearing among the people I already follow, in the place I already enjoy reading.