A Bassline video meeting with the participants panel open

Video Meeting

A Self-Hosted Video Meetings Stack with Waiting Room, Chat, Recording and Transcripts

Teams ran their whole operation on Bassline, then left it to have meetings outside of their control. The main concern was data sovereignty. Third-party meeting apps were collecting user data, and transcripts were exposed.

Problem

How can we keep conversations, recordings and transcripts private from third parties?

Outcome

Meetings built into the product, running entirely on the client’s own servers.

Role

Product Designer &
Developer

Timeline

3 Months

A Bassline video meeting open on a laptop, over a landscape photo

Why we couldn’tuse other video call apps

With recent developments in AI and the fast spread of AI products, user data is more vulnerable than ever before. Since users have conversations about their businesses and get transcripts of their meetings, they are more concerned about the data privacy of their business.

Every frame, every recording and every transcript had to stay on the client’s own infrastructure, and transcription couldn’t go near a hosted AI service.

The solution:open-source infrastructure for video, recordings and transcripts

I looked into the open-source solutions available for a video call feature. I found a few different open-source repositories on GitHub.

I looked at simple-peer and raw WebRTC first. The simple-peer library hadn’t been touched since 2022. Raw WebRTC meant writing my own signalling and giving up on recording entirely.

LiveKit gave me one codebase for one-to-one and group calls, on a server I could run myself.

Every user getsa fixed meeting ID

Every user gets one meeting room and a ten-digit ID, formatted 123-456-7890. Paste the host’s ID or the whole URL into the join box — both in the same place.

Users can quick-start a meeting with the “Start” button. The “Active” and “Ended” meeting lists are shown below.

Meetings page: the personal meeting ID, a join box, and lists of active and ended meetings
Personal meeting ID 100-100-2993 with its link and a Start button

Waiting room andnotification for the host

When a participant requests to join the meeting, the host gets a notification about the waiting room, and participants are updated with the request status.

Participants will be in the waiting room until the host admits them. As soon as the host admits them, they will be notified that they are being connected.

The host’s notification: 1 person entered the waiting room, with a View button
Participants panel showing Peter in the waiting room with an Admit button
The participant’s screen while waiting for the host to admit them
The participant’s screen as they are connected after the host admits them

Schedule meetingsand place ’em on the calendar

Users can schedule meetings for later and save them in the inbuilt calendar.

New Meeting dialog with Start Now and Schedule for Later
Schedule Meeting dialog with date, start time and end time

Host can’t leaveby accident

When the host clicks End, they get asked what they actually meant. End it for everyone, or hand the room to someone still in it and leave.

Leave Meeting dialog: End Meeting for All, or Transfer Host & Leave
Transfer Host dialog for choosing the participant who becomes the new host

What the host seesfrom their end

A simple demonstration of how a new meeting gets started, what the host sees and how a participant gets accepted into the meeting, plus a glimpse of the in-meeting chat.

What a participantsees from their end

What participants see when they try to join a meeting.

What’s happeningunder the hood

LiveKit does the hard part, the heavy lifting: streaming camera and microphone data nonstop. The Node backend does all the control, the lightweight occasional stuff, such as polling API calls.

  • How do audio and video get from one end to the other?

    The Node server never receives a single video frame. The LiveKit media server uses the SFU (Selective Forwarding Unit) method for that. We install and run it on our own server. Customer video goes to a machine we control, not a third party.

  • What does the app server do?

    The app server checks the session, who is allowed in the meeting, the waiting list, who the host is and the chat history. Most importantly, it issues the token that creates the link between LiveKit and the app server.

  • Where does the MySQL DB come into play?

    So where does “Monday standup, created by Hashan, nine people invited, with chat history” live? In MySQL. A meeting as a durable thing only exists in the database.

  • How is the waiting room enforced?

    The waiting room is not a gate on the media server. The media server has no idea the waiting room exists. Admitting a participant is just a database state change. The participant has a polling API call that checks the status of one MySQL row. Once it flips to ADMITTED, that polling API call mints the token. We enforce the waiting room by withholding the credential, not by blocking anyone at a door.

How a participantis admitted

A waiting participant holds no credential. Admission is a database state change — the media server is never told anyone is waiting.

Admission flow: the participant asks to join and polls the app server; the host admits them, the database row flips to ADMITTED, and only then is a LiveKit token issued so they can connect to the media server