feat(api): add Server-Sent Events (SSE) infrastructure#11556
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds Server-Sent Events (SSE) platform: ASGI/Gunicorn runtime changes, django-eventstream dependency and settings, SSE authentication fallback, channel-name utilities, BaseSSEViewSet and tenant-aware SSEChannelManager, tests, developer guide, and changelog entry. ChangesServer-Sent Events Platform
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
✅ All necessary |
|
✅ Conflict Markers Resolved All conflict markers have been successfully resolved in this pull request. |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
🔒 Container Security ScanImage: 📊 Vulnerability Summary
16 package(s) affected
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #11556 +/- ##
==========================================
+ Coverage 94.02% 94.04% +0.01%
==========================================
Files 241 247 +6
Lines 35705 35927 +222
==========================================
+ Hits 33573 33787 +214
- Misses 2132 2140 +8
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/src/backend/api/sse/utils.py`:
- Around line 34-36: Change the channel-shape check to require exactly three
segments and fail otherwise: replace the current len(segments) < 3 check with
len(segments) != 3 so channels with more than three segments are rejected;
additionally, after splitting, validate the first segment equals the expected
prefix constant (e.g., CHANNEL_PREFIX or the literal used elsewhere) and
validate the tenant_id segment is a UUID (use uuid.UUID(...) in a try/except) to
prevent separator-injection and non-canonical names when extracting tenant_id
from segments.
In `@api/src/backend/api/tests/test_authentication.py`:
- Around line 413-427: Add a test case in test_authentication.py that verifies
SSEAuthentication.authenticate raises an authentication error when an
access_token query param is present but invalid: create a MagicMock request with
request.query_params = {"access_token": "bad-token"}, patch
"api.authentication.JWTAuthentication" to return a jwt_instance whose
get_validated_token raises rest_framework.exceptions.AuthenticationFailed, call
SSEAuthentication().authenticate(request) and assert that AuthenticationFailed
is raised, and also assert get_validated_token was called with "bad-token" to
ensure the query-token path is exercised.
In `@docs/developer-guide/server-sent-events.mdx`:
- Around line 60-66: Standardize the prose to use "tenant ID" (uppercase)
everywhere while keeping code identifiers like `<tenant_id>` and CHANNEL_PREFIX
(and the example channel format `<prefix>:<tenant_id>:<resource_id>`) unchanged;
update occurrences such as "The tenant id is baked into every channel name" and
"parsing the tenant id embedded in the channel name" to "The tenant ID..." so
all narrative references use the uppercase form, but do not modify code snippets
or symbol names like make_channel_name, CHANNEL_PREFIX, or `<tenant_id>`.
- Line 60: Reword the phrase "owned by your feature" to remove the second-person
possessive in the sentence describing channel format; for example change it to
"provided by the feature", "controlled by the feature", or "assigned by the
feature" so the sentence reads like "The prefix is provided by the feature and
may contain hyphens but never colons (the parser splits on `:`)"; update the doc
text that describes channels and the `make_channel_name` usage accordingly.
- Around line 1-3: Add a Version Badge immediately after the section header
"Server-Sent Events (SSE)" to indicate this is new in Prowler API v1.32.0;
update docs/developer-guide/server-sent-events.mdx by inserting the standard
Version Badge element (matching project badge style) right below the top-level
title line so the page clearly shows "Prowler API v1.32.0" for the new SSE
infrastructure feature.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3e28d44a-07ed-482b-80d9-7f77bd3f1cc7
⛔ Files ignored due to path filters (1)
api/uv.lockis excluded by!**/*.lock,!**/uv.lock
📒 Files selected for processing (15)
api/CHANGELOG.mdapi/docker-entrypoint.shapi/pyproject.tomlapi/src/backend/api/authentication.pyapi/src/backend/api/sse/__init__.pyapi/src/backend/api/sse/base_views.pyapi/src/backend/api/sse/channelmanager.pyapi/src/backend/api/sse/utils.pyapi/src/backend/api/tests/test_authentication.pyapi/src/backend/api/tests/test_sse.pyapi/src/backend/config/django/base.pyapi/src/backend/config/guniconf.pyapi/src/backend/config/settings/eventstream.pydocs/developer-guide/server-sent-events.mdxdocs/docs.json
|
|
||
| <Step title="Pick a channel prefix"> | ||
|
|
||
| Channels follow the format `<prefix>:<tenant_id>:<resource_id>`, built only through `make_channel_name`. The prefix is owned by your feature and may contain hyphens but **never colons** (the parser splits on `:`). |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Minimize second-person possessive in descriptive text.
The phrase "owned by your feature" uses a second-person possessive in descriptive (non-imperative) text. As per coding guidelines, minimize explicit use of second-person pronouns and possessives except for imperative instructions.
♻️ Proposed alternative
-Channels follow the format `<prefix>:<tenant_id>:<resource_id>`, built only through `make_channel_name`. The prefix is owned by your feature and may contain hyphens but **never colons** (the parser splits on `:`).
+Channels follow the format `<prefix>:<tenant_id>:<resource_id>`, built only through `make_channel_name`. Feature implementations own the prefix, which may contain hyphens but **never colons** (the parser splits on `:`).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Channels follow the format `<prefix>:<tenant_id>:<resource_id>`, built only through `make_channel_name`. The prefix is owned by your feature and may contain hyphens but **never colons** (the parser splits on `:`). | |
| Channels follow the format `<prefix>:<tenant_id>:<resource_id>`, built only through `make_channel_name`. Feature implementations own the prefix, which may contain hyphens but **never colons** (the parser splits on `:`). |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/developer-guide/server-sent-events.mdx` at line 60, Reword the phrase
"owned by your feature" to remove the second-person possessive in the sentence
describing channel format; for example change it to "provided by the feature",
"controlled by the feature", or "assigned by the feature" so the sentence reads
like "The prefix is provided by the feature and may contain hyphens but never
colons (the parser splits on `:`)"; update the doc text that describes channels
and the `make_channel_name` usage accordingly.
Source: Coding guidelines
| Channels follow the format `<prefix>:<tenant_id>:<resource_id>`, built only through `make_channel_name`. The prefix is owned by your feature and may contain hyphens but **never colons** (the parser splits on `:`). | ||
|
|
||
| ```python | ||
| CHANNEL_PREFIX = "scan-progress" | ||
| ``` | ||
|
|
||
| The tenant id is baked into every channel name. That is what lets the platform enforce cross-tenant isolation without knowing anything about your feature. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Inconsistent capitalization of "tenant id" versus "tenant ID".
The document uses both "tenant id" (lines 60, 66, 201) and "tenant ID" (line 209, embedded in code/channel names as tenant_id). For consistency and clarity, standardize on one form throughout the prose. "tenant ID" (uppercase) is preferred when referring to identifiers in technical documentation.
Examples:
- Line 60: "format
<prefix>:<tenant_id>:<resource_id>" → code identifier (keep lowercase in code) - Line 66: "The tenant id is baked into every channel name" → prose (should be "tenant ID")
- Line 201: "by parsing the tenant id embedded in the channel name" → prose (should be "tenant ID")
Also applies to: 201-201, 209-209
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/developer-guide/server-sent-events.mdx` around lines 60 - 66,
Standardize the prose to use "tenant ID" (uppercase) everywhere while keeping
code identifiers like `<tenant_id>` and CHANNEL_PREFIX (and the example channel
format `<prefix>:<tenant_id>:<resource_id>`) unchanged; update occurrences such
as "The tenant id is baked into every channel name" and "parsing the tenant id
embedded in the channel name" to "The tenant ID..." so all narrative references
use the uppercase form, but do not modify code snippets or symbol names like
make_channel_name, CHANNEL_PREFIX, or `<tenant_id>`.
Source: Coding guidelines
Add the django-eventstream dependency that backs Server-Sent Events and bump gunicorn to a release that ships the native asgi worker class, so SSE streams can run on the event loop.
Run gunicorn with the native asgi worker against config.asgi so SSE streams are parked on the event loop instead of holding a sync worker per open connection; sync CRUD views keep running in the thread-sensitive executor. Disable preload under DEBUG so dev reload picks up edited code, and point the dev and prod entrypoints at the ASGI application.
Browser EventSource cannot set the Authorization header, so add an SSEAuthentication class that extends the standard JWT/API-key stack with an ?access_token=<jwt> query-parameter fallback (RFC 6750 section 2.3), consulted only when no Authorization header is present. The query path accepts a JWT only; API keys remain header-only.
Add the platform SSE layer that wires django-eventstream into the API: - BaseSSEViewSet: a base viewset features subclass to expose an SSE endpoint, reusing the regular DRF stack (auth, RBAC permissions, tenant transaction) and delegating the stream to django-eventstream. - SSEChannelManager: resolves the channel set off the request and enforces a tenant gate by parsing the tenant id embedded in the channel name. - make_channel_name/tenant_id_from_channel: the single source of truth for the <prefix>:<tenant_id>:<resource_id> channel format. - eventstream settings: Valkey Pub/Sub backend on a dedicated DB, the channel manager, and allowed headers; registered in Django settings. No endpoint streams over SSE yet; this is the reusable base.
Document the SSE infrastructure for backend developers: when to use SSE, the architecture and ASGI transport, a step-by-step worked example for adding an endpoint to a feature, the resource.verb event-naming convention, authentication, the tenant-isolation model, and reconnect/ state-recovery. Register the page in the Developer Guide navigation.
Enforce the canonical <prefix>:<tenant_id>:<resource_id> contract: make_channel_name now raises ValueError when any segment contains the ':' separator, and tenant_id_from_channel requires exactly three segments so a crafted name cannot slip a valid tenant UUID into position 1 while carrying extra segments.
Add an error-path test asserting SSEAuthentication.authenticate raises AuthenticationFailed when the access_token query param is present but invalid, complementing the existing valid-token fallback test.
Drive a real DRF request through the full viewset stack (auth, RLS, content negotiation, channel manager) and assert events() returns an SSE StreamingHttpResponse, guarding the DRF-request-into-django-eventstream path from silent regressions.
Mark the Server-Sent Events guide as new in Prowler API v1.32.0 with the standard VersionBadge component.
64d6255 to
7820d68
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/src/backend/api/authentication.py`:
- Around line 120-123: The code creates a new JWTAuthentication instance instead
of using the shared backend, causing divergence from
CombinedJWTOrAPIKeyAuthentication.jwt_auth; replace the direct instantiation
(JWTAuthentication()) with a reference to the shared backend
(CombinedJWTOrAPIKeyAuthentication.jwt_auth) when validating the raw_token and
retrieving the user so SSE fallback uses the same configured JWT backend; update
calls to get_validated_token and get_user to use that shared jwt_auth instance.
In `@api/src/backend/api/sse/channelmanager.py`:
- Around line 13-29: get_channels_for_request currently returns
request.sse_channels without checking the active JWT tenant; change it to
filter/validate every channel in request.sse_channels against request.tenant_id
(the tenant set by BaseRLSViewSet / request.auth["tenant_id"]) by parsing
tenant_id_from_channel(channel) and only returning channels whose embedded
tenant equals request.tenant_id (fail-closed by excluding mismatches or
malformed channels); keep can_read_channel as the secondary membership backstop
but ensure the primary authorization uses request.tenant_id before handing
channels to django-eventstream.
In `@api/src/backend/api/tests/test_authentication.py`:
- Around line 392-400: Add a new assertion to the existing
test_header_present_delegates_to_super that sets both an Authorization header
and a query access_token (e.g., request.query_params = {"access_token":
"query-token"}) and verifies SSEAuthentication().authenticate(request) delegates
to the superclass authenticate (patch
SSEAuthentication.__bases__[0].authenticate as in the test) and returns the
super result; this ensures the header takes precedence over the ?access_token=
fallback.
In `@api/src/backend/config/guniconf.py`:
- Around line 28-35: The preload/reload and logging decisions are using the
config variable DEBUG instead of the actual runtime Django settings; update the
module to read settings.DEBUG and settings.LOGGING from the active settings
module (via django.conf.settings or by loading DJANGO_SETTINGS_MODULE) and use
those values when computing preload_app, reload and any logging configuration;
specifically change references that set preload_app = not DEBUG and any LOGGING
usage to use settings.DEBUG and settings.LOGGING (ensure settings is
imported/initialized before use) so Gunicorn behavior matches the active
settings module.
In `@docs/developer-guide/server-sent-events.mdx`:
- Around line 27-35: Update the documentation to use consistent
repository-relative paths (prefer the existing repo convention like
api/src/backend/api/...) for all referenced files: replace instances of
api/sse/base_views.py, api/sse/channelmanager.py, api/authentication.py,
api/sse/utils.py, config/settings/eventstream.py and api/tests/test_sse.py with
their repository-relative equivalents (e.g.,
api/src/backend/api/sse/base_views.py,
api/src/backend/api/sse/channelmanager.py,
api/src/backend/api/authentication.py, api/src/backend/api/sse/utils.py,
api/src/backend/config/settings/eventstream.py,
api/src/backend/api/tests/test_sse.py) and ensure all other mentions on the page
use the same convention so paths are uniform throughout the guide.
- Line 15: The documentation uses sentence case for several section headers;
update each listed header to Title Case to match the docs standard — e.g.,
change "When to use SSE", "How it works", "Local development" and the other
occurrences (lines referenced: the headers with texts at 25, 37, 41, 56, 164,
181, 200, 209, 218, 235) to Title Case (e.g., "When to Use SSE", "How It Works",
"Local Development"); ensure every header in
docs/developer-guide/server-sent-events.mdx follows Title-Case capitalization
consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3e3c1b2a-d789-4931-ac69-b50dae15e9b6
⛔ Files ignored due to path filters (1)
api/uv.lockis excluded by!**/*.lock,!**/uv.lock
📒 Files selected for processing (15)
api/CHANGELOG.mdapi/docker-entrypoint.shapi/pyproject.tomlapi/src/backend/api/authentication.pyapi/src/backend/api/sse/__init__.pyapi/src/backend/api/sse/base_views.pyapi/src/backend/api/sse/channelmanager.pyapi/src/backend/api/sse/utils.pyapi/src/backend/api/tests/test_authentication.pyapi/src/backend/api/tests/test_sse.pyapi/src/backend/config/django/base.pyapi/src/backend/config/guniconf.pyapi/src/backend/config/settings/eventstream.pydocs/developer-guide/server-sent-events.mdxdocs/docs.json
| def test_header_present_delegates_to_super(self): | ||
| request = MagicMock() | ||
| request.headers = {"Authorization": "Bearer header-token"} | ||
| with patch.object( | ||
| SSEAuthentication.__bases__[0], "authenticate", return_value=("user", "tok") | ||
| ) as super_auth: | ||
| result = SSEAuthentication().authenticate(request) | ||
| super_auth.assert_called_once_with(request) | ||
| assert result == ("user", "tok") |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Add an explicit "header beats query token" test.
The suite covers header delegation and query fallback separately, but it never exercises the advertised case where both are present. Please add one assertion with Authorization plus ?access_token= so this precedence rule stays locked down.
Suggested test shape
def test_header_present_delegates_to_super(self):
request = MagicMock()
request.headers = {"Authorization": "Bearer header-token"}
+ request.query_params = {"access_token": "query-jwt"}
with patch.object(
SSEAuthentication.__bases__[0], "authenticate", return_value=("user", "tok")
- ) as super_auth:
+ ) as super_auth, patch("api.authentication.JWTAuthentication") as jwt_auth:
result = SSEAuthentication().authenticate(request)
super_auth.assert_called_once_with(request)
+ jwt_auth.assert_not_called()
assert result == ("user", "tok")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def test_header_present_delegates_to_super(self): | |
| request = MagicMock() | |
| request.headers = {"Authorization": "Bearer header-token"} | |
| with patch.object( | |
| SSEAuthentication.__bases__[0], "authenticate", return_value=("user", "tok") | |
| ) as super_auth: | |
| result = SSEAuthentication().authenticate(request) | |
| super_auth.assert_called_once_with(request) | |
| assert result == ("user", "tok") | |
| def test_header_present_delegates_to_super(self): | |
| request = MagicMock() | |
| request.headers = {"Authorization": "Bearer header-token"} | |
| request.query_params = {"access_token": "query-jwt"} | |
| with patch.object( | |
| SSEAuthentication.__bases__[0], "authenticate", return_value=("user", "tok") | |
| ) as super_auth, patch("api.authentication.JWTAuthentication") as jwt_auth: | |
| result = SSEAuthentication().authenticate(request) | |
| super_auth.assert_called_once_with(request) | |
| jwt_auth.assert_not_called() | |
| assert result == ("user", "tok") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api/src/backend/api/tests/test_authentication.py` around lines 392 - 400, Add
a new assertion to the existing test_header_present_delegates_to_super that sets
both an Authorization header and a query access_token (e.g.,
request.query_params = {"access_token": "query-token"}) and verifies
SSEAuthentication().authenticate(request) delegates to the superclass
authenticate (patch SSEAuthentication.__bases__[0].authenticate as in the test)
and returns the super result; this ensures the header takes precedence over the
?access_token= fallback.
Source: Coding guidelines
| The platform ships the SSE **infrastructure** (`api.sse`) and wiring. No feature endpoint streams over SSE out of the box — this guide shows how to build one on top of the shared base. | ||
| </Info> | ||
|
|
||
| ## When to use SSE |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Standardize headers to Title Case across the page.
Several section headers are sentence case (for example, “When to use SSE”, “How it works”, “Local development”). Apply one capitalization style consistently to match the documentation standard.
As per coding guidelines, "Use Title-Case capitalization for all titles and headers in documentation."
Also applies to: 25-25, 37-37, 41-41, 56-56, 164-164, 181-181, 200-200, 209-209, 218-218, 235-235
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/developer-guide/server-sent-events.mdx` at line 15, The documentation
uses sentence case for several section headers; update each listed header to
Title Case to match the docs standard — e.g., change "When to use SSE", "How it
works", "Local development" and the other occurrences (lines referenced: the
headers with texts at 25, 37, 41, 56, 164, 181, 200, 209, 218, 235) to Title
Case (e.g., "When to Use SSE", "How It Works", "Local Development"); ensure
every header in docs/developer-guide/server-sent-events.mdx follows Title-Case
capitalization consistently.
Source: Coding guidelines
| SSE is wired through [`django-eventstream`](https://github.com/fanout/django_eventstream) and a small platform layer in `api/src/backend/api/sse/`: | ||
|
|
||
| | Piece | File | Responsibility | | ||
| |-------|------|----------------| | ||
| | `BaseSSEViewSet` | `api/sse/base_views.py` | Base DRF viewset a feature subclasses. The feature implements `get_channels`; the base handles auth, the tenant transaction, and delegates streaming to `django-eventstream`. | | ||
| | `SSEChannelManager` | `api/sse/channelmanager.py` | Registered in `settings.EVENTSTREAM_CHANNELMANAGER_CLASS`. Reads the channel set off the request and enforces the platform-wide tenant gate. | | ||
| | `SSEAuthentication` | `api/authentication.py` | Same JWT/API-key stack as the rest of the API, plus an `?access_token=<jwt>` fallback for browser `EventSource` clients. Lives with the other authentication classes, not in the `sse` package. | | ||
| | `make_channel_name` / `tenant_id_from_channel` | `api/sse/utils.py` | Single source of truth for the channel-name format, so publishers and the channel manager agree byte-for-byte. | | ||
| | Settings | `config/settings/eventstream.py` | Valkey Pub/Sub backend (dedicated DB), channel manager, allowed headers. | |
There was a problem hiding this comment.
Use consistent repository-relative paths for referenced files.
This page mixes api/src/backend/api/sse/... with shorter api/sse/... and later references api/tests/test_sse.py, which can mislead contributors about actual file locations. Standardize all path references to one convention (prefer repository-relative paths used elsewhere in this guide).
Also applies to: 237-237
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/developer-guide/server-sent-events.mdx` around lines 27 - 35, Update the
documentation to use consistent repository-relative paths (prefer the existing
repo convention like api/src/backend/api/...) for all referenced files: replace
instances of api/sse/base_views.py, api/sse/channelmanager.py,
api/authentication.py, api/sse/utils.py, config/settings/eventstream.py and
api/tests/test_sse.py with their repository-relative equivalents (e.g.,
api/src/backend/api/sse/base_views.py,
api/src/backend/api/sse/channelmanager.py,
api/src/backend/api/authentication.py, api/src/backend/api/sse/utils.py,
api/src/backend/config/settings/eventstream.py,
api/src/backend/api/tests/test_sse.py) and ensure all other mentions on the page
use the same convention so paths are uniform throughout the guide.
The ?access_token= fallback in SSEAuthentication created a fresh JWTAuthentication() instead of the shared CombinedJWTOrAPIKeyAuthentication.jwt_auth instance used by the header path. Reuse self.jwt_auth so the query-token fallback stays on the same configured backend if the parent auth stack is ever customized. Patch the shared instance instead of the constructor in the SSE auth tests.
SSEChannelManager.get_channels_for_request returned every channel stashed on the request, leaving can_read_channel (a membership check) as the only tenant gate. A user belonging to multiple tenants could then read another tenant's stream if a viewset ever returned the wrong channel set. Filter request.sse_channels against the active JWT tenant (request.tenant_id, set by BaseRLSViewSet) before handing channels to django-eventstream, keeping can_read_channel as the membership backstop. Fail-closed: a missing or unparseable request tenant, or a malformed channel, yields no channels. Add type hints and docstrings to the manager methods and cover the cross-tenant, malformed, and missing-tenant cases in the tests.
Context
The API needs a reusable foundation for Server-Sent Events (SSE) so endpoints can push a one-way stream of events to clients over a single long-lived HTTP connection (live progress, token-by-token LLM output, cross-client sync). This work lands the shared SSE infrastructure and developer documentation so feature endpoints can adopt it. No existing endpoint is converted to SSE in this PR — the goal is to provide the basis.
Description
Adds the platform SSE layer that wires
django-eventstreaminto the API, plus the runtime and auth changes needed to serve streams:django-eventstreamand bumpgunicornto the release that ships the nativeasgiworker.asgiworker againstconfig.asgiso SSE streams are parked on the event loop instead of holding a sync worker per connection (sync CRUD views keep running in the thread-sensitive executor).preload_appis disabled under DEBUG so dev reload works; dev and prod entrypoints point at the ASGI app.SSEAuthentication: extends the standard JWT/API-key stack with an?access_token=<jwt>query-parameter fallback (RFC 6750 §2.3), since browserEventSourcecannot set theAuthorizationheader. The query path accepts a JWT only; API keys remain header-only.api/sse/):BaseSSEViewSet(subclass + implementget_channels, reuses the regular DRF auth/RBAC/tenant-transaction stack),SSEChannelManager(tenant gate via the tenant id embedded in the channel name), andmake_channel_name/tenant_id_from_channel(single source of truth for the<prefix>:<tenant_id>:<resource_id>channel format). Valkey Pub/Sub backend configured on a dedicated DB.resource.verbevent-naming convention, auth, the tenant-isolation model, and reconnect/state recovery.New dependencies:
django-eventstream==5.3.3,gunicorn==26.0.0(was23.0.0).Steps to review
docs/developer-guide/server-sent-events.mdx) for the intended architecture and the worked example.api/src/backend/api/sse/): the base viewset, the channel manager's two-layer authorization (resource lookup inget_channels+ tenant gate incan_read_channel), and the channel-name helpers.SSEAuthenticationinapi/src/backend/api/authentication.pyand the header-wins / query-fallback precedence.config/guniconf.py,docker-entrypoint.sh, and the settings wiring inconfig/django/base.py+config/settings/eventstream.py.Checklist
Community Checklist
SDK/CLI
API
uv.lockupdated[1.32.0] (Prowler UNRELEASED)License
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
Summary by CodeRabbit
New Features
Chores
Documentation