Skip to main content

Jitsi Meet Video Conference Integration

This integration connects your Vodia PBX with a self-hosted Jitsi Meet server, enabling one-click video conferencing directly from the web portal. Conferences are secured with JWT authentication — moderators receive a signed token while guests join via a shared link. Room names are generated automatically using the extension number and a random hash to prevent unauthorized access.

tip

Enhance your experience by using this integration with our web portal features.

How It Works

  1. A user right-clicks an extension in the web portal and selects Video Conference (Jitsi)
  2. The PBX backend generates a JWT-authenticated moderator URL and an unauthenticated guest URL
  3. The moderator's browser opens the Jitsi conference room automatically
  4. The invited user receives a notification with the guest link to join the room
  5. When all participants leave, the Jitsi room is automatically destroyed (rooms are ephemeral)

Requirements

Jitsi Meet Server

  • Ubuntu 24.04 LTS server (dedicated or virtual)
  • DNS A record pointing to the server's public IP
  • Firewall ports open: 80/TCP, 443/TCP, 10000/UDP
  • Minimum 2 CPU cores and 4 GB RAM recommended

Vodia PBX

  • Vodia PBX with integration plugin support
  • Admin access to the tenant settings

Part 1: Jitsi Meet Server Setup

This section walks through installing Jitsi Meet with JWT authentication on a fresh Ubuntu 24.04 server. The example uses meet.example.com — replace this with your actual domain throughout.

Step 1 — Prepare the Server

Set the hostname and update the system:

sudo hostnamectl set-hostname meet.example.com
echo "127.0.0.1 meet.example.com" | sudo tee -a /etc/hosts
sudo apt update && sudo apt upgrade -y

Step 2 — Install Dependencies

Install the required packages for Jitsi and JWT authentication:

sudo apt install -y apt-transport-https curl gnupg2 lua5.4 liblua5.4-dev libssl-dev luarocks

Set Lua 5.4 as the default version (required by Prosody):

sudo update-alternatives --install /usr/bin/lua lua-interpreter /usr/bin/lua5.4 100
sudo update-alternatives --set lua-interpreter /usr/bin/lua5.4
sudo update-alternatives --install /usr/bin/luac lua-compiler /usr/bin/luac5.4 100
sudo update-alternatives --set lua-compiler /usr/bin/luac5.4
caution

Do not remove lua5.1 — it breaks the alternatives system. Setting lua5.4 as preferred is sufficient.

Verify with lua -v — it should show Lua 5.4.x.

Step 3 — Add Package Repositories

Add the Prosody repository:

curl -fsSL https://prosody.im/files/prosody-debian-packages.key | sudo gpg --dearmor -o /etc/apt/keyrings/prosody-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/prosody-keyring.gpg] http://packages.prosody.im/debian $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/prosody.list
tip

If you get a GPG key error during apt update, the Prosody key may have changed. Try fetching it from a keyserver instead:

sudo gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys F7A37EB33D0B25D7
sudo gpg --export F7A37EB33D0B25D7 | sudo tee /etc/apt/keyrings/prosody-keyring.gpg > /dev/null

Add the Jitsi repository:

curl -sL https://download.jitsi.org/jitsi-key.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/jitsi-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/jitsi-keyring.gpg] https://download.jitsi.org stable/" | sudo tee /etc/apt/sources.list.d/jitsi-stable.list

Step 4 — Install Jitsi Meet

sudo apt update
sudo apt install -y jitsi-meet

When prompted during installation:

  • Hostname: Enter your domain (e.g., meet.example.com)
  • SSL certificate: Select "Let's Encrypt certificates" and enter your email address

If dpkg errors occur (e.g., Lua version issues), run sudo dpkg --configure -a and retry.

Verify Prosody is running:

sudo systemctl status prosody

At this point, open https://meet.example.com in your browser to confirm Jitsi is working with open access (no authentication yet).

Step 5 — Install JWT Dependencies

sudo luarocks --lua-version=5.4 install luajwtjitsi
sudo luarocks --lua-version=5.4 install inspect

Step 6 — Generate a JWT Secret

Generate a secure secret and save it — you will need it for both the Prosody config and the Vodia PBX integration settings:

openssl rand -hex 32

Example output: 0b9780402cc144544e74ab27e0700f3b051dbb691907668b5807af5244e61b2f

Step 7 — Configure Prosody for JWT Authentication

Back up the original config:

sudo cp /etc/prosody/conf.avail/meet.example.com.cfg.lua /etc/prosody/conf.avail/meet.example.com.cfg.lua.bak

Edit the Prosody configuration:

sudo nano /etc/prosody/conf.avail/meet.example.com.cfg.lua

Make the following changes to the file:

Main VirtualHost — change authentication to token and add JWT settings:

VirtualHost "meet.example.com"
authentication = "token"
app_id = "vodia"
app_secret = "YOUR_SECRET_FROM_STEP_6"
allow_empty_token = false

MUC component — add restrict_room_creation to prevent unauthenticated room creation:

Component "conference.meet.example.com" "muc"
restrict_room_creation = true
caution

Do not add token_verification to the MUC modules_enabled list. This would block anonymous guests from joining existing rooms.

Guest VirtualHost — add this at the end of the file to allow anonymous guest access:

VirtualHost "guest.meet.example.com"
authentication = "anonymous"
c2s_require_encryption = false

See the Full Prosody Configuration section at the end of this page for a complete reference file.

Step 8 — Configure Jitsi Meet for Guest Access

Edit the Jitsi Meet JavaScript configuration:

sudo nano /etc/jitsi/meet/meet.example.com-config.js

Find the hosts: block and add the anonymousdomain line:

hosts: {
domain: 'meet.example.com',
anonymousdomain: 'guest.meet.example.com',
// ... other settings
}

Optionally, disable the welcome page to prevent users from creating random rooms:

enableWelcomePage: false,

Step 9 — Configure Jicofo for Authentication

Edit the Jicofo configuration:

sudo nano /etc/jitsi/jicofo/jicofo.conf

Set the authentication block and ensure the focus user password is configured:

jicofo {
authentication: {
enabled = true
type = XMPP
login-url = "meet.example.com"
}
xmpp: {
client: {
client-proxy: "focus.meet.example.com"
xmpp-domain: "meet.example.com"
domain: "auth.meet.example.com"
username: "focus"
password: "YOUR_JICOFO_PASSWORD"
}
trusted-domains: [ "recorder.meet.example.com" ]
}
bridge: {
brewery-jid: "JvbBrewery@internal.auth.meet.example.com"
}
}

Choose a secure password for YOUR_JICOFO_PASSWORD — you will register it with Prosody in the next step.

Step 10 — Restart Services and Register Users

Restart all services:

sudo systemctl restart prosody
sudo systemctl restart jicofo
sudo systemctl restart jitsi-videobridge2

Register the Jicofo focus user in Prosody with the same password you set in jicofo.conf:

sudo prosodyctl register focus auth.meet.example.com YOUR_JICOFO_PASSWORD
tip

If Jicofo shows SASLError using SCRAM-SHA-1: not-authorized in the logs, the focus user password is out of sync. Re-run the prosodyctl register command above and restart Jicofo.

Verify Jicofo connects successfully:

sudo tail -20 /var/log/jitsi/jicofo.log

Step 11 — Test the Installation

Test 1: Unauthenticated access is blocked Open https://meet.example.com in your browser. You should see a disconnection error — this confirms JWT is required.

Test 2: Authenticated moderator access Generate a test JWT token:

sudo apt install -y python3-jwt
python3 -c "
import jwt, time
payload = {
'aud': 'jitsi',
'iss': 'vodia',
'sub': 'meet.example.com',
'room': '*',
'exp': int(time.time()) + 3600,
'context': {'user': {'name': 'Test User', 'email': 'test@example.com', 'moderator': True}}
}
print(jwt.encode(payload, 'YOUR_SECRET_FROM_STEP_6', algorithm='HS256'))
"

Open https://meet.example.com/testroom?jwt=TOKEN — you should join as moderator.

Test 3: Guest access While the moderator is in the room, open https://meet.example.com/testroom in another browser. The guest should join without any credentials.

Part 2: Vodia PBX Configuration

Once the Jitsi server is running, configure the integration in Vodia PBX.

Integration Setup

  1. In the Tenant settings, navigate to Advanced → CRM Integration
  2. Select Jitsi and configure the following:
    • Jitsi Server URL: The full URL of your Jitsi server (e.g., https://meet.example.com)
    • JWT App ID: The app_id from your Prosody config (e.g., vodia)
    • JWT App Secret: The secret generated in Step 6
    • Max Conference Duration: Maximum allowed conference time (1 to 24 hours)
  3. Save the settings

jitsi-1.png

Part 3: Using Video Conferences

Once configured, users will see the Jitsi video conference option in the web portal.

Starting a Conference

  1. In the web portal, right-click on an extension in the BLF / contacts panel
  2. Select Video Conference (Jitsi) from the popup menu
  3. A new browser window opens with the Jitsi conference room (you join as moderator)
  4. The invited user receives a browser notification with a link to join

jitsi-2.png

jitsi-3.png

Joining a Conference

When invited to a Jitsi conference:

  1. A browser notification appears with the caller's name
  2. Click the notification to open the conference room in a new window
  3. Guests join without needing any credentials

Inviting Additional Participants

While a conference is active, right-click on another extension and select Video Conference (Jitsi) again. The new participant will receive an invitation to the same room. You also have the option of inviting external guests.

Security Model

The integration uses a layered security approach:

  • JWT authentication: Moderators receive a signed JWT token with a configurable expiry time. The token includes the user's name, email, and moderator status.
  • Room name randomization: Room names are generated as extension-randomhash (e.g., 500-a3f9b2c8e1d4), making rooms unguessable.
  • Restricted room creation: Only JWT-authenticated users can create new rooms. Guests can only join rooms that already exist.
  • Ephemeral rooms: Jitsi rooms automatically destroy themselves when the last participant leaves — no cleanup required.

Full Prosody Configuration

Below is a complete reference Prosody configuration file. Replace meet.example.com with your domain and YOUR_SECRET_HERE with your JWT secret from Step 6.

component_admins_as_room_owners = true

plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }

muc_mapper_domain_base = "meet.example.com";

external_service_secret = "GENERATE_A_RANDOM_SECRET";
external_services = {
{ type = "stun", host = "meet.example.com", port = 3478 },
{ type = "turn", host = "meet.example.com", port = 3478, transport = "udp", secret = true, ttl = 86400, algorithm = "turn" },
{ type = "turns", host = "meet.example.com", port = 5349, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" }
};

cross_domain_bosh = false;
consider_bosh_secure = true;
consider_websocket_secure = true;

ssl = {
protocol = "tlsv1_2+";
ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"
}

unlimited_jids = {
"focus@auth.meet.example.com",
"jvb@auth.meet.example.com"
}

smacks_max_unacked_stanzas = 5;
smacks_hibernation_time = 60;
smacks_max_old_sessions = 1;

VirtualHost "meet.example.com"
authentication = "token"
app_id = "vodia"
app_secret = "YOUR_SECRET_HERE"
allow_empty_token = false
ssl = {
key = "/etc/prosody/certs/meet.example.com.key";
certificate = "/etc/prosody/certs/meet.example.com.crt";
}
modules_enabled = {
"bosh";
"websocket";
"smacks";
"presence_identity";
"ping";
"external_services";
"features_identity";
"conference_duration";
"muc_lobby_rooms";
"muc_breakout_rooms";
}
c2s_require_encryption = false
lobby_muc = "lobby.meet.example.com"
breakout_rooms_muc = "breakout.meet.example.com"
main_muc = "conference.meet.example.com"

Component "conference.meet.example.com" "muc"
restrict_room_creation = true
storage = "memory"
modules_enabled = {
"muc_hide_all";
"muc_meeting_id";
"muc_domain_mapper";
"muc_rate_limit";
"muc_password_whitelist";
}
admins = { "focus@auth.meet.example.com" }
muc_password_whitelist = {
"focus@auth.meet.example.com"
}
muc_room_locking = false
muc_room_default_public_jids = true

Component "breakout.meet.example.com" "muc"
restrict_room_creation = true
storage = "memory"
modules_enabled = {
"muc_hide_all";
"muc_meeting_id";
"muc_domain_mapper";
"muc_rate_limit";
}
admins = { "focus@auth.meet.example.com" }
muc_room_locking = false
muc_room_default_public_jids = true

Component "internal.auth.meet.example.com" "muc"
storage = "memory"
modules_enabled = {
"muc_hide_all";
"ping";
}
admins = { "focus@auth.meet.example.com", "jvb@auth.meet.example.com" }
muc_room_locking = false
muc_room_default_public_jids = true

VirtualHost "auth.meet.example.com"
ssl = {
key = "/etc/prosody/certs/auth.meet.example.com.key";
certificate = "/etc/prosody/certs/auth.meet.example.com.crt";
}
modules_enabled = {
"limits_exception";
"smacks";
}
authentication = "internal_hashed"
smacks_hibernation_time = 15;

VirtualHost "recorder.meet.example.com"
modules_enabled = {
"smacks";
}
authentication = "internal_hashed"
smacks_max_old_sessions = 2000;

Component "focus.meet.example.com" "client_proxy"
target_address = "focus@auth.meet.example.com"

Component "speakerstats.meet.example.com" "speakerstats_component"
muc_component = "conference.meet.example.com"

Component "endconference.meet.example.com" "end_conference"
muc_component = "conference.meet.example.com"

Component "avmoderation.meet.example.com" "av_moderation_component"
muc_component = "conference.meet.example.com"

Component "filesharing.meet.example.com" "filesharing_component"
muc_component = "conference.meet.example.com"

Component "lobby.meet.example.com" "muc"
storage = "memory"
restrict_room_creation = true
muc_room_locking = false
muc_room_default_public_jids = true
modules_enabled = {
"muc_hide_all";
"muc_rate_limit";
}

Component "metadata.meet.example.com" "room_metadata_component"
muc_component = "conference.meet.example.com"
breakout_rooms_component = "breakout.meet.example.com"

Component "polls.meet.example.com" "polls_component"

VirtualHost "guest.meet.example.com"
authentication = "anonymous"
c2s_require_encryption = false