What Agorà is
Confidential information flows through online meetings: decisions, documents, customer data. On an external platform all of this is handled by third parties, under rules we do not set.
That is why we built Agorà: a video conferencing platform running in production on our cluster, defined entirely as IaC, where every change goes through a pipeline. Today we also offer it to customers who want a platform of their own.
The components
Agorà reinvents nothing: it brings together established open-source components, each with a precise role, and makes them reproducible.
- BigBlueButton 3.0Video conferencing
- Proxmox VEVirtualisation
- OpenTofuInfrastructure as code
- Ansible and MoleculeConfiguration
- OPNsense in HAPerimeter
- Django and CeleryApplication
- GitLab CIDelivery
- Let's EncryptCertificates
Infrastructure as code
Nobody makes manual changes in production. The virtual machine is defined in OpenTofu, the state is kept in GitLab and the repository contains 0 secrets. The firewall rules are code too: the plan shows every change before it is applied.
$ tofu plan # opnsense_firewall_filter.this["lan_turn"] will be createdPlan: 3 to add, 0 to change, 0 to destroy.$ tofu apply plan.cacheApply complete! Resources: 3 added.$ sh ci/ha-sync.sh✓ configuration synced to the standby nodeAll 18 merge requests in the project followed the same path:
- 1Merge requestcode review
- 2Lint and testtofu test, Molecule, pytest
- 3Planpreview of the changes
- 4Applymanual approval
- 5Configureconfiguration with Ansible
Configuration and testing
Ansible configures the rest of the system: prerequisites, BigBlueButton installation with hash verification, API secrets, media server and landing page. A second run must change nothing: if anything changes, the test fails.
The tests run with Molecule on an Ubuntu container. For the invitation proxy a real nginx is started and checked against edge cases as well: disallowed paths, POST requests, backend unavailable.
The custom welcome page and favicon are protected with dpkg-divert, so BigBlueButton updates do not overwrite them.
Diagnosing an audio problem
In the first real test, video and screen sharing worked but audio did not: the microphone would not connect and returned errorCode 2003.
We also introduced split-horizon DNS: from inside, the Agorà name resolves to the private IP, so office clients use the TURN server on the internal network without crossing the NAT.
Room management
BigBlueButton exposes only APIs, with no interface for managing rooms. So we developed a reusable Django app, with 282 automated tests and 99% code coverage, distributed as a Python package in our private registry. The main features:
- Multiple BigBlueButton servers, with secrets encrypted and never visible in the admin.
- Signed calls: SHA-256 checksums towards the APIs, tokens and JWTs verified on callbacks and notifications.
- Consistent state: Django signals sent only after the commit, and Celery tasks to resynchronise.
- Form-based admin: room options are fields to fill in, not JSON to write by hand.
- Permanent closure of finished meetings, except in rooms configured as reusable.
- Invitation links that can be revoked and expire, valid even without an account.
Guest access
The rooms app lives on our intranet, which is not reachable from the internet. Yet customers and partners also take part in meetings, and they must be able to join without the intranet being exposed.
The video conferencing server, already public, acts as a reverse proxy for a single path: the invitation page. nginx accepts only UUID keys and GET requests, verifies the intranet certificate and rate limits requests per IP, with a threshold that tolerates several guests behind the same corporate NAT. Every other path returns 404.
If the intranet does not respond, only invitations return 502: meetings in progress are not interrupted.
Scalability
The Django app already manages multiple BigBlueButton servers. A new environment is a new configuration for OpenTofu and Ansible, with the same pipelines and the same tests; to add capacity, you simply add a node.


