A lightweight reverse proxy for Parley Chat that sits in front of a Sova backend. It lets you control which paths are exposed, block unwanted routes, forward frontend requests, and route traffic through an upstream HTTP or SOCKS5 proxy.
Relay is a Python application (Flask + Waitress) that accepts HTTP requests and forwards them to a configured Sova backend. It is designed to be the public-facing entry point for a Parley Chat deployment, replacing or supplementing the default nginx-only setup.
Key capabilities:
- Managed paths: each path prefix can independently be proxied to the backend, blocked (403), or redirected to another URL
- Frontend handling: serve the Mura frontend from a local directory, forward to a separate URL, or disable non-API routes entirely
- Upstream proxy: route all outbound backend requests through an HTTP or SOCKS5 proxy
- SSE streaming: the
/api/v1/streamendpoint is proxied with full Server-Sent Events streaming support - nginx + SSL: the installer sets up its own nginx reverse proxy with Let's Encrypt or self-signed certificates
Download and run the installer:
wget https://raw.githubusercontent.com/Parley-Chat/relay/main/install.sh -O install.sh
chmod +x install.sh
sudo ./install.shWith curl:
curl -fsSL https://raw.githubusercontent.com/Parley-Chat/relay/main/install.sh -o install.sh
chmod +x install.sh
sudo ./install.shThe script will walk you through:
- Domain or IP address
- URI path prefix (must match the
uri_prefixvalue in your Sovaconfig.toml) - Sova backend URL (e.g.
http://127.0.0.1:42836) - Install directory
- Thread count
- nginx reverse proxy (optional) with SSL certificate choice
- Upstream proxy (optional)
- Frontend mode
It installs Python dependencies, writes config.toml, creates systemd services (parley-relay and optionally parley-relay-nginx), and starts everything.
To uninstall:
sudo ./install.sh
# choose [X] UninstallThe relay reads config.toml from its working directory. All fields:
version = 1
# Must match Sova's uri_prefix. Leave empty if Sova has no prefix.
uri_prefix = "your20charprefix"
[server]
host = "127.0.0.1" # Bind address
port = 7861 # Bind port
threads = 16 # Waitress worker threads
max_content_length = 67108864 # Max request body size in bytes (64 MB)
[backend]
url = "http://127.0.0.1:42836" # Sova backend URL
[upstream_proxy]
enabled = false
url = "" # e.g. "socks5://user:pass@host:1080" or "http://proxy:3128"
[frontend]
# "serve" — serve static files from `directory`
# "forward" — proxy all non-API requests to `url`
# "disabled" — return 404 for all non-API routes
mode = "serve"
directory = "/opt/parley-relay/mura"
url = "" # used when mode = "forward"
# Managed paths — checked in order, first prefix+method match wins.
# action: "proxy", "block", or "redirect"
# methods: optional list — omit to match all methods
# block_files: optional bool (action="proxy" only) — reject multipart requests
# that contain file uploads (403), but proxy plain form posts through
# max_size: optional int (action="proxy" only) — for GET/HEAD requests, send an
# upstream HEAD request first and reject responses larger than this size
# For "redirect": target = "https://example.com"
# code = 301 (default), 302, 307, or 308
[[paths]]
prefix = "/api/v1"
action = "proxy"
[[paths]]
prefix = "/pfp"
action = "proxy"
[[paths]]
prefix = "/attachment"
action = "proxy"
# max_size = 10485760 # 10 MB download limit for this route
[[paths]]
prefix = "/health"
action = "proxy"Managed paths let you control what the relay does with each incoming route before it reaches the frontend handler.
Each entry has a prefix (matched against the full request path, after prepending uri_prefix) and an action:
| Action | Behaviour |
|---|---|
proxy |
Forward the requests and responses to the Sova backend |
block |
Return 403 Forbidden with a JSON error |
redirect |
Redirect to the URL in target with the status code in code (default 301) |
Each entry also accepts an optional methods list. When present, the rule only applies if the request method is in that list. Rules without methods match all methods. This allows multiple rules for the same prefix with different per-method behaviour.
Paths are checked in order, the first matching prefix+method combination wins. A prefix matches if the request path equals the prefix exactly or starts with prefix + "/".
* can be used anywhere in a prefix to match any string within a single path segment:
| Pattern | Matches | Does not match |
|---|---|---|
/api/v1/channel/*/messages |
/api/v1/channel/abc123/messages |
/api/v1/channel/messages |
/api/v1/channel/a*a/messages |
/api/v1/channel/abca/messages |
/api/v1/channel/abc/messages |
/api/v1/channel/*/messages |
/api/v1/channel/abc/messages/ack (sub-path) |
/api/v1/channel/abc/other |
Wildcards only expand within their segment, they do not cross /. A wildcard pattern still behaves as a prefix: if the path continues past the pattern, it still matches.
# Block message sending only on a specific channel
[[paths]]
prefix = "/api/v1/channel/announcements-*/messages"
methods = ["POST"]
action = "block"
[[paths]]
prefix = "/api/v1"
action = "proxy"The default managed paths correspond to Sova's actual routes:
| Prefix | Purpose |
|---|---|
/api/v1 |
All REST API endpoints (auth, channels, messages...) |
/pfp |
Profile picture file serving |
/attachment |
Message attachment file serving |
Any request that does not match a managed path is handled by the frontend mode setting.
[[paths]]
prefix = "/pfp"
action = "block"The code field controls the redirect status. Supported codes:
| Code | Meaning |
|---|---|
302 |
Temporary, method changes to GET |
307 |
Temporary, preserves the method |
301 |
Permanent, method may change to GET |
308 |
Permanent, preserves the method |
[[paths]]
prefix = "/old-api"
action = "redirect"
target = "https://example.com/new-api"
# code = 301 ← default, omit or set explicitly
[[paths]]
prefix = "/beta"
action = "redirect"
target = "https://example.com/beta-new"
code = 302
[[paths]]
prefix = "/api/v1/upload"
action = "redirect"
target = "https://upload.example.com/api/v1/upload"
code = 307 # preserve POST body and methodThe methods field lets you apply different actions to different HTTP methods on the same path. Rules are checked in order, the first rule where both the prefix and the method match is used.
[[paths]]
prefix = "/api/v1/channel"
methods = ["POST", "PATCH", "DELETE"]
action = "block"
[[paths]]
prefix = "/api/v1/channel"
# no methods matches everything else (GET, OPTIONS, HEAD, ...)
action = "proxy"
[[paths]]
prefix = "/api/v1"
action = "proxy"With this config, GET /api/v1/channel/<id>/messages is proxied normally while POST /api/v1/channel/<id>/messages (sending a message) returns 403.
Block all write operations across the entire API:
[[paths]]
prefix = "/api/v1"
methods = ["POST", "PUT", "PATCH", "DELETE"]
action = "block"
[[paths]]
prefix = "/api/v1"
action = "proxy"
[[paths]]
prefix = "/pfp"
action = "proxy"
[[paths]]
prefix = "/attachment"
action = "proxy"[[paths]]
prefix = "/api/v1/signup"
action = "block"
[[paths]]
prefix = "/api/v1/login"
action = "block"
[[paths]]
prefix = "/api/v1"
action = "proxy"[[paths]]
prefix = "/api/v1/stream"
action = "block"
[[paths]]
prefix = "/api/v1"
action = "proxy"Sova's call routes are POST/DELETE /api/v1/channel/<id>/call and POST /api/v1/channel/<id>/call/signal. Using a wildcard targets them precisely without touching message routes:
[[paths]]
prefix = "/api/v1/channel/*/call"
action = "block"
[[paths]]
prefix = "/api/v1"
action = "proxy"/api/v1/channel/*/call matches the call endpoint exactly and as a prefix, so /api/v1/channel/<id>/call/signal is also blocked.
Attachment files are served from GET /attachment/<file_id>. To prevent clients from downloading any attachment:
[[paths]]
prefix = "/attachment"
action = "block"Set max_size on any proxied managed path to a positive byte value to make the relay send an upstream HEAD request before proxying GET or HEAD requests on that route. If the backend reports a Content-Length larger than the configured limit, the relay returns 413 and does not fetch the body.
[[paths]]
prefix = "/attachment"
action = "proxy"
max_size = 10485760 # 10 MBYou can use the same option on any other proxied prefix, not just /attachment.
If the backend does not provide Content-Length on the upstream HEAD response, the relay returns 502 because it cannot verify the size safely.
Profile pictures are served from GET /pfp/<pfp_id>. To block all profile picture serving:
[[paths]]
prefix = "/pfp"
action = "block"Put both blocks before the /api/v1 proxy rule so they are matched first:
[[paths]]
prefix = "/attachment"
action = "block"
[[paths]]
prefix = "/pfp"
action = "block"
[[paths]]
prefix = "/api/v1"
action = "proxy"In Sova, file uploads are not a separate endpoint, they are embedded inside regular API calls as multipart form data:
- Message attachments are uploaded via
POST /api/v1/channel/<channel_id>/messages(same endpoint as plain text messages, with an additionalfilesfield) - Profile pictures are uploaded via
PATCH /api/v1/me(same endpoint as display name changes, with an optionalpfpfield)
Use block_files = true on a proxy rule to inspect the multipart body: if the request contains file parts, it is rejected with 403; if it is a plain form post (text message, display name change), it is proxied through normally.
Block attachment uploads while allowing plain text messages:
[[paths]]
prefix = "/api/v1/channel"
methods = ["POST"]
action = "proxy"
block_files = true
[[paths]]
prefix = "/api/v1/channel"
action = "proxy"
[[paths]]
prefix = "/api/v1"
action = "proxy"Block profile picture uploads while allowing display name changes:
[[paths]]
prefix = "/api/v1/me"
methods = ["PATCH"]
action = "proxy"
block_files = true
[[paths]]
prefix = "/api/v1/me"
action = "proxy"
[[paths]]
prefix = "/api/v1"
action = "proxy"Block all uploads and all downloads:
[[paths]]
prefix = "/attachment"
action = "block"
[[paths]]
prefix = "/pfp"
action = "block"
[[paths]]
prefix = "/api/v1/channel"
methods = ["POST"]
action = "proxy"
block_files = true
[[paths]]
prefix = "/api/v1/me"
methods = ["PATCH"]
action = "proxy"
block_files = true
[[paths]]
prefix = "/api/v1"
action = "proxy"pip3 install -r requirements.txt
python3 main.pyconfig.toml must exist in the same directory as main.py.