Relayium

Run Relayium as an always-on receive service

Last updated: 2026-08-06

relayium serve --once handles a single incoming transfer and exits — fine for a one-off pull. But if you want a machine to be a standing drop point — a home server that backups land on overnight, a build box that CI pushes artifacts to, a NAS your phone can send photos to whenever — you want serve running all the time, not started by hand for each transfer.

This guide covers starting a long-running listener, approving who's allowed to push to it, pre-authorizing peers for the cases where no one's at the terminal, running it under systemd, and letting a sync --delete sender mirror deletions.

Before you start

Everything below is the relayium CLI, so install it first if you haven't. On macOS or Linux, one command drops a prebuilt binary on your PATH:

curl -fsSL https://relayium.com/install.sh | sh

Start the listener

What you need before step 1

  • The CLI on both machines. relayium version prints a version string on each; a shell that answers command not found means it is not installed there yet.
  • A directory for incoming files on this machine, and the disk to hold what will land in it.
  • An address the sender can reach and an open inbound port. serve listens on 9031 unless you pass --port.
  • If this machine will run without a terminal — which is the whole point of a service — the pusher's fingerprint, ahead of time. There is a section on that below, and it is the single most common reason an always-on receiver rejects everything.

serve listens for daemon-direct pushes (relayium://host:port) over a pinned TLS 1.3 connection and writes what it receives into a directory. Nothing needs to be pre-shared to start it — no fingerprints to copy, no server to register:

relayium serve --dir ~/inbox
relayium serve --dir /srv/drop --port 9040   # non-default port
relayium serve --dir ~/inbox --allow-delete  # let a sync --delete sender mirror deletions
  1. Pick where files should land and start the listener. Nothing has to be pre-shared to get this far.

    relayium serve --dir ~/inbox
  2. On the sending machine, push something at this host by its relayium:// address.

    relayium push ./report.pdf relayium://drop.example.com:9031
  3. Back on the receiver, answer the approval prompt. y writes that fingerprint to authorized_fingerprints, and later pushes from the same machine never prompt again.

  4. Confirm the file actually landed in --dir rather than the directory you started serve from.

    ls -l ~/inbox

What a working listener looks like

serve says up front that it has no authorized peers, then prompts on the first push from a new machine and stays silent on every later one. The sender exits 0 and the file is in --dir.

$ relayium serve --dir ~/inbox
no authorized peers yet — you'll be asked to approve each new peer on its first push.
Incoming push from 203.0.113.7:54021
  fingerprint: 74318e3b…
Accept and remember this peer? [y/N] y

Approve who's allowed to push

The first time a new peer pushes, serve — if it's running in a terminal — shows you where the push came from and its fingerprint and asks you to approve it, the same way SSH asks about an unknown host on first connect:

Incoming push from 203.0.113.7:54021
  fingerprint: 74318e3b…
Accept and remember this peer? [y/N] y

Pre-authorize peers for non-interactive setups

When serve has no terminal to prompt on — a systemd service, a background process, a pipe — it can't ask, so it rejects any fingerprint it doesn't already recognize. Authorize peers ahead of time instead. On the machine that will push, run relayium id to print its fingerprint; on the receiver, add it before the first push arrives:

# on the machine that will PUSH: print its fingerprint
relayium id

# on this always-on RECEIVER: authorize it in advance
relayium authorize 74318e3b...
  1. On the machine that will push, print its fingerprint. It is 64 hex characters and identifies that machine, not its address.

    relayium id
  2. On this receiver, authorize it — with the same --config-dir the service will run under. Authorize as the wrong user, or with the default path while the unit uses another, and the fingerprint lands in a file the service never reads.

    relayium authorize 74318e3b… --config-dir /etc/relayium

Run it under systemd

For a service that survives reboots and crashes, hand serve to systemd. Point --config-dir at a fixed path so the host's identity and its authorized-peer list stay put across restarts:

# /etc/systemd/system/relayium-serve.service
[Unit]
Description=Relayium always-on receiver
After=network-online.target

[Service]
ExecStart=/usr/local/bin/relayium serve --dir /srv/drop --port 9031 --config-dir /etc/relayium --allow-delete
Restart=always
User=relayium

[Install]
WantedBy=multi-user.target
  1. Authorize every peer that should be able to push, before the service exists. It cannot prompt, so anything not already trusted is rejected.

  2. Write the unit file above to /etc/systemd/system/relayium-serve.service, with --config-dir pointing at the same fixed path you authorized under.

  3. Reload systemd and start the service, enabling it so it comes back after a reboot.

    sudo systemctl daemon-reload
    sudo systemctl enable --now relayium-serve
  4. Confirm it is running and that the reject-everything warning is absent from its log. Only the second of those is specific to this setup.

    systemctl is-active relayium-serve
    journalctl -u relayium-serve -n 20 --no-pager

What a working service looks like

is-active answers active, and the startup warning about having no authorized peers does not appear. That warning is the one line that tells you, before any sender complains, that this service will refuse every push.

$ systemctl is-active relayium-serve
active
$ journalctl -u relayium-serve -n 20 --no-pager | grep -c 'all pushes will be rejected'
0

Run it at boot on macOS (launchd)

macOS has no systemd — its service manager is launchd. To keep serve running on a Mac (a Mac mini left on as a drop point, say), install it as a LaunchDaemon so it starts at boot, before anyone logs in. Set UserName so it runs as you rather than root, and give --dir and --config-dir absolute paths so its identity and trust files stay in your own ~/.config/relayium:

<!-- /Library/LaunchDaemons/com.relayium.serve.plist  (replace YOU with your macOS username) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>              <string>com.relayium.serve</string>
  <key>UserName</key>           <string>YOU</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/relayium</string>
    <string>serve</string>
    <string>--dir</string>         <string>/Users/YOU/inbox</string>
    <string>--port</string>        <string>9031</string>
    <string>--config-dir</string>  <string>/Users/YOU/.config/relayium</string>
    <string>--allow-delete</string>
  </array>
  <key>RunAtLoad</key>   <true/>
  <key>KeepAlive</key>   <true/>
  <key>StandardOutPath</key>    <string>/Users/YOU/relayium-serve.log</string>
  <key>StandardErrorPath</key>  <string>/Users/YOU/relayium-serve.log</string>
</dict>
</plist>
# 1) authorize each pusher first — launchd gives serve no terminal to prompt on:
relayium authorize <fingerprint>     # get the fingerprint from the pusher's  relayium id

# 2) save the plist above to that path, then load it (root-owned, starts at boot):
sudo chown root:wheel /Library/LaunchDaemons/com.relayium.serve.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/com.relayium.serve.plist

# check it's running / follow logs / stop it:
sudo launchctl print system/com.relayium.serve | grep state
tail -f ~/relayium-serve.log
sudo launchctl bootout system/com.relayium.serve

Let a sync --delete sender mirror deletions

By default serve only ever adds or updates files — a sender running sync --delete against it still copies new and changed files, but any deletions it asks for are skipped, with a warning logged on the receiver. Start serve with --allow-delete to opt in to true mirroring, where files removed on the sender's side are removed here too:

relayium serve --dir /srv/mirror --allow-delete

When it doesn't work

Four of these five failures leave the service running and looking healthy — a listener that rejects everything is still a listener. Each has a line to read or a command to run that settles it.

Symptom, check, fix

The service starts and stays up, but every push is rejected.
journalctl -u relayium-serve -n 20 --no-pager
# warning: no authorized peers and no terminal to approve on; all pushes will be rejected.

A service has no terminal, so it can never run the first-push approval prompt, and an unknown fingerprint is all it can see. Pre-authorize each sender: relayium id on the sending machine, relayium authorize <fingerprint> here — with the same --config-dir the unit uses. That warning is printed at startup, so it is in the log from the first line.

The service will not start at all, complaining about insecure permissions on id.key.
stat -c '%a %U %n' /etc/relayium/id.key
# 400 relayium /etc/relayium/id.key

The key must be exactly 0600. Not 0644, and — the part that catches people — not 0400 either, so tightening it further breaks the service just as surely as loosening it. chmod 600 the key and make sure it is owned by the unit's User=.

The sender reports that the connection was refused.
relayium push ./build relayium://drop.example.com:9031
# hint: if the peer refused the connection, it may not have authorized this host.

The receiver did not recognise this sender. Run relayium id on the sender and relayium authorize that fingerprint on the receiver. If you already did, check you did it under the unit's --config-dir: the trust file is per-directory, and a fingerprint authorized into ~/.config/relayium is invisible to a service reading /etc/relayium.

Pushes work when you run serve by hand, but not through the service or not from another machine.
sudo ss -tlnp | grep 9031

Two separate causes with one check. If nothing is listening, the unit is not enabled — systemctl is-enabled relayium-serve. If it is listening, the port is blocked: open 9031 on the host firewall and in any cloud security group. A non-default --port must match the port in the sender's relayium://host:N.

A sync --delete sender's deletions never happen on this machine.
journalctl -u relayium-serve | grep -i delete

Deletion is a receiver-side opt-in and is off by default: new and changed files still copy, and each skipped deletion is logged here as a warning. Add --allow-delete to the unit's ExecStart and restart. The sender asking for it is not enough, and that asymmetry is deliberate — a receiver never loses files because of a flag typed somewhere else.

Frequently asked questions

What port does serve listen on by default?

9031. Change it with --port on both the listener (serve --port N) and the pusher's target (relayium://host:N).

Do I have to approve every single push by hand?

Only the first push from a given fingerprint, and only when serve is running with a terminal attached. It's remembered after that. Running serve non-interactively (systemd, a pipe) skips the prompt entirely and rejects unknown peers — pre-authorize them with relayium authorize instead.

Can a sender delete files on my always-on receiver?

Only if you started serve with --allow-delete and the sender is running sync --delete. Without --allow-delete, deletions are silently skipped and everything else still transfers.

Is running an always-on receiver free?

Yes. relayium serve is part of the free, self-hostable CLI — no account, no paid tier, on either side of the connection.

Where does serve keep its identity and peer list?

In ~/.config/relayium by default (id.key/id.crt for this host's identity, authorized_fingerprints for the allow-list). Point --config-dir somewhere fixed, like /etc/relayium, for a systemd service.

How do I run serve at boot on macOS?

macOS has no systemd — use launchd. Install serve as a LaunchDaemon in /Library/LaunchDaemons (starts at boot; set UserName to run as you), or as a LaunchAgent in ~/Library/LaunchAgents (starts at login). This guide has a ready-to-edit plist; there's no Homebrew service. Pre-authorize pushers with relayium authorize first, since launchd gives serve no terminal to prompt on.

Turn any machine you own into an always-on, free receiver — direct pushes over pinned TLS, no relay involved.

Get the CLI

Keep reading