Self-hosting the support desk
Revaal's support desk can run on your company's own server. Conversations, call recordings and the search index stay in your own database, and the server talks only to api.revaal.app, inside Iran. This page walks every step, from an empty server to a desk in use.
What runs where#
Four services run on your server: the desk's web app, its api, the knowledge store (for search) and Caddy, the web front door with its TLS certificate. If you read call recordings, a connector runs beside them. All of the data lives in your own PostgreSQL: conversations, calls and their audio, the search index and the topic tree.
The AI work (transcription, summaries and filing, the vectors search runs on) is done by Revaal. For all of it your server connects to api.revaal.app alone, and Revaal keeps none of it.
- Nothing on the internet needs to reach the server, unless you want a public name with a Let's Encrypt certificate.
- New versions install themselves at night. The database is backed up before every update, and if the new version does not come up, the previous one comes back.
- The subscription runs on one license key. The install checks in with Revaal once a day and reports usage totals, never content.
Before you start#
Revaal gives you:
| Item | What for |
|---|---|
| The license key | Activation, creating the first admin and the daily check-in. The only key that goes on the server. |
| An account for registry.revaal.app (download only) | Pulling the images, and the new versions. |
And you prepare:
- A Linux server with Docker (the next section).
- A PostgreSQL database (the Database section).
- A name for the desk, such as desk.company.ir, pointing at that server.
- Optional: your company's TLS certificate for that name. Without one, Caddy makes its own.
Server and network#
| Item | Minimum |
|---|---|
| Operating system | Ubuntu 22.04 or 24.04 (tested on 24.04) |
| CPU and memory | 2 cores, 4 GB of RAM |
| Disk | 30 GB free for Docker. The images take about 1.6 GB, and the backups taken before updates are kept here too. |
| Software | Docker Engine with the docker compose plugin (v2), curl and jq |
| Direction | Destination | Port | What for |
|---|---|---|---|
| Outbound | api.revaal.app (Iran) | 443 | License, transcription, models, vectors |
| Outbound | registry.revaal.app (Iran) | 443 | Images and new versions |
| Outbound | api.goftino.com | 443 | Only with a Goftino source |
| Internal | The phone system's FTP | 21 | Only to read call recordings |
| Internal | PostgreSQL | 5432 | The desk's data |
| Inbound | Staff machines | 443 (and 80) | Opening the desk in a browser |
With a public name and a Let's Encrypt certificate, ports 80 and 443 must be reachable from the internet so the certificate can be issued and renewed.
The PostgreSQL database#
The desk keeps its tables, its search index and its files in your database, and its backups are yours.
- PostgreSQL 18 (16 has been tested too).
- The vector (pgvector) and uuid-ossp extensions, which only a superuser can create.
- A user that owns the database: every update creates and changes the desk's tables.
- Reachable from the desk's server. If the database insists on TLS, set DB_SSLMODE=require in .env.
Your database administrator runs this once, as a superuser (choose the password yourselves):
CREATE ROLE desk LOGIN PASSWORD '...';
CREATE DATABASE desk OWNER desk;
\c desk
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";Installation#
Every command runs on the desk's server. Wherever <version> appears, write the version Revaal gives you; from then on the desk takes new versions by itself.
Log in to the registry
With the account Revaal gave you:
docker login registry.revaal.appThe install folder, and the release's files
The files come from the registry itself: docker-compose.yml, Caddyfile, .env.example and the updater, revaal-desk-update.
sudo mkdir -p /opt/revaal-desk && sudo chown "$USER" /opt/revaal-desk cd /opt/revaal-desk docker create --name bundle registry.revaal.app/onprem/desk-bundle:<version> x docker cp bundle:/bundle/. . && docker rm bundle cp .env.example .env && chmod 600 .envTwo local secrets
These stay on this server and are never sent anywhere:
for k in AUTH_JWT_SECRET ML_API_KEY; do sed -i "s/^$k=$/$k=$(openssl rand -hex 32)/" .env; doneFill in the rest of .env
As KEY=value, with no quotes:
Variable Value DESK_DOMAIN The desk's name, such as desk.company.ir DESK_TLS internal for an internal name (Caddy makes its own certificate); for a public name, an email address, so Caddy gets and renews a Let's Encrypt certificate; or your company's own: /certs/fullchain.pem /certs/privkey.pem (the files in a certs folder beside this one) DESK_VERSION The same <version>; from then on the updater moves it DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD The database from the previous section DB_SSLMODE disable, or require REVAAL_LICENSE_KEY The license key from Revaal DESK_UPDATE_WINDOW (optional) The hours, Tehran time, when updates may install on their own; 2-5 by default Pull the images and start
The first start creates the tables in the empty database, which takes a few seconds.
docker compose pull && docker compose up -d docker compose psCheck the activation
A few seconds after it starts, the install checks in with Revaal and is activated. Until then every page but sign-in is locked.
D=$(grep ^DESK_DOMAIN= .env | cut -d= -f2) curl -sk --resolve "$D:443:127.0.0.1" "https://$D/api/desk/license"The right answer: "mode":"open", and all three values under reachable are true.
Install the updater
Once. From then on new versions install themselves at night (see Updates and backups).
cd /opt/revaal-desk sudo install -m 0755 revaal-desk-update /usr/local/bin/ sudo DESK_DIR=/opt/revaal-desk revaal-desk-update install
The first admin, and colleagues#
The first admin is created once, with the license key; once anyone exists the request is refused. It asks for a name, an email, a mobile number, the company's name and a password (8 characters or more):
cd /opt/revaal-desk && D=$(grep ^DESK_DOMAIN= .env | cut -d= -f2)
read -rp 'Name: ' NAME; read -rp 'Email: ' EMAIL; read -rp 'Mobile (09...): ' MOBILE
read -rp 'Workspace (company name): ' WS; read -rsp 'Password (8+): ' PASS; echo
jq -n --arg name "$NAME" --arg email "$EMAIL" --arg mobile "$MOBILE" --arg password "$PASS" --arg workspaceName "$WS" \
'{name:$name,email:$email,mobile:$mobile,password:$password,workspaceName:$workspaceName}' |
curl -sk --resolve "$D:443:127.0.0.1" -H 'content-type: application/json' \
-H "x-desk-license: $(grep ^REVAAL_LICENSE_KEY= .env | cut -d= -f2-)" -d @- "https://$D/api/desk/setup"Then open https://<DESK_DOMAIN> in a browser and sign in with that email and password. The desk is already switched on.
- An internal certificate: with DESK_TLS=internal, browsers warn until Caddy's root is trusted on staff machines. Take the root file and distribute it, with Group Policy for example: docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./desk-root.crt
- Colleagues: the install sends no email. In People, add each person by email or mobile, copy the invite link and send it inside the company; each person sets their own password on it.
- A forgotten password: the workspace's owner opens People, chooses Password link from the person's ⋯ menu, and sends the link inside the company. It works once, for 24 hours, and signs that person out everywhere else.
- The owner's own link is made on the server (with no email, it lists the users): docker compose exec api node dist/auth/desk-password-link.js <email>
- Everyone changes their own password under Settings, in the My account tab.
Calls and Goftino#
If your desk is moving here from Revaal's cloud, connect these two only after your data has moved, together with Revaal. A Goftino source read in two places at once is read, and billed, twice.
Calls: the connector reads the recordings from your phone system's FTP (Asterisk, Issabel or FreePBX) and sends them to the desk.
The sending key
In the desk: Sources, then Push via API, then copy the key.
The connector.env file
Make it beside docker-compose.yml (chmod 600) and fill it in:
Variable Value REVAAL_KEY The key from the previous step FTP_HOST, FTP_USER, FTP_PASSWORD The phone system's FTP address, and a read-only user FTP_DIR The folder holding the year folders (2026/, 2025/ …) SINCE The first day to send calls from, YYYY-MM-DD OPERATORS Operators' names by extension: 101=Name,102=Name Start it
docker compose --profile calls up -d connectorCalls shorter than 15 seconds and calls between two extensions are not sent. The FTP password stays on this server.
If the phone system sits in another network (the desk on a cloud server and the PBX in the office, say), run the connector on a machine inside the office network and point it at the desk: the same variables in connector.env, plus REVAAL_URL=https://<DESK_DOMAIN>, and beside it this docker-compose.yml:
services:
connector:
image: registry.revaal.app/onprem/desk-connector:<version>
restart: unless-stopped
env_file: connector.env
volumes:
- connector-data:/data
volumes:
connector-data:With DESK_TLS=internal, give the connector Caddy's root too: put desk-root.crt in the container and point NODE_EXTRA_CA_CERTS at it. The connector-data volume is the record of what has been sent; keep it, or every call is sent, and paid for, again.
Goftino: in the desk, Sources, then Goftino, with your company's Goftino token.
Updates and backups#
Updates are automatic. The updater, revaal-desk-update, looks for a new version every hour and installs it at night, between 2 and 5 a.m. Tehran time. The workspace's owner can update at once, or switch the automatic updates off, under Settings, in the Version tab.
Before every update the database is backed up (pg_dump) into /opt/revaal-desk/backups, and the last five are kept. If the new version does not answer within ten minutes, the previous one comes back by itself. Regular backups of the database are still yours to run.
The three commands you may need: the status and the last update's outcome, updating right now, and the updater's log:
sudo revaal-desk-update status
sudo revaal-desk-update apply
journalctl -u revaal-desk-updateGoing back to an older version is done together with Revaal: the backup from before the update and the previous version's files (docker-compose.yml.<version>, beside the new one) come back together.
Health checks and troubleshooting#
| Check | Command | Healthy answer |
|---|---|---|
| The license, and Revaal | curl -sk https://<DESK_DOMAIN>/api/desk/license | "mode":"open", and everything under reachable is true |
| The containers | docker compose ps | api, web, ml and caddy Up |
| The application log | docker compose logs api --tail 200 | A check-in ok line |
- A false under reachable: that part of Revaal is not answering right now, or the server cannot reach api.revaal.app. The work that needs it waits and resumes by itself; nothing is lost. Check outbound 443 to api.revaal.app.
- A mode other than open after installation: lastError in the same answer says why (a wrong key, Revaal unreachable, a suspended license).
- A database error on the first start (in the api log): the connection (DB_HOST, the network, DB_SSLMODE), a wrong password, or extension "vector" is not available, which means the extensions were not created.
- bound to another install: the license is bound to another server, and Revaal has to free it.
- An update did not happen: sudo revaal-desk-update status gives the last attempt's outcome, and the reason if it failed.
To report a problem to Revaal, send the output of these commands. Never send the .env file.
What leaves the server#
| Destination | What | Why |
|---|---|---|
| api.revaal.app | The daily check-in (the version, the install's id, spending totals, no content); call audio for transcription; conversation text for summaries, filing and agents; pieces of text for vectors | The license, billing, the AI work |
| registry.revaal.app | Image downloads, and the list of versions once an hour | Installation and updates |
| api.goftino.com | Reading your own Goftino chats | The Goftino source |
- The desk's server makes no connection outside Iran itself. Revaal passes the work on: text through Revaal's proxy to OpenRouter's models outside Iran, and only to providers that keep no data; audio to Soniox for transcription; pieces of text to Revaal's vector server in Iran.
- Revaal stores neither the text nor the audio, and Soniox's copy of each call is deleted as soon as the transcript is back.
- The search index, the conversations and the call recordings stay in your database alone.
- The database credentials, the local secrets and the FTP password never leave the server.
- Error reporting (Sentry) is off on an install.