Feature flags are read from the analytics backend. Without one,
NoOpAnalytics returned no flag, so "summary-enabled" was always off and
a self-hosted instance could not get automatic summaries without PostHog.
NoOpAnalytics now accepts a static "feature_flags" mapping through the
existing ANALYTICS_BACKEND_SETTINGS, and get_analytics() passes the
settings to it. Nothing changes when no flag is declared.
Fixes#1610
Case-insensitive user lookups during token issuance currently take
around 100 ms in production, accounting for roughly a third of the
total 300 ms token request trace.
Add an `UPPER(email)` index on the user table so these lookups can
use the index instead of scanning, cutting the dominant cost on
the token issuance path.
Emails were rendered and sent inline in the LiveKit webhook, so a
slow SMTP server blocked the request and a transient failure lost
the email. Delivery now runs in a worker, with retries and backoff.
The task is generic (template, subject, recipients, context,
language) so other emails can reuse it. The subject is translated
in the worker, and dates are formatted per owner before queuing,
so the arguments stay JSON-serializable.
Without Celery, the task still runs synchronously. Success now
means "queued", not "delivered".
Add a Recording tab to the settings dialog with two selectors, a
content profile and a resolution, building on the per-recording
encoding API.
The goal is to let an instance default to a cheaper profile and
resolution to save egress CPU, while users who really need a smooth
HD recording can still opt in. Most users only need a recording of
acceptable quality, so the default stays cheap and configuring it is
optional.
Profiles are labelled by content ("Talking heads", "Presentations and
text", ...) rather than by fps or bitrate: users know what they are
about to record, not which encoder settings fit it. Each profile shows
a one-line description of its trade-off, so picking one requires no
technical knowledge.
The panel gets a third row, matching the meeting language row of the
transcript panel. It is a shortcut to the settings, without adding
mental load about how to configure a recording. It reads as a short
statement with a trailing "Change" link, like the informational rows
above it, and stays on a single line in every locale: the statement
truncates if a translation gets too long, the link never wraps.
The selected options are intentionally not shown in the panel.
Several users can access it, and each would see their own local
configuration while the running recording may use another user's
encoding, which would be misleading. The issue is milder in the
settings tab, which is frozen while a recording is starting or in
progress.
The selectors are disabled from the moment a start request is sent
until the recording is saved, for both screen recordings and
transcripts. Settings are sent once, when the recording starts, so
changing them afterwards would have no effect and suggest otherwise.
Choices are persisted in localStorage to keep things simple,
consistent with the meeting language, which is already stored
client-side.
The backend now exposes the available resolutions and profiles, and
their defaults, in the frontend configuration, so the UI follows
whatever the operator configures instead of hardcoding values. Stored
choices are checked against it before being sent: a profile removed by
the operator falls back to the instance default instead of being
rejected by the API. No encoding is sent when per-recording encoding
is disabled, as the backend would reject it. Operator-defined values
without a translation are shown by their raw key.
Alternatives considered:
- Storing the preference in room metadata: it would turn a pre-start,
personal choice into shared editable state, requiring a write
endpoint, permission checks and handling of concurrent edits.
- Notifying participants of the chosen encoding: notifications are
one-off, so users who join or reload after the recording started
would miss them, and the information is noise for most participants.
Next steps:
This is a deliberately small first iteration, meant to deliver most
of the value and evolve with feedback and usage. Possible next steps:
- persist the preferences in the backend instead of localStorage
- store the encoding chosen for a recording on the room (or on a
session, if such a model is introduced) and read it from there
rather than from user settings, so every user sees the same value
authenticate and authenticate_header hardcoded "Bearer" instead of
using the token type the backend is configured with. Setting
APPLICATION_JWT_TOKEN_TYPE, ADDONS_TOKEN_TYPE or USER_ACCESS_TOKEN_TYPE
to anything else made every token be ignored, since the Authorization
scheme never matched.
Store the token type on the backend and use it both to match the
Authorization header scheme (case-insensitively) and as the
WWW-Authenticate scheme.
generate_user_access_token was copy-pasted in seven test modules
(user access token authentication, and rooms create, list, retrieve,
update, participants management, rename/toggle and subtitle).
Extract it into core.tests.utils and reuse it everywhere. The shared
version keeps the optional application and claim overrides from the
authentication tests, and reads the token type claim from
USER_ACCESS_TOKEN_TYPE_CLAIM instead of hardcoding it.
The participant id returned by request-entry was the raw LiveKit UUID,
so anyone knowing it could poll the lobby and obtain the admitted
participant's LiveKit token.
Return the id signed with SECRET_KEY under a lobby-specific salt, and
reject unsigned, tampered or foreign-signed ids with a 400. The raw
UUID is still used internally for the cache and LiveKit identity.
Bind an accepted lobby entry to the username the participant had at
the moment of acceptance.
This prevents a participant, once accepted, from changing their
display name and reusing the same lobby grant to enter the room
under a different identity.
The lobby system relied on cookies to identify the participant
across the wait/enter cycle, which does not work in an iframe
context where our cookies are dropped.
Simplify the lobby behavior:
* The POST request that enters the lobby now returns the
participant id in the response.
* The frontend passes that id back on subsequent requests to keep a
sticky session while trying to enter the room.
This moves a bit more logic to the frontend but should be a
transparent refactoring, without decreasing the security of the
lobby flow.
We now use `Authorization: Bearer <token>` to authenticate users
from the token exchange flow (used for iframe embeds).
Until now, the `Bearer` scheme was also reused for the alternative
LiveKit authentication, where a client presents its LiveKit token
issued by the backend to prove room membership on actions open to
any room participant. Sharing the scheme between the two flows is
not viable anymore.
Switch the LiveKit token authentication to a dedicated
`Authorization` scheme, so `Bearer` stays reserved for the iframe /
token-exchange flow.
Follow-up: a broader effort should look into harmonizing and
hardening the backend authentication stack of the app.
Some integrators render our videoconference inside an iframe, where
our cookie-based authentication does not work: our cookies are
SameSite=Lax/Strict, so the iframe drops them.
We looked at what Jitsi offers: a shared secret used to sign JWTs
that authenticate users coming from external services. Since we
already expose an external API where third parties authenticate as
a given user, it was simpler for us to add an exchange mechanism on
top of that.
Flow:
* Through the external API, mint a short-lived, single-use exchange
code for a user.
* The third party hands that code to the frontend as a URL fragment.
* The frontend exchanges the code for a longer-lived JWT that can be
used to query the regular API viewsets.
Known limitations and follow-ups:
* At some point it would be nice to shorten the JWT lifetime and
add a refresh mechanism. This will be handled in a follow-up PR
when actually needed.
* CSP rules to control which origins are allowed to embed the app
in an iframe still need to be added.
* This alternative authentication cannot easily be scoped to a
subset of endpoints without adding a lot of complexity, so it is
accepted globally on the API for now.
Load allowed domains once and compare the lowercased email domain
in Python. For restricted applications with an existing user,
this reduces successful token requests from four queries to three.
Keep domain loading after credential validation so rejected
credentials require only the application lookup.
Add endpoint query-count tests for restricted and unrestricted
successes and invalid credentials.
The client id and client secret are auto-generated with high entropy
when an application is created. Allowing them to be edited from the
Django admin let any administrator replace them with a short or
weak value, undermining that guarantee.
Make both fields read-only in the admin so they can only be
regenerated through the intended flow.
Application token authentication currently uses Django's default
password hasher. On our production hardware, token requests take
at least ~500 ms. Authentication happens per user of each
application, so this cost accumulates across frequently used
integrations.
Password hashers deliberately make guessing expensive to protect
human-chosen passwords after a database leak. Our application
secrets are generated server-side using a cryptographically secure
random generator, with a default length of 128 alphanumeric
characters. Guessing these secrets is already computationally
infeasible, making password stretching an unnecessary CPU cost.
Use salted SHA-256 for application secrets while retaining
constant-time comparison, and keep user password hashing
unchanged. Existing secrets migrate after successful verification
without requiring key rotation. Conditional updates prevent
migration from overwriting a concurrent rotation.
Store the new hash in `client_secret_sha256` while preserving the
original Django hash in `client_secret`. New applications populate
both fields, allowing the previous release to authenticate both
existing and newly created applications if we need to roll back.
Once every application has migrated and the rollback window has
closed, complete the migration by removing the legacy verification
code and `client_secret` field.
This assumes securely generated, high-entropy secrets. Deployments
that reduce `APPLICATION_CLIENT_SECRET_LENGTH` or supply
predictable secrets lose the offline guessing protection that the
previous slow hasher provided.
When the user is updated, their lists on Brevo are overwritten with
the new value: this removes lists set by other products.
Switch to the common lib implementation from `django-lasuite`,
which manages this correctly.
This change was initially proposed by @qbey, but at the time our
deployment did not have a Celery worker running alongside the
backend. Since then, a Celery worker has been deployed, so the
switch to the common lib approach is now safe to adopt.
Highlight in the connection test when the user is connecting
through a TURN relay, especially over TLS or TCP. This usually
indicates that some network configuration is required on their
side, and gives them a concrete signal to pass to their IT team.
Suggested by a technical user, this is a first step toward making
users more autonomous when troubleshooting access to the tool.
Follow-up: show a similar warning in-product when we detect a
mid-meeting fallback to TURN/TLS. A one-time hint for first-time
users would likely be enough.
The home page only offers meeting creation to authenticated users, while the
backend already serves ephemeral rooms to anonymous visitors when
ALLOW_UNREGISTERED_ROOMS is enabled (the flag is checked in the room retrieve
view, not in the create one).
Expose the flag in the frontend configuration and, when it is on, show the
existing "Create a meeting" button to signed-out visitors. It navigates to a
freshly generated room id rather than calling POST /rooms/, which stays
reserved for registered rooms and for authenticated users.
The invite dialog opens for such a creator when the room is unregistered
(null id) and the navigation carries `create`. The `mode` computed in Room
is left untouched, so permissions and the join screen behave as before.
The home buttons row now wraps: signed-out visitors can see three controls
(create, join, login), and at the xsm breakpoint the fixed-width ProConnect
button leaves too little room for the other two.
Add new options to query start-start recording API. A resolution
("540p", "720p", "1080p") and a profile ("talking_heads", "text", "mixed")
are resolved to provide a width, height, fps and bitrate which
are passed on to the encoder. Using profiles allows for some flexibility
on quality if necessary without changing front facing user config.
Co-authored-by: sarthakbahal <sarthakbahal.45@gmail.com>
Address the following HIGH severity CVEs reported by Trivy on the
backend image:
* Django 5.2.16 → 5.2.17
- CVE-2026-15307 — remote code execution via GeoDjango spatial
lookups.
* urllib3 2.7.0 → 2.8.0
- CVE-2026-97687 — traffic interception via HTTPS proxy TLS
configuration override.
- CVE-2026-97689 — denial of service via unbounded memory
allocation in the chunk parser.
Get the spellcheck CI job to pass again by:
* Fixing the actual spelling issues it caught in the project.
* Excluding generated files from the scan, since they are not
written by us.
* Excluding translation files, whose content is not necessarily in
English and would trigger false positives.
The per-minute room creation throttle absorbs bursts but does not stop
a compromised account from steadily creating rooms over hours or days.
Add RoomCreationDailyUserRateThrottle, a per-user throttle with its own
"room_creation_daily" scope, applied to room creation only alongside
the existing short-term throttle. It defaults to 1000 rooms per day and
is configurable via ROOM_CREATION_DAILY_THROTTLE_RATES.
Tests use a controllable clock and patch rates with monkeypatch so they
are restored after each test.
Add throttling on the endpoint used to generate meeting links, so a
compromised authenticated account cannot silently generate thousands
of links without hitting any suspicious errors or alerts.
The limits are set high enough not to affect legitimate usage, while
capping the damage a leaked account can do.
Bump PyJWT from 2.13.0 to 2.14.0 to address the following CVEs
reported by Trivy:
* CVE-2026-102268 (CRITICAL)
* CVE-2026-102266 (HIGH) — authentication bypass via empty HMAC
key acceptance.
* CVE-2026-102267 (HIGH) — verification key substitution via
unvalidated JWKS redirects.
* CVE-2026-102271 (HIGH) — authentication bypass via acceptance
of DER public keys as HMAC.
* CVE-2026-102272 (HIGH) — token forgery via improper Unicode
byte-order mark handling.
* CVE-2026-102273 (HIGH) — token forgery via acceptance of
public JWK containers as HMAC.
Add a `purge_inactive_rooms` management command which permanently
deletes the rooms that were not started for
`ROOM_INACTIVITY_DELETION_DAYS` days. Rooms never started are aged from
their creation date.
Important points :
- The setting is unset by default, which disables the purge.
- Rooms holding a recording their users may still access are kept,
- It refuses to run while unregistered rooms are allowed, as the link
of a purged room would turn into a public unregistered room,
Rooms pile up in database with no way to tell the ones still in use from
the abandoned ones. Record on the room the last time LiveKit reported it
as started, in a new `last_started_at` field which stays NULL until the
first time the room is started on a LiveKit node.
The migration stamps existing rooms with the migration time to avoid
deleting preexisting rooms.
The development stacks now run Garage instead of MinIO which is
deprecated.
Garage is a bit stricter than MinIO:
- key IDs and secrets must be at least 8 and 16 characters long
- requests must be signed for its region, so every development env now sets
AWS_S3_REGION_NAME=local;
- cross-origin requests are denied unless the bucket CORS rules allow
them, so a one-shot aws-cli container allows the frontend origin to
upload files straight to the bucket.
Address the following MEDIUM severity CVEs reported against
`djangorestframework` 3.17.1:
* CVE-2026-73228 (CVSS 5.3)
* CVE-2026-73229 (CVSS 4.3)
Bump `djangorestframework` to the patched version to pick up the
fixes.
Most call sites of `update_metadata` already wrap the call in a
try/except that logs the failure at info level.
Remove the warning log inside `update_metadata` itself to avoid
redundant logs, without losing any information.
`EGRESS_ABORTED` and `EGRESS_FAILED` events were previously ignored, leaving
recordings indefinitely in `ACTIVE` state and potentially blocking subsequent
recordings with 409 errors. Add handling and logging for failed and aborted
egresses, discarding failed recordings while preserving the existing behavior
for savable recordings.
Rename `handle_complete` to `handle_savable` to reflect that it handles both
`EGRESS_COMPLETE` and `EGRESS_LIMIT_REACHED`.
Slight refactor to separate LiveKit event handling from recording concerns as
part of a general separation concern to allow for future SFU swapping.
NB:
- FAILED recordings are currently discarded although exploitable media files
may exist
- There is a theoretical hole: if stop observes EGRESS_FAILED before the
egress_ended webhook is processed, the recording is immediately marked as
FAILED. Since only ACTIVE and STOPPED recordings are savable, the webhook
then skips the failure notification and LiveKit error log. In that rare race
condition, participants may therefore not see the failure toast. We accept
this trade-off for now, as this should be very infrequent.
- Another theoretical hole: There is a short race window where the user
clicks stop while the limit-reached status is being processed. Since the user
explicitly requested the stop, we consider skipping the limit notification
acceptable and do not handle this case.
fix(recording): log aborted worker events at info level
`DockerflowMiddleware` serves the endpoints `/__heartbeat__`, `/__lbheartbeat__
that we use for the kubernetes probes. Sitting at the bottom of
MIDDLEWARE, every Kubernetes probe traversed all middlewares which is not
efficient.
get_release() read the version from a version.json file but nothing generates
it during the CI Docker image build, therefore the release reported to Sentry
was always "NA".
Read the version from pyproject.toml instead, which is bumped at each
release and copied into the image.
The resource server backend returned any user matching the token's
`sub` claim without checking `User.is_active`. The upstream lasuite
backend only validates the token's introspection `active` claim, so a
deactivated Django account kept API access until its token expired.
Raise `SuspiciousOperation` in `get_or_create_user` when the user is
inactive, which the authentication class turns into a 401, consistent
with `BaseJWTAuthentication`. Add unit and end-to-end tests.
Add a configuration knob for the trace sampling rate, so we can
enable tracing on middleware and cache spans when debugging slow
requests in production.
Sampling is set to 0 by default, so tracing stays fully off unless
explicitly enabled.
Around 0.76% of incoming LiveKit webhooks were being flagged as
unprocessable and returned a 422, even though LiveKit was sending
legitimate data — just with event types we do not handle. This
inflated error metrics and made real webhook issues harder to spot.
Return a 200 for these webhooks instead. When a new, unhandled
event type shows up, log a warning so we can decide whether it is
worth adding explicit handling.
The request summary info logs were spamming the log stream, making
around 46% of the total volume, without carrying any exploitable
information.
Silence them so the remaining logs are easier to explore and cheaper
to store; roughly halves the overall log volume.
On a busy morning, `/me` alone produced 72k warning logs — 97% of
all warnings. They all come from anonymous requests to `/me`
without credentials, which is normal: `/me` is how the app
determines the current auth status.
These warnings carry no diagnostic value on this endpoint, so
silence them there to cut down on log volume.
AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME was only enforced at
LiveKit token generation and by hiding the name field in the frontend.
The `rooms/{id}/rename/` endpoint never checked it, so any authenticated
user with a valid room token could rename themselves via the API even
when the self-hoster had disabled it.
Return 403 from the rename action for authenticated users when the
setting is disabled, mirroring the `can_edit` rule in
`core.utils.generate_token`. Anonymous participants are unaffected, as
they have no account name to fall back on.
Add tests covering the disabled/enabled cases for authenticated users
and the anonymous exception.
Highlighted by a suggestion from @florent, the waiting participant
list was not sorted, so moderators could see participants in an
arbitrary order.
Add an explicit `entered_at` attribute on each waiting participant,
so the list can be sorted by arrival time. Participants are now
shown in a stable order of arrival, both across polls and across
moderators.