Matrix deployment on Arch Linux LXC behind Nginx
This guide describes a native, systemd-managed deployment of Synapse and Element Web in an Arch Linux LXC that already runs Odoo and PostgreSQL. An existing Nginx reverse proxy on the Proxmox side terminates public HTTPS. Docker and client-side software installation are not required.
The conferencing section adds native LiveKit and MatrixRTC authorization services. Element Web supplies the browser interface; Synapse alone does not supply the conferencing backend. Odoo loads the separately hosted Element interface in an iframe and manages reviewed identities and memberships through HTTPS APIs.
This is an operator runbook, not an automatic installer. Commands below are to be run on the identified deployment machine after replacing examples. Writing this guide does not install services, change existing databases, or establish live acceptance. Sources were reviewed on October 12, 2026; package versions and experimental MatrixRTC interfaces must be checked when deploying.
Contents
- 1. Deployment layout
- 2. Prepare the existing LXC
- 3. Install native Arch packages
- 4. Add a dedicated Synapse database
- 5. Configure and start Synapse
- 6. Deploy Element Web inside the LXC
- 7. Configure the front Nginx
- 8. Create initial Matrix accounts
- 9. Add WebRTC and conferencing
- 10. Connect the Odoo addon
- 11. Operational checks
- 12. Updates, backups, and recovery
- 13. Troubleshooting
- 14. References
1. Deployment layout
1.1 Example names and addresses
All names, addresses, account names, and secret placeholders in this guide are synthetic. Substitute actual deployment values before using any configuration.
| Example | Purpose |
|---|---|
example.com |
Permanent Matrix server name; accounts are @alice:example.com |
matrix.example.com |
Public Synapse HTTPS API |
element.example.com |
Public Element Web HTTPS interface |
rtc.example.com |
Public LiveKit signaling and legacy RTC authorization endpoints |
turn.example.com |
TURN hostname, covered by its own TLS certificate |
odoo.example.com |
Existing Odoo HTTPS interface |
10.20.0.1 |
Front Nginx address on the private network |
10.20.0.20 |
Arch LXC address on the private network |
203.0.113.10 |
Example public IPv4 address; replace this documentation-only address |
The Matrix server name is an account identity domain, not necessarily the
hostname serving the API. Choose it before initializing Synapse and retain it
through upgrades and restores. The examples use example.com with discovery
delegating connections to matrix.example.com.
See Synapse server-name selection and delegation.
1.2 Services and traffic
Internet browsers and Matrix peers
|
| HTTPS :443
v
Front Nginx on the Proxmox side
example.com -> Matrix discovery responses
matrix.example.com -> LXC 10.20.0.20:8008, Synapse
element.example.com -> LXC 10.20.0.20:8081, internal Nginx
rtc.example.com -> LXC 10.20.0.20:7880, LiveKit signaling
-> LXC 10.20.0.20:8080, legacy authorization
odoo.example.com -> existing Odoo upstream, unchanged
Arch LXC
Odoo -> existing Odoo databases and existing database role
Synapse -> new matrix_synapse database and matrix_synapse role
internal Nginx -> /usr/share/webapps/element/
LiveKit -> WebRTC media and embedded TURN
MatrixRTC service -> LiveKit authorization; optional Synapse appservice wiring
Browser WebRTC media -> direct routed/NATed media ports -> LiveKit in LXC
The second Nginx serves static Element files inside the LXC. The front Nginx handles TLS and hostname routing. An alternative is to serve Element files directly from the front host; that moves Element installation and updates there. The main instructions keep all application packages in the LXC.
HTTP proxying covers API requests and WebSocket signaling. It does not carry ordinary UDP WebRTC media or TURN traffic. Configure media routing separately.
1.3 DNS and routing
- Point
matrix.example.com,element.example.com, andrtc.example.comat the public address of the front proxy. Pointturn.example.comat the address that exposes the TURN listener. - Ensure
example.comserves HTTPS and can answer both Matrix discovery paths. If it already hosts a website, add the locations to its existing server block. - Publish AAAA records only if IPv6 routing, firewall rules, listeners, and media connectivity actually work. An unusable AAAA record can break discovery or calls.
- From the LXC, ensure the public Matrix and RTC HTTPS names are reachable. If
public-address hairpin NAT is unavailable, use split DNS resolving those names
to
10.20.0.1internally. Keep the same HTTPS names and valid certificates. - Reserve the LXC's address and confirm front-proxy connectivity to its private service ports. Restrict these HTTP ports to the proxy and authorized local use.
2. Prepare the existing LXC
Warning: this LXC already contains Odoo databases. Do not run initdb, replace
its PostgreSQL data directory, or follow a fresh-cluster initialization tutorial.
Matrix uses an additional database in the existing PostgreSQL cluster.
Before installing packages:
- Record the current Odoo, PostgreSQL, and Python versions and the Odoo interpreter path. Confirm Odoo uses its intended virtual environment.
- Take current backups through the existing Odoo/PostgreSQL backup procedure. Include Odoo filestores. A running-container snapshot alone is not a substitute for a coherent database backup.
- Schedule the Arch upgrade as maintenance. A full upgrade can affect the existing PostgreSQL major version, Python interpreter, and Odoo services.
- Reserve RAM, database connections, disk space, and upload bandwidth for Synapse and conferences. They share the LXC's resource limits with Odoo.
- Keep existing Odoo service units, virtual environments, Nginx routes, and database authentication rules intact.
Run these read-only inspections inside the LXC:
systemctl status postgresql
sudo -u postgres psql -X -d postgres -c 'SELECT version();'
sudo -u postgres psql -X -d postgres -c 'SHOW hba_file;'
sudo -u postgres psql -X -d postgres -c 'SHOW max_connections;'
ss -lntup
If PostgreSQL runs through a custom unit, socket, port, or operating-system user, adapt the commands to that installation. Do not start a second cluster to make the examples work.
3. Install native Arch packages
Run as root inside the LXC, during the maintenance window:
pacman -Syu matrix-synapse python-psycopg2 element-web nginx sudo curl jq
PostgreSQL is already installed and running. The command intentionally does not initialize it. Review the proposed package transaction before accepting it; complete any required PostgreSQL major-version upgrade through the existing cluster's maintenance procedure first.
Synapse uses the distribution's Python packages. Do not install its dependencies into Odoo's virtual environment. Element Web is prebuilt static content; no Node.js build environment is needed to serve the Arch package.
Element Web is an external frontend dependency, not an asset copied into the Odoo addon or a Python package. See Element Web setup and updates for the package ownership, embedding boundary, and frontend update procedure.
Confirm the installed layout:
pacman -Q matrix-synapse element-web postgresql
pacman -Ql matrix-synapse
pacman -Ql element-web
getent passwd synapse
systemctl cat synapse.service
The documented Arch layout uses the synapse service user, synapse.service,
/etc/synapse/homeserver.yaml, and /var/lib/synapse. Element files are under
/usr/share/webapps/element/. If the installed package changes these paths, use
its actual layout rather than creating a competing service.
Sources: ArchWiki Matrix, Synapse package, and Element file list.
4. Add a dedicated Synapse database
4.1 Check names before creating anything
Inside the LXC, verify the proposed names are unused:
sudo -u postgres psql -X -d postgres -c "SELECT datname FROM pg_database WHERE datname = 'matrix_synapse';"
sudo -u postgres psql -X -d postgres -c "SELECT rolname FROM pg_roles WHERE rolname = 'matrix_synapse';"
If either exists, identify its owner and purpose. These instructions are for a new Synapse database, not for replacing or adopting an existing unknown database.
4.2 Create the role and database
Use an independently generated database password and save it in the deployment's secret store. The password prompt avoids putting it in shell arguments/history.
sudo -u postgres createuser --login --no-superuser --no-createdb --no-createrole --pwprompt matrix_synapse
sudo -u postgres createdb --owner=matrix_synapse --encoding=UTF8 --locale=C --template=template0 matrix_synapse
sudo -u postgres psql -X -d postgres -c 'REVOKE CONNECT ON DATABASE matrix_synapse FROM PUBLIC;'
sudo -u postgres psql -X -d postgres -c 'GRANT CONNECT ON DATABASE matrix_synapse TO matrix_synapse;'
The new database uses UTF-8 and C collation/ctype. Existing Odoo databases retain
their own locales and owners. If template0 uses ICU or another locale provider,
select --locale-provider=libc when creating the new database on PostgreSQL 15 or
newer; check createdb --help for the installed version's options. Confirm only
the new database's settings:
sudo -u postgres psql -X -d postgres -c "SELECT datname, pg_encoding_to_char(encoding), datcollate, datctype FROM pg_database WHERE datname = 'matrix_synapse';"
See Synapse PostgreSQL requirements.
4.3 Restrict authentication to the new database
Edit the existing pg_hba.conf identified by SHOW hba_file. Insert these
role-specific entries before broader matching entries; leave existing Odoo
rules in place:
host matrix_synapse matrix_synapse 127.0.0.1/32 scram-sha-256
host all matrix_synapse 0.0.0.0/0 reject
host all matrix_synapse ::/0 reject
local all matrix_synapse reject
The Synapse connection below uses TCP 127.0.0.1, not a Unix socket. These rules
allow its dedicated role only into matrix_synapse through that connection.
If the cluster still generates MD5 password verifiers, configure SCRAM for this
role and reset its password interactively before using scram-sha-256.
Check the edited rules and reload PostgreSQL configuration without restarting the whole cluster:
sudo -u postgres psql -X -d postgres -c 'SELECT line_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;'
sudo -u postgres psql -X -d postgres -c 'SELECT pg_reload_conf();'
psql -X -h 127.0.0.1 -U matrix_synapse -d matrix_synapse -W -c 'SELECT current_database(), current_user;'
Do not reload until the rule inspection reports no errors. HBA rule order is significant; PostgreSQL uses the first matching rule. PostgreSQL authentication reference.
5. Configure and start Synapse
5.1 Generate a fresh configuration
Run as root inside the LXC. Confirm there is no existing Synapse configuration or signing key to preserve before running these fresh-install commands:
install -d -o root -g synapse -m 0750 /etc/synapse
install -d -o synapse -g synapse -m 0750 /var/lib/synapse
generate_config --server-name example.com --config-dir /etc/synapse --data-dir /var/lib/synapse --report-stats no --generate-secrets --output-file /etc/synapse/homeserver.yaml
generate_log_config -o /etc/synapse/example.com.log.config -f /var/lib/synapse/homeserver.log
generate_signing_key --output_file /etc/synapse/example.com.signing.key
chown root:synapse /etc/synapse/homeserver.yaml /etc/synapse/example.com.log.config /etc/synapse/example.com.signing.key
chmod 0640 /etc/synapse/homeserver.yaml /etc/synapse/example.com.log.config /etc/synapse/example.com.signing.key
The generated signing key is permanent server identity material. Back it up and never regenerate it as an ordinary upgrade step.
5.2 Edit the generated configuration
Edit /etc/synapse/homeserver.yaml. Replace the corresponding sections with
the following values; do not append duplicate YAML keys or discard generated
secrets. Replace the database-password placeholder privately.
server_name: "example.com"
public_baseurl: "https://matrix.example.com/"
report_stats: false
log_config: "/etc/synapse/example.com.log.config"
signing_key_path: "/etc/synapse/example.com.signing.key"
media_store_path: "/var/lib/synapse/media"
enable_registration: false
url_preview_enabled: false
max_upload_size: "50M"
listeners:
- port: 8008
type: http
tls: false
bind_addresses:
- "127.0.0.1"
- "10.20.0.20"
x_forwarded: true
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
host: "127.0.0.1"
port: 5432
user: "matrix_synapse"
password: "<REPLACE_WITH_THE_DATABASE_PASSWORD>"
database: "matrix_synapse"
cp_min: 2
cp_max: 10
Keep the generated registration_shared_secret for the administrative account
creation step. Do not expose it in Element configuration, source control, or Odoo
settings. Public self-registration remains disabled.
Port 8008 must be reachable from the front proxy at 10.20.0.1, but not from
arbitrary public clients. Loopback-only binding would prevent the separate proxy
from reaching Synapse. x_forwarded: true assumes only trusted proxies can reach
the listener and supply forwarded headers.
The example enables federation APIs for discovery, federation, and Matrix OpenID validation used by calling services. If public federation is intentionally restricted, retain the required OpenID endpoints and apply the deployment's explicit peer policy; merely removing all server-server endpoints can break calls.
Proxy behavior is described in the Synapse reverse-proxy guide.
5.3 Start the packaged service
systemctl enable --now synapse.service
systemctl status synapse.service
curl --fail http://127.0.0.1:8008/_matrix/client/versions
A successful versions response establishes local API reachability, not completed public federation or calling setup. If startup fails, inspect locally:
journalctl -u synapse.service -n 100 --no-pager
The package's service sandbox may restrict writes to /var/lib/synapse. Keeping
media and logs there avoids widening its writable paths. Resolve any package-unit
differences through a narrow systemd drop-in rather than disabling its sandbox.
6. Deploy Element Web inside the LXC
6.1 Maintain configuration separately from package assets
Element has no backend service process. Nginx serves the prebuilt Arch files.
Create /etc/webapps/element/config.json if it does not already exist, using
the packaged sample as a starting point:
install -d -m 0755 /etc/webapps/element
if [ ! -e /etc/webapps/element/config.json ]; then
cp /etc/webapps/element/config.sample.json /etc/webapps/element/config.json
fi
Preserve an existing configuration. A minimal public configuration is:
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "example.com"
}
},
"disable_custom_urls": true,
"disable_guests": true,
"brand": "Example Matrix",
"default_country_code": "SI",
"element_call": {
"disable": false,
"use_exclusively": true
}
}
use_exclusively selects Element Call instead of relying on legacy one-to-one or
Jitsi calling. Apply it when completing the RTC section; chat can be commissioned
first. Check supported options against the installed Element release.
This file is publicly downloadable. It must contain no passwords, access tokens, OIDC client secrets, LiveKit API secrets, or recovery keys. Do not place local overrides in the package-managed webroot; the Nginx alias below serves the separately maintained configuration instead.
chmod 0644 /etc/webapps/element/config.json
jq empty /etc/webapps/element/config.json
6.2 Configure the internal Nginx
Inside the LXC, inspect /etc/nginx/nginx.conf. In its existing http block,
include the directory used below if it is not already included:
include /etc/nginx/conf.d/*.conf;
Also retain the usual include /etc/nginx/mime.types; in the http block.
Confirm the installed MIME map includes application/wasm for .wasm files
and a JavaScript MIME type for .js files. Incorrect types can prevent the
browser's encryption or calling code from loading, especially with nosniff.
Create the directory and /etc/nginx/conf.d/element.conf. If another Nginx already
runs in the LXC, add this server to that instance rather than replacing its main
configuration. The example uses private port 8081 to avoid existing listeners.
server {
listen 10.20.0.20:8081;
server_name element.example.com;
root /usr/share/webapps/element;
index index.html;
allow 10.20.0.1;
allow 127.0.0.1;
deny all;
location = /config.json {
alias /etc/webapps/element/config.json;
default_type application/json;
add_header Cache-Control "no-store" always;
}
location ~ ^/config\.[^/]+\.json$ {
return 404;
}
location / {
try_files $uri $uri/ =404;
add_header Cache-Control "no-cache" always;
}
}
This deliberately simple caching policy also covers /, /index.html, /version,
and language files. A missing host-specific config.element.example.com.json
returns 404 so Element falls back to /config.json. Hash-based room navigation
does not require rewriting missing asset URLs to HTML.
install -d -m 0755 /etc/nginx/conf.d
nginx -t
systemctl enable --now nginx.service
If Nginx was already running, use systemctl reload nginx.service after the
successful configuration check. From the front proxy, inspect the upstream:
curl --fail -H 'Host: element.example.com' http://10.20.0.20:8081/config.json
Review Element's deployment and caching requirements when optimizing the caching policy later.
7. Configure the front Nginx
All examples in this section belong to the existing front proxy, not the LXC. Keep Proxmox management routes and the existing Odoo server block intact. Add dedicated Matrix virtual hosts using the proxy's established include layout.
7.1 Obtain HTTPS certificates
Use the front proxy's existing ACME/certificate-management workflow. Issue a
certificate covering matrix.example.com, element.example.com, and
rtc.example.com. The examples below use one certificate named
matrix.example.com. Adjust paths if separate certificates are used.
For an existing Certbot webroot workflow, first create an HTTP server with the
following location and a configured /var/lib/acme webroot:
server {
listen 80;
server_name matrix.example.com element.example.com rtc.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/lib/acme;
}
location / {
return 301 https://$host$request_uri;
}
}
Enable that HTTP configuration before obtaining the certificate. Do not enable TLS server blocks referencing certificate files that do not yet exist. If Certbot is already the deployed ACME client:
install -d -m 0755 /var/lib/acme
nginx -t
systemctl reload nginx
certbot certonly --webroot -w /var/lib/acme --cert-name matrix.example.com -d matrix.example.com -d element.example.com -d rtc.example.com
Manage example.com through its existing certificate workflow. Obtain a separate
turn.example.com certificate when adding TURN/TLS. Configure renewal and Nginx
reload through the existing certificate manager; do not install a competing ACME
service on the same host. See Certbot instructions.
7.2 Matrix API virtual host
Create a TLS server for matrix.example.com:
server {
listen 443 ssl;
server_name matrix.example.com;
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 50m;
access_log off;
location ^~ /_matrix/ {
proxy_pass http://10.20.0.20:8008;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
location ^~ /_synapse/client/ {
proxy_pass http://10.20.0.20:8008;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
access_log off;
}
location ^~ /_synapse/admin/ {
allow 10.20.0.20;
deny all;
proxy_pass http://10.20.0.20:8008;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
return 404;
}
}
There is intentionally no trailing slash on the Synapse proxy_pass URLs.
Preserve Matrix URI encoding; rewriting or normalizing signed federation paths
can break authentication. The request body limit matches Synapse's upload limit.
The admin API is reachable only from the example Odoo LXC address. Add another explicit management source only if needed. With NAT or another trusted proxy in front, confirm the actual source Nginx sees before relying on this allowlist. Do not make the admin API globally reachable to simplify setup.
OIDC callback access logging is disabled here to avoid retaining authorization codes in ordinary URL logs. Apply an equivalent redacted-log policy if request logs are required. Keep application diagnostics private as well.
7.3 Element virtual host and embedding policy
server {
listen 443 ssl;
server_name element.example.com;
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://odoo.example.com" always;
add_header Permissions-Policy 'camera=(self), microphone=(self), display-capture=(self), fullscreen=(self), autoplay=(self)' always;
location / {
proxy_pass http://10.20.0.20:8081;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
access_log off;
}
}
Element should retain its own browser origin, separate from Odoo. The CSP permits
embedding only by itself and the named Odoo origin. A conflicting X-Frame-Options:
SAMEORIGIN or DENY header prevents cross-origin embedding. proxy_hide_header
removes an upstream header; it does not remove headers added by the front Nginx.
Check global includes and final responses for conflicting policies.
If a stronger existing CSP is present, amend its frame-ancestors directive while
preserving the other protections. Multiple CSP headers are enforced together,
so adding a second permissive header cannot override a restrictive first one.
Do not include a generic security snippet that reintroduces a conflicting frame
policy. Nginx add_header inheritance also requires attention when adding more
location-level headers.
Sources: Element security notes and Nginx header inheritance.
7.4 Matrix discovery on the identity domain
Add these two locations to the existing HTTPS server for example.com:
location = /.well-known/matrix/server {
default_type application/json;
add_header Cache-Control "public, max-age=3600" always;
return 200 '{"m.server":"matrix.example.com:443"}';
}
location = /.well-known/matrix/client {
default_type application/json;
add_header Access-Control-Allow-Origin "*" always;
add_header Cache-Control "public, max-age=300" always;
return 200 '{"m.homeserver":{"base_url":"https://matrix.example.com"}}';
}
The wildcard CORS header exposes public discovery metadata, not credentials.
Preserve any necessary site security headers when merging these locations.
Federation delegates to HTTPS port 443, so this layout does not require a
separate public 8448 listener. MatrixRTC OpenID validation can follow this
delegation too. This differs from deployment playbooks that assume port 8448.
7.5 Odoo's parent-page media policy
If the existing Odoo server sends a restrictive Permissions-Policy, update it
to delegate media to Element while retaining Odoo's own permissions:
add_header Permissions-Policy 'camera=(self "https://element.example.com"), microphone=(self "https://element.example.com"), display-capture=(self "https://element.example.com"), fullscreen=(self "https://element.example.com"), autoplay=(self "https://element.example.com")' always;
Merge this into the existing policy, rather than emitting duplicate incompatible
headers. If Odoo already has a CSP, its frame-src must permit
https://element.example.com; preserve other existing frame sources. For a calling
widget hosted on a different origin, add that actual UI origin to the policies
and to the addon's Element Call Origin field. The SFU/backend URL is not a
calling-widget UI origin.
7.6 Activate and inspect
On the front proxy:
nginx -t
systemctl reload nginx
curl --fail https://matrix.example.com/_matrix/client/versions
curl --fail https://example.com/.well-known/matrix/server
curl --fail https://example.com/.well-known/matrix/client
curl --fail https://element.example.com/config.json
curl --head https://element.example.com/
From an external browser, open https://element.example.com. Confirm that it
selects the intended homeserver. Repeat the public API checks from inside the
LXC to catch split-DNS or hairpin-routing failures. Never use curl -k as the
normal solution to certificate errors.
8. Create initial Matrix accounts
Run inside the LXC as the Synapse user, with interactive password prompts:
sudo -u synapse register_new_matrix_user -c /etc/synapse/homeserver.yaml http://127.0.0.1:8008
Create a dedicated administrator for server administration. Repeat the command to create non-admin accounts for initial standalone chat/call checks and a non-admin Odoo room-management integration account. Choose administrator access only for the account that genuinely needs the Synapse admin API.
Do not supply passwords on command lines or put tokens in URLs. Keep account credentials in a secret store. The generated registration shared secret grants powerful account-creation authority even when public registration is disabled.
Sign two ordinary users into Element, create an encrypted private room, invite
the other user, and exchange messages. Complete Element's device verification
and recovery setup. This checks the independent Matrix deployment before linking
Odoo's reviewed identities; a manually created @alice:example.com account is
not automatically the addon's UUID-derived person account.
For SMTP/password reset, configure Synapse's email section using the deployed
mail service's documented settings. Until SMTP or an alternative recovery process
is configured, password-reset emails are unavailable. Encryption recovery keys
remain separate from account-password recovery.
9. Add WebRTC and conferencing
9.1 Choose a compatible version set
Deploy a maintained LiveKit release and MatrixRTC authorization service alongside
Synapse and Element. The native release examples below use LiveKit v1.13.9
and lk-jwt-service v0.8.0, available when this guide was written. These are
explicit download examples, not a claim that this complete deployment has been
tested or that these versions should remain pinned indefinitely.
The authorization service's v0.8.0 release includes an OpenID validation fix;
do not select an older affected release simply to match an older tutorial.
Release notes.
There are two RTC authorization interfaces:
- Current clients can use homeserver-proxied application-service endpoints and
the transport's
urlvalue. - Older clients use
livekit_service_urland the legacy/sfu/getendpoint. Upstream deprecates this interface but retains it for compatibility.
The example wires the application service and advertises both transport values. An older packaged Element can use the legacy path while newer clients use the homeserver-mediated path. Verify the selected Synapse version supports MSC4502 and MSC4512; do not assume arbitrary older versions support these flags. See Synapse MatrixRTC configuration.
The inspected Synapse v1.162.0 sources support both transport fields and the
appservice flags used here:
transport schema
and experimental configuration.
This establishes configuration support, not live interoperability of the complete
version set.
9.2 Install native release binaries
Inside the LXC, confirm the machine architecture with uname -m. The commands
below target x86-64; choose matching upstream assets for ARM. Download into a
fresh operator-owned staging directory:
mkdir -m 0700 matrix-rtc-downloads
cd matrix-rtc-downloads
curl --fail --location --output livekit.tar.gz https://github.com/livekit/livekit/releases/download/v1.13.9/livekit_1.13.9_linux_amd64.tar.gz
curl --fail --location --output lk-jwt-service https://github.com/element-hq/lk-jwt-service/releases/download/v0.8.0/lk-jwt-service_linux_amd64
tar -tzf livekit.tar.gz
Before installation, verify the files using the selected release's published checksums/signatures where available and inspect the archive paths. Download directly from the release pages; do not pipe an unreviewed remote script to a root shell. Extract the verified archive into this staging directory, then:
tar -xzf livekit.tar.gz
sudo install -o root -g root -m 0755 livekit-server /usr/local/bin/livekit-server
sudo install -o root -g root -m 0755 lk-jwt-service /usr/local/bin/lk-jwt-service
Adjust the extracted binary path if the archive's layout differs. Use a separate
operator-managed update procedure for these binaries; pacman does not manage
files installed under /usr/local/bin.
Release sources: LiveKit and MatrixRTC authorization service.
9.3 Create service users and configuration directories
As root inside the LXC, first confirm these names are unused, then create them:
getent passwd livekit
getent passwd matrixrtc
getent group matrixrtc-registration
For a new setup:
useradd --system --user-group --home-dir /var/lib/livekit --shell /usr/bin/nologin livekit
useradd --system --user-group --home-dir /var/lib/matrixrtc --shell /usr/bin/nologin matrixrtc
groupadd --system matrixrtc-registration
usermod -aG matrixrtc-registration synapse
usermod -aG matrixrtc-registration matrixrtc
install -d -o root -g livekit -m 0750 /etc/livekit
install -d -o root -g matrixrtc-registration -m 0750 /etc/matrix-rtc
install -d -o livekit -g livekit -m 0750 /var/lib/livekit
install -d -o matrixrtc -g matrixrtc -m 0750 /var/lib/matrixrtc
The shared group permits reading only the appservice registration. Do not add the RTC service to Synapse's general group, which could expose its database credentials.
9.4 Configure LiveKit and TURN
Generate a unique LiveKit API key and a strong independent API secret through the deployment's secret-management workflow. Use the same pair in LiveKit and the authorization service. These are backend secrets, never Element configuration.
Create /etc/livekit/livekit.yaml:
port: 7880
rtc:
tcp_port: 7881
udp_port: 7882
use_external_ip: false
node_ip: "203.0.113.10"
keys:
"<REPLACE_WITH_LIVEKIT_API_KEY>": "<REPLACE_WITH_LIVEKIT_API_SECRET>"
room:
auto_create: false
webhook:
api_key: "<REPLACE_WITH_LIVEKIT_API_KEY>"
urls:
- "http://127.0.0.1:8080/sfu_webhook"
turn:
enabled: true
domain: "turn.example.com"
udp_port: 3478
tls_port: 5349
cert_file: "/etc/livekit/turn.fullchain.pem"
key_file: "/etc/livekit/turn.key.pem"
relay_range_start: 30000
relay_range_end: 30100
allow_restricted_peer_cidrs:
- "10.20.0.20/32"
Replace node_ip with the publicly reachable media address; the example disables
automatic public-IP discovery and explicitly advertises the selected NAT address.
Forward the listed ports without translating their port numbers. Do not also set
port_range_start/port_range_end under rtc when using this UDP mux configuration.
The TURN allowlist is limited to the exact private SFU address for this layout; it is not permission to relay to all private networks. Adapt it only if the actual SFU candidates require another reviewed address. The relay-range size is an example capacity choice and may need expansion for simultaneous calls.
Install a valid turn.example.com certificate and its private key at the two
configured LXC paths, readable by livekit but not world-readable. Transfer and
renew these through the existing certificate-management system; do not copy the
front proxy's entire private certificate store into the LXC. LiveKit terminates
TURN/TLS itself in this example. Restart it after certificate renewal.
chown root:livekit /etc/livekit/livekit.yaml /etc/livekit/turn.fullchain.pem /etc/livekit/turn.key.pem
chmod 0640 /etc/livekit/livekit.yaml /etc/livekit/turn.fullchain.pem /etc/livekit/turn.key.pem
The webhook is signed using the configured API key/secret and stays on loopback. Disabling automatic room creation leaves authorization-controlled creation to the RTC service. Review the selected release's LiveKit configuration reference for changes to these options.
9.5 Configure the MatrixRTC authorization service
Create /etc/matrix-rtc/livekit-key and /etc/matrix-rtc/livekit-secret, each
containing the corresponding value from LiveKit. Store them as root:matrixrtc,
mode 0640. Use a private editor/secret manager; do not include real values in
shell history or repository files.
Create /etc/matrix-rtc/registration.yaml, replacing the two token placeholders
with two distinct strong random tokens:
id: "matrixrtc-livekit"
as_token: "<REPLACE_WITH_APPSERVICE_TOKEN>"
hs_token: "<REPLACE_WITH_HOMESERVER_TOKEN>"
sender_localpart: "_matrixrtc_service"
namespaces:
users:
- exclusive: false
regex: ".*"
url: null
io.element.msc4502.scopes:
- "urn:matrix:client:io.element.msc4502:rooms:is_joined"
io.element.msc4512.proxy_prefix: "rtc/livekit"
io.element.msc4512.proxy_url: "http://127.0.0.1:8080"
The namespace and scope let this appservice inspect local room membership; registration tokens therefore require privileged custody. The null event URL avoids subscribing it to room-event traffic.
Create /etc/matrix-rtc/service.env:
LIVEKIT_URL=wss://rtc.example.com/livekit/sfu
LIVEKIT_KEY_FROM_FILE=/etc/matrix-rtc/livekit-key
LIVEKIT_SECRET_FROM_FILE=/etc/matrix-rtc/livekit-secret
LIVEKIT_JWT_BIND=127.0.0.1:8080
LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com
LIVEKIT_HS_SERVER_NAME=example.com
LIVEKIT_AS_REGISTRATION_FILE=/etc/matrix-rtc/registration.yaml
LIVEKIT_SANITY_CHECK_INTERVAL_SECONDS=30
LIVEKIT_LOG_LEVEL=info
The full-access value is the permanent Matrix server name, not the API hostname.
It restricts creation through the legacy interface; appservice endpoints use
their homeserver authorization instead. Do not use * or disable TLS verification.
chown root:matrixrtc-registration /etc/matrix-rtc/registration.yaml
chmod 0640 /etc/matrix-rtc/registration.yaml
chown root:matrixrtc /etc/matrix-rtc/livekit-key /etc/matrix-rtc/livekit-secret
chmod 0640 /etc/matrix-rtc/livekit-key /etc/matrix-rtc/livekit-secret
chown root:root /etc/matrix-rtc/service.env
chmod 0600 /etc/matrix-rtc/service.env
Systemd reads the environment file before dropping privileges. The service reads
the key and registration files through its restricted groups. Without a configured
Redis-compatible store, authorization-service pending leave state is in memory
and is lost on restart. For persistent delegated-leave handling, configure a
dedicated authenticated Redis-compatible endpoint with LIVEKIT_REDIS_URL using
the selected release's persistence instructions; do not expose that endpoint
publicly or silently adopt Odoo's existing cache instance.
Configuration source: lk-jwt-service v0.8.0.
9.6 Add systemd units
Create /etc/systemd/system/livekit.service:
[Unit]
Description=LiveKit MatrixRTC SFU
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=livekit
Group=livekit
WorkingDirectory=/var/lib/livekit
ExecStart=/usr/local/bin/livekit-server --config /etc/livekit/livekit.yaml
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/livekit
UMask=0077
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Create /etc/systemd/system/matrixrtc.service:
[Unit]
Description=MatrixRTC LiveKit Authorization Service
Wants=network-online.target
After=network-online.target livekit.service
[Service]
Type=simple
User=matrixrtc
Group=matrixrtc
SupplementaryGroups=matrixrtc-registration
WorkingDirectory=/var/lib/matrixrtc
EnvironmentFile=/etc/matrix-rtc/service.env
ExecStart=/usr/local/bin/lk-jwt-service
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/matrixrtc
UMask=0077
[Install]
WantedBy=multi-user.target
These units run without root and without privileged network ports. Their filesystem restrictions must be compatible with the LXC's systemd configuration. Diagnose specific denial messages rather than making the LXC privileged or broadly removing service protections.
9.7 Add RTC routing on the LXC Nginx
The authorization service remains loopback-only. Add this separate server in the
LXC's Nginx http configuration:
server {
listen 10.20.0.20:8082;
server_name rtc.example.com;
allow 10.20.0.1;
deny all;
location ^~ /livekit/jwt/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
access_log off;
}
location / {
return 404;
}
}
The trailing slash here intentionally strips the public /livekit/jwt/ prefix.
This behavior is different from Synapse's signed-path proxying. The front proxy
overwrites forwarded headers; only that trusted proxy can reach this internal port.
9.8 Add RTC routing on the front Nginx
In the front proxy's http context, define this map once if no equivalent exists:
map $http_upgrade $matrix_connection_upgrade {
default upgrade;
'' close;
}
Add the RTC TLS server:
server {
listen 443 ssl;
server_name rtc.example.com;
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location ^~ /livekit/sfu/ {
proxy_pass http://10.20.0.20:7880/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $matrix_connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
access_log off;
}
location ^~ /livekit/jwt/ {
proxy_pass http://10.20.0.20:8082;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
access_log off;
}
location / {
return 404;
}
}
The SFU prefix is stripped before reaching LiveKit; requests such as
/livekit/sfu/rtc reach its /rtc route. The JWT prefix remains intact until the
internal Nginx strips it. Do not independently change these prefixes without
updating the advertised URLs. WebSocket forwarding needs the explicit upgrade
headers. Nginx WebSocket reference.
9.9 Configure Synapse RTC discovery and appservice wiring
Merge the following into the existing /etc/synapse/homeserver.yaml sections:
experimental_features:
msc3266_enabled: true
msc4143_enabled: true
msc4222_enabled: true
msc4502_enabled: true
msc4512_enabled: true
max_event_delay_duration: 24h
rc_message:
per_second: 0.5
burst_count: 30
rc_delayed_event_mgmt:
per_second: 1
burst_count: 20
app_service_config_files:
- "/etc/matrix-rtc/registration.yaml"
matrix_rtc:
transports:
- type: livekit
url: "wss://rtc.example.com/livekit/sfu"
livekit_service_url: "https://rtc.example.com/livekit/jwt"
Preserve other appservice registrations if present. The url and
livekit_service_url are two interfaces to the same transport, not two separate
conferences. Experimental features, endpoint schemas, and client support evolve;
review the installed version's notes before changing them.
For clients still using well-known focus discovery, replace the client discovery
response in the existing example.com HTTPS server with:
location = /.well-known/matrix/client {
default_type application/json;
add_header Access-Control-Allow-Origin "*" always;
add_header Cache-Control "public, max-age=300" always;
return 200 '{"m.homeserver":{"base_url":"https://matrix.example.com"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://rtc.example.com/livekit/jwt"}]}';
}
The appservice integration and webhook allow delegated leave handling. Reviewed Matrix room membership controls calling access; Odoo has no separate call approvals or invitation leases. Refer to Element Call hosting requirements when checking discovery and delayed-event behavior.
9.10 Route media and TURN ports
For the exact LiveKit configuration above, configure these routes through the upstream router/Proxmox networking and the LXC firewall:
| Public destination | Destination inside LXC | Purpose |
|---|---|---|
7882/UDP |
10.20.0.20:7882/UDP |
Direct WebRTC media, UDP mux |
7881/TCP |
10.20.0.20:7881/TCP |
Direct ICE/TCP fallback |
3478/UDP |
10.20.0.20:3478/UDP |
Embedded TURN/UDP |
5349/TCP |
10.20.0.20:5349/TCP |
Embedded TURN/TLS |
30000–30100/UDP |
Same ports on 10.20.0.20 |
Configured TURN relay range |
Use destination-NAT port forwards if the LXC has only a private address. If the public address belongs to the Proxmox host, that host needs the corresponding forwarding/NAT rules as well as firewall permission. Preserve existing bridge, management, and Odoo rules; do not replace the Proxmox firewall with a generic internet example. Ensure return routing and LAN hairpin access work.
Restrict private HTTP ports 8008, 8081, 8082, and 7880 to the front proxy
and necessary local connections. Keep PostgreSQL 5432 and authorization-service
8080 off the public network. TURN/media ports need actual client reachability.
Port 5349 works only on networks that permit it. To support networks allowing
only TCP 443, give TURN/TLS a separate public IP on port 443, or deliberately
configure an L4/SNI routing arrangement. Ordinary Nginx HTTP proxy_pass cannot
proxy TURN. Do not bind LiveKit's TURN listener to a port already owned by the
front HTTPS proxy. If changing the public TURN port, update the listener and
advertised configuration coherently.
See LiveKit ports and firewall.
9.11 Start and commission conferencing
After completing certificate, proxy, and firewall configuration:
- Inside the LXC, run
nginx -tand reload its Nginx. - On the front proxy, run
nginx -tand reload its Nginx. - Inside the LXC, start the new native services and restart Synapse to load the registration and updated group membership:
systemctl daemon-reload
systemctl enable --now livekit.service matrixrtc.service
systemctl restart synapse.service
systemctl status livekit.service matrixrtc.service synapse.service
- Check Matrix discovery again from both outside and inside the LXC. Confirm the authorization service can reach the public Matrix and SFU URLs with valid TLS; local health alone does not prove this.
- In Element, use two separate user sessions/devices to join an encrypted room call. Check audio, video, screen sharing, mute, leave, and reconnect.
- Repeat from a different network, such as mobile data. A same-LAN call alone does not exercise public NAT and TURN connectivity.
- Check that participants disappearing after an interrupted connection do not leave permanently stuck call membership.
The Arch Element package inspected for this guide includes
widgets/element-call/. For calls inside Element, use its packaged widget;
there is no need to deploy an unrelated standalone guest-call frontend. If its
version cannot use the selected transport, upgrade the client through a maintained
release path and recheck compatibility instead of inventing unsupported Labs flags.
10. Connect the Odoo addon
Server deployment does not automatically create Odoo federation identities or grant users company access. Configure the installed addon through its reviewed workflows; see the module README.
10.1 Configure the target and company connection
- Create the verified Matrix target under Federation > Matrix.
- Set Server Name to
example.com, Homeserver URL tohttps://matrix.example.com, and Element URL tohttps://element.example.com. - Select the reviewed instance and Directory Owner company.
- For the packaged same-origin Element Call widget, leave Element Call Origin
empty. Set it only if the actual calling UI is hosted on another HTTPS origin;
do not put
https://rtc.example.comthere merely because it hosts the SFU. - In that company's Settings > Matrix, enable integration, select the target, and enter the full dedicated integration user ID and its personal Matrix client access token. This account manages rooms and memberships; an ordinary person's session must not be reused for it.
- Enter a separate administrator token only for account administration. Retrieve tokens through a trusted Matrix client/login workflow and store them directly in the protected settings; do not put them in this guide, logs, jobs, or URLs.
- If split DNS resolves the HTTPS API to
10.20.0.1, enter that exact address in Approved Matrix IP Addresses. Approve the resolved HTTPS destination, not the LXC's plain-HTTP upstream address. TLS hostname verification remains mandatory. Include every intended resolved address if using dual-stack DNS. - Save and choose Test Saved Connection.
10.2 Configure shared login and provisioning deliberately
The current connector provisions permanent user IDs derived from verified person UUIDs and binds them to an existing Synapse OIDC provider. It does not automatically adopt manually named accounts or configure Synapse's login provider.
To provision people through the addon:
- Configure the actual OIDC provider in Synapse and verify its issuer, client registration, and subject mapping. The subject must match the reviewed person UUID used by the federation provider.
- If using
pmis_federation_provider, register the Matrix login client with the exact callbackhttps://matrix.example.com/_synapse/client/oidc/callbackand the target's reviewed instance. Follow the existing shared-login contract for provider/client setup and key custody. - Enter the actual existing Synapse OIDC Provider ID in company settings. Determine the identifier used by Synapse for external account bindings; do not assume it equals a display label or arbitrarily invent it.
- Create and provision the verified Matrix person identity. Then sign into Element through the configured provider as that same reviewed person.
- Provision organization Spaces/rooms and separately approve each membership. Use Open in Odoo or Matrix Client after native mappings and provisioning are current.
Consult Synapse OIDC configuration for the server-side provider parameters. This deployment guide intentionally does not guess an issuer URL or client secret from an Odoo hostname.
10.3 Understand the current integration boundary
The embedded client retains its own Matrix session and encryption storage. Odoo cannot inspect the cross-origin Element account, and loading the iframe does not automatically log that account in. Verify the displayed identity and complete authentication/recovery inside Element.
The addon checks Odoo identity/company scope periodically and unloads the iframe when checks fail. This does not revoke the independent Matrix session or enforce an SFU-side admission lease. Element and the MatrixRTC deployment own call admission, moderation, and disconnection. Matrix room withdrawal does not guarantee immediate removal of an already connected SFU participant. The encrypted Discuss relay uses a dedicated nio device, independently of Element and LiveKit. Configure its token, verified device fingerprints, and protected persistent key store through the module's bridge setup; installing dependencies alone does not configure it.
11. Operational checks
Perform these checks before making the deployment available to ordinary users:
| Check | Expected result |
|---|---|
| Existing Odoo login and application access | Continue working after host maintenance |
| PostgreSQL | Existing Odoo databases unchanged; dedicated Synapse role/database |
| Public Matrix API | Valid TLS; versions response through front Nginx |
| Identity-domain discovery | Correct API hostname and federation delegation |
| Element configuration | Intended homeserver; no embedded secrets |
| Element direct browser login | Two users can exchange encrypted text |
| Odoo iframe | Loads with allowed framing/media headers; no conflicting X-Frame-Options |
| Synapse admin API | Denied from unapproved public sources |
| Conferencing | Works across different networks, with TURN fallback where required |
| Interrupted calls | Membership converges after connection loss/reconnect |
| Addon connection | Saved connection check succeeds for the intended company |
| Recovery | Verified device/recovery workflow restores encrypted history |
Keep logs and credentials local to the administrator. Redact tokens, usernames, message content, authorization codes, and private infrastructure details before sharing diagnostics. Do not treat an HTTP 200 response from a health endpoint as proof of working encrypted chat or WebRTC media.
12. Updates, backups, and recovery
12.1 Native Arch updates
Review relevant Arch news and Synapse/Element release notes, make backups, then perform a planned full system upgrade inside the LXC:
pacman -Syu
Arch does not support partial system upgrades. Review .pacnew files and merge
changes without discarding local configuration. Recheck the Odoo interpreter and
PostgreSQL version as part of this shared-host maintenance.
After compatible package/configuration updates, restart Synapse and reload Nginx. Element has no separate process to restart; browser reloads fetch the new assets and public configuration. The conservative cache policy above makes those files revalidate. Odoo does not install, update, or pin the separately hosted Element.
12.2 RTC binary updates
Download and verify explicit maintained releases. Install replacement binaries through the same operator-controlled process and restart the corresponding services. Review RTC endpoint changes together with Synapse and Element compatibility. Retain the previous binaries and version inventory until the new deployment is accepted; do not assume a schema-changing upgrade is reversible by binary rollback.
12.3 PostgreSQL major upgrades
A PostgreSQL major upgrade affects the shared cluster, including Odoo and Synapse.
Plan it as one coordinated database maintenance operation using the deployment's
approved PostgreSQL upgrade procedure. Stop dependent application services as
required, preserve all databases, and validate both applications afterward.
Do not point new PostgreSQL binaries at an old data directory or run initdb
over it. See Arch PostgreSQL.
12.4 Backups
Include the following in protected backups:
- The dedicated Synapse PostgreSQL database and the required role definitions.
- Synapse configuration, permanent signing key, and media store.
- Element's separately maintained configuration and the deployed version inventory.
- Front/LXC Nginx configuration, certificate-management configuration, and the exact networking/NAT rules.
- LiveKit configuration, secrets, authorization-service registration, and any configured persistent authorization-service state store.
- Existing Odoo database/filestore backups through their existing procedure.
Use PostgreSQL's supported backup tools. For a consistent Synapse database/media backup, pause Synapse writes for the chosen backup window or use a documented coordinated backup method. LXC snapshots and database dumps need a coherent restore plan; test that plan on an isolated target.
Restore the same Matrix server name and signing key. Do not bring an old cloned server online beside its original with the same identity and independent history. Server backups do not replace users' end-to-end encryption recovery material. Odoo's Matrix credentials are also database-bound; copied Odoo databases require explicit administrator reconfiguration before connector jobs can execute.
13. Troubleshooting
Element returns 502
From the front proxy, request http://10.20.0.20:8081/ with the Element Host header.
Check the LXC address, internal Nginx listener, allowed proxy source, and firewall.
Do not point the Element vhost at Synapse port 8008; Synapse does not serve the
Element application.
Element loads directly but not inside Odoo
Inspect the browser console and final Element response headers. Check
frame-ancestors, Odoo's frame-src, and any inherited X-Frame-Options.
Inspect both Nginx layers. If third-party storage/authentication restrictions
interfere, use the view's Open Element link to authenticate in a top-level
tab and assess the selected browser's behavior. Do not disable browser security
globally to make the embedded client work.
Device recovery fails
Verify device cross-signing, key backup, and recovery setup inside Element. Odoo's recovery reminder records acknowledgment only; it does not hold the key or create a usable backup. Signing into an account alone does not establish access to its historical encrypted messages.
Synapse reports database or locale errors
Check the dedicated connection, HBA rule order, password verifier, UTF-8 encoding,
and C collation/ctype. Fix only the Matrix database configuration. Do not
change Odoo's database locale or enable an unsafe-locale bypass as a shortcut.
Calls have no transport or authorization fails
Check homeserver transport discovery, legacy well-known metadata where used, selected client support, and the configured RTC URLs. Check matching LiveKit API secrets and protected appservice tokens without printing them. Inspect the authorization service's ability to reach the public homeserver and SFU names.
Call connects but has no audio/video
Check browser media permissions first, then ICE candidates, public media IP, UDP/TCP forwards, TURN certificate, and TURN relay connectivity. Test from an external network. A working HTTPS/WebSocket handshake does not prove that media packets can reach LiveKit. For embedded calls, check both the Odoo parent policy and Element's policy.
Call remains visible after a participant disappears
Check delayed-event support, appservice registration, signed webhook delivery, and authorization-service restart/state behavior. Confirm all components use compatible endpoint versions rather than combining unrelated tutorial snippets.
Odoo's saved connection test fails
Check the exact company/target, integration token owner, TLS certificate, DNS, and approved resolved IP addresses. On this split-DNS layout, the approved destination is the front proxy. Check the admin-source allowlist separately when account provisioning fails but client API health succeeds.