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:
python -m zipfile -e waveline-selfhost-kit.zip . (Linux/macOS: python3 -m zipfile -e) or unzip waveline-selfhost-kit.zip./opt/wrelay-kit, and can then perform this guide's setup for you: it asks first, creates the two service users, generates the admin key and the Alexa secrets, installs and starts both relay services (with live health checks), and configures the Apache HTTPS proxy + certificate for the domain you type in. New secrets are shown once and saved to /root/wrelay-credentials.txt; re-running is safe (existing keys, secrets and pairings are kept), and sudo bash install.sh --uninstall removes a test run (it backs the secrets up first and never touches other services). Save it and run sudo bash install.sh (the main page shows the curl | sudo bash one-liner). Answer "no" at any question to do that part by hand below.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.
waveline.example.com — replace it everywhere.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.
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
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.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
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.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}
| Setting | Meaning |
|---|---|
RELAY_PORT=8640 | Loopback port Apache will proxy to. Any free port works. |
RELAY_ADMIN_KEY | Unlocks /remote/admin (device list, revoke, abuse summary). Leave empty to disable the admin page entirely. |
WAVELINE_RELAY_DB | Where 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. |
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:
| Variable | Meaning (default) |
|---|---|
WAVELINE_SMTP_HOST | SMTP server the relay connects to (default 127.0.0.1). |
WAVELINE_SMTP_PORT | Submission port (default 587). |
WAVELINE_SMTP_USER / WAVELINE_SMTP_PASS | Submission login; AUTH is only attempted when both are set. |
WAVELINE_SMTP_FROM | From header, e.g. Waveline <waveline@your-domain>. |
WAVELINE_SMTP_TLS | starttls (default) | implicit (port 465) | none. |
Two supported routes:
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.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
mail.your-domain (it must match your PTR record).mail.your-domain, a contact e-mail; then
Reload configuration. The certificate arrives within a minute or
two via port 443.your-domain. Stalwart generates DKIM keys (RSA + Ed25519)
automatically.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).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.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.
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.
nc -zv gmail-smtp-in.l.google.com 25 - "succeeded"
means open, otherwise ask the provider to unblock it.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
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.
https://waveline.example.com/relay.sudo cat /etc/wrelay-alexa/client-token, press Save.https://waveline.example.com/relay/status (shared-secret protected admin view) and executes in the app.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.
ufw: sudo ufw allow OpenSSH && sudo ufw allow "Apache Full" && sudo ufw enable.The Alexa relay itself does not talk to Amazon — something must translate Alexa's directives into relay commands. Waveline ships two options:
node-red-contrib-virtual-smart-home plugin on your server, bind it to the Virtual Smart Home skill, and forward directives to your relay. The flow maps Alexa directives like SetTargetTemperature or Play to commands such as sleep_set or play, and posts each one to https://waveline.example.com/relay/command with the shared secret.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.
| Feature | Where in Waveline | What 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.
Visit https://waveline.example.com/remote/admin, paste your RELAY_ADMIN_KEY:
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
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
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.
| Symptom | Cause / 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 domain | Apache proxy not loaded: sudo a2enmod proxy proxy_http then reload. Or DNS doesn't point at this server yet. |
| Alexa commands never arrive | Check 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 everywhere | You 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 reboot | sudo systemctl enable wrelay-remote wrelay-alexa (the --now flag starts it once; enable makes it permanent). |
python3 updated via apt.