All documentation

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:

ItemWhat for
The license keyActivation, 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#

ItemMinimum
Operating systemUbuntu 22.04 or 24.04 (tested on 24.04)
CPU and memory2 cores, 4 GB of RAM
Disk30 GB free for Docker. The images take about 1.6 GB, and the backups taken before updates are kept here too.
SoftwareDocker Engine with the docker compose plugin (v2), curl and jq
DirectionDestinationPortWhat for
Outboundapi.revaal.app (Iran)443License, transcription, models, vectors
Outboundregistry.revaal.app (Iran)443Images and new versions
Outboundapi.goftino.com443Only with a Goftino source
InternalThe phone system's FTP21Only to read call recordings
InternalPostgreSQL5432The desk's data
InboundStaff machines443 (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):

SQL
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.

  1. Log in to the registry

    With the account Revaal gave you:

    docker login registry.revaal.app
  2. The 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 .env
  3. Two 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; done
  4. Fill in the rest of .env

    As KEY=value, with no quotes:

    VariableValue
    DESK_DOMAINThe desk's name, such as desk.company.ir
    DESK_TLSinternal 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_VERSIONThe same <version>; from then on the updater moves it
    DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORDThe database from the previous section
    DB_SSLMODEdisable, or require
    REVAAL_LICENSE_KEYThe license key from Revaal
    DESK_UPDATE_WINDOW (optional)The hours, Tehran time, when updates may install on their own; 2-5 by default
  5. 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 ps
  6. Check 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.

  7. 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.

  1. The sending key

    In the desk: Sources, then Push via API, then copy the key.

  2. The connector.env file

    Make it beside docker-compose.yml (chmod 600) and fill it in:

    VariableValue
    REVAAL_KEYThe key from the previous step
    FTP_HOST, FTP_USER, FTP_PASSWORDThe phone system's FTP address, and a read-only user
    FTP_DIRThe folder holding the year folders (2026/, 2025/ …)
    SINCEThe first day to send calls from, YYYY-MM-DD
    OPERATORSOperators' names by extension: 101=Name,102=Name
  3. Start it

    docker compose --profile calls up -d connector

    Calls 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:

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-update

Going 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#

CheckCommandHealthy answer
The license, and Revaalcurl -sk https://<DESK_DOMAIN>/api/desk/license"mode":"open", and everything under reachable is true
The containersdocker compose psapi, web, ml and caddy Up
The application logdocker compose logs api --tail 200A 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#

DestinationWhatWhy
api.revaal.appThe 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 vectorsThe license, billing, the AI work
registry.revaal.appImage downloads, and the list of versions once an hourInstallation and updates
api.goftino.comReading your own Goftino chatsThe 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.

Revaal

Manifesto

A person’s attention is the most valuable thing they bring to work. Every one of us has a limited amount of energy, focus, and presence each day, and a day that is spent does not come back. This is true for every single person on a team, not only the ones at the top. So the real question for any company is a simple one: what are we asking people to spend their days on?

Too often, the answer is repetition. We ask people to copy, paste, reformat, check the same list again, and write the same message for the hundredth time. The work gets finished, but something is lost. A person doing repetitive work is a person who is not thinking. We believe this is a serious mistake, not because such work is beneath anyone, but because a human being is the wrong tool for it. Machines are patient with repetition. People are not, and they should not have to be.

We are building for the other side of that trade. Models now have the processing power and the intelligence to carry the repeating part: the reading, the sorting, the drafting, everything that follows a known pattern. Hand that over, and look at what is left for people: creating things, staying curious, wrestling with problems that have no certain answer, sitting with a question while it is still unclear, and making something new and then changing it again. That is what a human day should be spent on. We think it should be the normal day, not the rare one.