Waveline — Self-hosting your own relays

For people with a VPS or home server who want their remote access and Alexa voice control to run on their own infrastructure instead of Waveline's shared relay.

You do not need this. Out of the box, Waveline's remote access and Alexa control work through a hosted relay without any setup. This guide is only for hosting the two small relay services yourself:

The scripts — download them here, nobody has to invent anything:

Both are single-file Python programs with zero third-party dependencies (standard library only), bind to loopback, sit behind an HTTPS proxy, and are hardened against abuse with rate limits and constant-time authentication. A 512 MB VPS is plenty.

Why HTTPS matters: your app authenticates with secret tokens. Both relays only bind to 127.0.0.1, and the guide fronts them with Apache + a Let's Encrypt certificate — secrets never cross the internet unencrypted, and no port is ever opened directly to the world.

0. What you need

1. Server basics

Copy-paste each block onto your server (or run it locally in an SSH session). Every command block has a Copy button.

sudo apt update && sudo apt -y upgrade
sudo apt -y install python3 apache2 certbot python3-certbot-apache
sudo adduser --system --group --home /nonexistent --shell /usr/sbin/nologin wrelay
sudo adduser --system --group --home /nonexistent --shell /usr/sbin/nologin walexa

These create two locked-down service users (no login, no home). Each relay runs as its own unprivileged user.

2. Remote-access relay

2.1 Install the files

Download the real script first: this site, /selfhost/relay.py — the same code the hosted relay runs (with its own server addresses replaced by yours). It needs accounts.py (accounts, sessions, e-mail) and liveroom.py (Live Room support) sitting next to it. Save both, then upload them to the server:
sudo mkdir -p /opt/wrelay-remote /etc/wrelay-remote /var/lib/wrelay-remote
# from the PC where you saved the three downloads:
scp relay.py accounts.py liveroom.py your-server:/tmp/
sudo mv /tmp/relay.py /tmp/accounts.py /tmp/liveroom.py /opt/wrelay-remote/
sudo chown -R wrelay:wrelay /opt/wrelay-remote
sudo chown wrelay:wrelay /var/lib/wrelay-remote
All files are pure Python standard library (no pip install). liveroom.py is required - the relay imports it exactly like accounts.py, and without it the service crashes at start. After downloading, check they are the current versions — they are refreshed with every Waveline release.

2.2 Generate an admin key and write the environment file

sudo bash -c 'echo "RELAY_ADMIN_KEY=$(head -c 32 /dev/urandom | sha256sum | cut -d" " -f1)" > /etc/wrelay-remote/admin.env'
sudo chown root:wrelay /etc/wrelay-remote/admin.env
sudo chmod 640 /etc/wrelay-remote/admin.env
The admin key unlocks the private admin page (https://your-domain/remote/admin) where you can see paired devices, revoke them, and read the abuse summary. Save a copy of the key — it is shown only once.

2.3 systemd service

sudo tee /etc/systemd/system/wrelay-remote.service > /dev/null <<'EOF'
[Unit]
Description=Waveline remote-access relay
After=network.target

[Service]
Type=simple
User=wrelay
Group=wrelay
ExecStart=/usr/bin/python3 /opt/wrelay-remote/relay.py
Environment=RELAY_PORT=8640
EnvironmentFile=/etc/wrelay-remote/admin.env
Environment=WAVELINE_RELAY_DB=/var/lib/wrelay-remote/pairs.json
Restart=on-failure
RestartSec=5
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now wrelay-remote
sleep 1
curl -s http://127.0.0.1:8640/healthz

Expected output: {"ok": true}

2.4 What the settings mean

SettingMeaning
RELAY_PORT=8640Loopback port Apache will proxy to. Any free port works.
RELAY_ADMIN_KEYUnlocks /remote/admin (device list, revoke, abuse summary). Leave empty to disable the admin page entirely.
WAVELINE_RELAY_DBWhere pairings persist (survives restarts, 30-day idle TTL).
WAVELINE_LR_KEY (optional)Enables Live Room hosting on this relay (persistent host key).
WAVELINE_SMTP_*E-mail settings for verification/reset mail - see 2.5. Until they are set, account mails fail silently.

2.5 E-mail: give accounts a working mail server (Stalwart)

Sign-up verification, password resets, security alerts and the feedback copies all leave the relay as e-mail. Until a mail server answers, those sends fail quietly (logged, never fatal) and nobody can finish signing up. The relay reads its mail settings from the environment file - every knob has a default that already matches the Stalwart setup below:

VariableMeaning (default)
WAVELINE_SMTP_HOSTSMTP server the relay connects to (default 127.0.0.1).
WAVELINE_SMTP_PORTSubmission port (default 587).
WAVELINE_SMTP_USER / WAVELINE_SMTP_PASSSubmission login; AUTH is only attempted when both are set.
WAVELINE_SMTP_FROMFrom header, e.g. Waveline <waveline@your-domain>.
WAVELINE_SMTP_TLSstarttls (default) | implicit (port 465) | none.

Two supported routes:

Shortcut: the one-line installer can do all of this for you - it installs Stalwart, finishes the one-time web setup (hostname, mail domain, TLS + DKIM) or waits while you do it in the browser, then creates the waveline sending account with a generated password, wires the environment file and offers a real test send. The manual steps below are the do-it-yourself path.

2.5.1 Install Stalwart

Three prerequisites: ~1 GB of RAM free, TCP port 25 allowed both ways by your provider, and DNS - create the A record mail.your-domain pointing at your server IP now, and set reverse DNS (PTR) to mail.your-domain in your provider's panel (receiving servers check that pairing first).

curl --proto '=https' --tlsv1.2 -sSf https://get.stalw.art/install.sh -o stalwart-install.sh
less stalwart-install.sh          # read before running
sudo sh stalwart-install.sh       # defaults: /opt/stalwart, embedded data store
sudo systemctl status stalwart    # on older releases the unit is stalwart-mail
sudo ufw allow OpenSSH && sudo ufw allow 25,80,443,465,587,993/tcp && sudo ufw enable

(This extends section 4's firewall: mail needs 25, 465, 587 and 993 too.) The installer prints the WebAdmin address and a one-time admin password - copy it now. Port 8080 is plain HTTP: never open it to the world; tunnel it from your PC instead:

ssh -L 8080:127.0.0.1:8080 you@your-server
# then browse http://localhost:8080/login, sign in, change the admin password

2.5.2 Hostname, certificate, DNS records

  1. WebAdmin → Settings → Network: set Hostname to mail.your-domain (it must match your PTR record).
  2. TLS → ACME Providers: add Let's Encrypt (production), domain mail.your-domain, a contact e-mail; then Reload configuration. The certificate arrives within a minute or two via port 443.
  3. Management → Directory → Domains: add your-domain. Stalwart generates DKIM keys (RSA + Ed25519) automatically.
  4. Open the domain's DNS records view and publish at your DNS provider exactly what it lists: MX (mail.your-domain, priority 10), SPF (v=spf1 mx -all), both DKIM keys, and DMARC (v=DMARC1; p=quarantine; rua=mailto:postmaster@your-domain).
  5. Verify: dig +short MX your-domain, dig +short TXT your-domain, dig +short TXT _dmarc.your-domain. Do not send account mail until all of them answer.

2.5.3 Create the sending account and wire the relay

Management → Directory → Accounts: create an account named waveline with e-mail waveline@your-domain and a strong password. Then hand the credentials to the relay:

sudo tee -a /etc/wrelay-remote/admin.env > /dev/null <<'EOF'
WAVELINE_SMTP_HOST=127.0.0.1
WAVELINE_SMTP_PORT=587
WAVELINE_SMTP_USER=waveline@your-domain
WAVELINE_SMTP_PASS=change-me-strong
WAVELINE_SMTP_FROM=Waveline <waveline@your-domain>
WAVELINE_SMTP_TLS=starttls
EOF
sudo chown root:wrelay /etc/wrelay-remote/admin.env && sudo chmod 640 /etc/wrelay-remote/admin.env
sudo systemctl restart wrelay-remote

Stalwart's port 587 is submission with STARTTLS + AUTH - exactly the relay's defaults (starttls, 127.0.0.1:587), so nothing else to tune. Sending only needs submission; IMAP/JMAP stay available for reading that mailbox in any client.

2.5.4 Test before you rely on it

sudo apt -y install swaks
swaks --to you@somewhere-else.example --from waveline@your-domain \
  --server 127.0.0.1 --port 587 --tls \
  --auth-user 'waveline@your-domain' --auth-password 'change-me-strong'

The transcript should end in a 250; open the message at the far end and check the headers say spf=pass, dkim=pass, dmarc=pass. Then one real end-to-end test: sign up a throwaway Waveline account and click the verification link. Finally send one to a mail-tester address for a deliverability score.

Port 25 outbound: many VPS providers block it by default on new servers; without it your mail cannot leave. Test with nc -zv gmail-smtp-in.l.google.com 25 - "succeeded" means open, otherwise ask the provider to unblock it.

3. Alexa relay

3.1 Install the files

Download the real script first: this site, /selfhost/relay_server.py — the same Alexa-relay code the hosted service runs (this is its file name relay_server.py; the systemd unit below installs it as server.py).
sudo mkdir -p /opt/wrelay-alexa /etc/wrelay-alexa
# from the PC where you saved the download:
scp relay_server.py your-server:/tmp/
sudo mv /tmp/relay_server.py /opt/wrelay-alexa/server.py
sudo chown -R walexa:walexa /opt/wrelay-alexa
# Per-user shared secret + client token (two trust tiers):
sudo bash -c 'head -c 32 /dev/urandom | sha256sum | cut -d" " -f1 > /etc/wrelay-alexa/shared-secret'
sudo bash -c 'head -c 32 /dev/urandom | sha256sum | cut -d" " -f1 > /etc/wrelay-alexa/client-token'
sudo chown -R root:walexa /etc/wrelay-alexa
sudo chmod 750 /etc/wrelay-alexa
sudo chmod 640 /etc/wrelay-alexa/shared-secret /etc/wrelay-alexa/client-token

3.2 systemd service

sudo tee /etc/systemd/system/wrelay-alexa.service > /dev/null <<'EOF'
[Unit]
Description=Waveline Alexa relay API
After=network.target

[Service]
Type=simple
User=walexa
Group=walexa
ExecStart=/usr/bin/python3 /opt/wrelay-alexa/server.py
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ReadWritePaths=

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now wrelay-alexa
sleep 1
curl -s -X POST http://127.0.0.1:8099/command -H "Content-Type: application/json" -d '{"token":"wrong"}'

Expected output includes "unauthorized" — that proves it is up and refusing strangers.

Two secrets, two tiers: the shared secret ( Alexa / admin side) may enqueue commands and read the wake flag; the client token (your PC app) may only poll and drain its own queue. A leaked phone token can never push commands.

3.3 Connecting YOUR PC to it

  1. Waveline → Settings → Alexa tab → tick Remote mode (or "Use a hosted relay").
  2. Set the relay URL to https://waveline.example.com/relay.
  3. Paste the client token from the server: sudo cat /etc/wrelay-alexa/client-token, press Save.
  4. Say "Alexa, pause on Waveline" — the command appears in https://waveline.example.com/relay/status (shared-secret protected admin view) and executes in the app.

4. HTTPS proxy (Apache)

sudo tee /etc/apache2/sites-available/waveline.conf > /dev/null <<'EOF'
<VirtualHost *:80>
    ServerName waveline.example.com
    ProxyPreserveHost On
    ProxyPass        /remote http://127.0.0.1:8640/ retry=0
    ProxyPassReverse /remote http://127.0.0.1:8640/
    ProxyPass        /relay/ http://127.0.0.1:8099/ retry=0
    ProxyPassReverse /relay/ http://127.0.0.1:8099/
</VirtualHost>
EOF
sudo a2enmod proxy proxy_http headers
sudo a2ensite waveline
sudo apache2ctl configtest
sudo systemctl reload apache2
sudo certbot --apache -d waveline.example.com

Certbot rewrites the vhost to HTTPS automatically and renews the certificate. Test from your PC:

curl https://waveline.example.com/remote/healthz
curl -X POST https://waveline.example.com/relay/command -H "Content-Type: application/json" -d "{}"

First should say {"ok": true}; second should refuse you politely (unauthorized) — both prove the proxy works.

Firewall: only 22 (SSH), 80 and 443 should be open. The relays themselves listen on 127.0.0.1 and must never be exposed directly. If you use ufw: sudo ufw allow OpenSSH && sudo ufw allow "Apache Full" && sudo ufw enable.

5. Alexa control over your relay (optional, advanced)

The Alexa relay itself does not talk to Amazon — something must translate Alexa's directives into relay commands. Waveline ships two options:

The minimal flow logic (paste into a Node-RED function node, then wire a vsh-virtual-device node into it and an http request node after):

const dir = msg.payload && msg.payload.rawDirective && msg.payload.rawDirective.directive;
if (!dir) return null;
const name = dir.header.name;
const map = { Play: "play", Pause: "pause", Stop: "stop",
              Next: "next", Previous: "previous" };
let command = map[name];
if (!command && name === "SetVolume") {
    command = "volume_set";
    msg.value = dir.payload.volume;
}
if (!command) return null;
msg.url = "https://waveline.example.com/relay/command";
msg.headers = { "Content-Type": "application/json" };
msg.payload = { token: flow.get("relaySecret"), command: command, value: msg.value || 0 };
return msg;

Store the shared secret in a Node-RED flow variable named relaySecret. This is exactly how Waveline's hosted relay does it — the full flow also maps thermostats for sleep timer and playback speed.

6. Pointing your PC at your relays

FeatureWhere in WavelineWhat to enter
Remote access Settings → Remote tab → "Relay URL" field https://waveline.example.com/remote
Alexa (remote mode) Settings → Alexa tab → "Relay URL" https://waveline.example.com/relay + client token

Pair your phone exactly as normal: the pairing code comes from your relay now. Open https://waveline.example.com/remote/ on the phone, enter the code, done.

7. Your admin page

Visit https://waveline.example.com/remote/admin, paste your RELAY_ADMIN_KEY:

8. Maintenance

Updating the relay code

When a Waveline update ships a newer relay, re-download the scripts from this site (/selfhost/relay.py + /selfhost/accounts.py + /selfhost/liveroom.py + /selfhost/relay_server.py), copy them over and restart:

sudo cp relay.py accounts.py /opt/wrelay-remote/
sudo systemctl restart wrelay-remote
sudo cp relay_server.py /opt/wrelay-alexa/server.py
sudo systemctl restart wrelay-alexa
New Waveline versions can add new whitelisted remote actions (7.1 added the alarm Snooze/Dismiss pair) - an OLD relay rejects those commands until you update it. Update the relay whenever you update the app.

Useful checks

systemctl status wrelay-remote wrelay-alexa
journalctl -u wrelay-remote -n 50 --no-pager
curl -s https://waveline.example.com/remote/healthz
tail -20 /var/lib/wrelay-remote/pairs.json   # who is paired

Backups

tar czf wrelay-alexa-backup.tar.gz /etc/wrelay-remote /etc/wrelay-alexa /var/lib/wrelay-remote

That archive contains your secrets and pairings — store it somewhere safe.

9. Troubleshooting

SymptomCause / fix
Phone page says "pairing not found"The code was generated against the old relay. Generate a fresh code in Settings after switching relay URL.
curl /healthz works locally but not via domainApache proxy not loaded: sudo a2enmod proxy proxy_http then reload. Or DNS doesn't point at this server yet.
Alexa commands never arriveCheck the client token matches /etc/wrelay-alexa/client-token byte-for-byte; check app is in remote mode; check the app's relay URL ends at /relay.
429 responses everywhereYou are rate-limited (too many attempts). Wait a minute; if a whole office is behind one IP, raise the limits at the top of the relay file.
Relay dies after rebootsudo systemctl enable wrelay-remote wrelay-alexa (the --now flag starts it once; enable makes it permanent).

10. Security notes