From 4f24f8785cc530118412a6ee7b70d3e0ee702aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Ro=C5=BCnawski?= Date: Fri, 17 Jul 2026 09:48:35 +0200 Subject: [PATCH 1/3] FCE-3559: Document webhook signature verification - Document the x-fishjam-signature-256 header and SDK verification helpers (verifyWebhookSignature / verify_webhook_signature) with examples - Point webhook URL configuration at the Dashboard Webhooks tab instead of per-room webhookUrl params - De-duplicate webhook prose: server-setup is canonical, examples link to it - Bump js-server-sdk and python-server-sdk submodules to include the helpers --- docs/api/reference.md | 9 +-- docs/how-to/backend/fastapi-example.mdx | 17 +++--- docs/how-to/backend/fastify-example.mdx | 23 +++++--- docs/how-to/backend/production-deployment.mdx | 38 ++++++++---- docs/how-to/backend/server-setup.mdx | 58 +++++++++++++++++-- docs/tutorials/backend-quick-start.mdx | 2 +- packages/js-server-sdk | 2 +- packages/python-server-sdk | 2 +- 8 files changed, 113 insertions(+), 38 deletions(-) diff --git a/docs/api/reference.md b/docs/api/reference.md index d0d7f9ae..e10073e1 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -32,13 +32,14 @@ The notifications can be configured using Webhook or Websocket. #### Webhook -When using webhooks for receiving notifications, the `webhookUrl` must be passed -in the `RoomConfig` options when creating a room. +When using webhooks for receiving notifications, configure your webhook URL in +the **Webhooks** tab of the [Fishjam Dashboard](https://fishjam.io/app). +Fishjam then delivers all notifications to that URL. -The HTTP POST to the `webhookUrl` uses "application/x-protobuf" content type. +The HTTP POST to your webhook URL uses "application/x-protobuf" content type. The body is binary data, that represents encoded `ServerMessage`. -Setting `batchWebhookNotifications` to `true` in the `RoomConfig` is recommended. Fishjam then coalesces several notifications into one POST: the body is still a single `ServerMessage`, but its `notification_batch` field holds a `NotificationBatch`, which carries the individual notifications as a repeated list of `ServerMessage`s (see `server_notifications.proto`). This delivers notifications faster and with fewer requests. The SDK decoders (`decodeServerNotifications` / `decode_server_notifications`) unwrap the batch for you, returning the notifications as a flat list — so a single notification and a batch are handled the same way. +Setting `batchWebhookNotifications` to `true` in the `RoomConfig` is recommended. Fishjam then coalesces several notifications into one POST: the body is still a single `ServerMessage`, but its `notification_batch` field holds a `NotificationBatch`, which carries the individual notifications as a repeated list of `ServerMessage`s (see `server_notifications.proto`). This delivers notifications faster and with fewer requests. The SDK decoders unwrap the batch for you. For more information see also [server setup documentation](../how-to/backend/server-setup#webhooks) diff --git a/docs/how-to/backend/fastapi-example.mdx b/docs/how-to/backend/fastapi-example.mdx index 844b3fad..0163749b 100644 --- a/docs/how-to/backend/fastapi-example.mdx +++ b/docs/how-to/backend/fastapi-example.mdx @@ -38,28 +38,29 @@ async def get_rooms(fishjam_client: Annotated[FishjamClient, Depends(fishjam_cli ## Listening to events -Fishjam instance is a stateful server that is emitting messages upon certain events. -You can listen for those messages and react as you prefer. -There are two options for obtaining these. +Fishjam emits messages upon certain events — see [Listening to events](../../how-to/backend/server-setup#listening-to-events) for how delivery, batching, and signing work. ### Webhooks -To receive and parse Fishjam protobuf messages, create a dependency function that decodes the request body using the `decode_server_notifications` function from the `fishjam` package. -It returns a list of notifications and transparently unwraps batched payloads (rooms created with `batch_webhook_notifications=True`), so you can iterate over the result and use pattern matching to respond to different kinds of events. +Create a dependency function that [verifies the webhook signature](../../how-to/backend/server-setup#verifying-webhook-signatures) and decodes the request body into notifications, then use pattern matching to respond to different kinds of events. :::note -`decode_server_notifications` replaces the now-deprecated `receive_binary`. It always returns a list — a single notification comes back as a one-element list — which lets the same handler work whether or not batching is enabled. +`decode_server_notifications` replaces the now-deprecated `receive_binary`. ::: ```python -from fastapi import Depends, FastAPI, Request -from fishjam import decode_server_notifications +import os +from fastapi import Depends, FastAPI, HTTPException, Request +from fishjam import decode_server_notifications, verify_webhook_signature from fishjam.events import ServerMessagePeerAdded app = FastAPI() async def parse_fishjam_notifications(request: Request): binary = await request.body() + signature = request.headers.get("x-fishjam-signature-256", "") + if not verify_webhook_signature(binary, signature, os.environ["FISHJAM_WEBHOOK_SECRET"]): + raise HTTPException(status_code=401, detail="Invalid webhook signature") return decode_server_notifications(binary) @app.post("/fishjam-webhook") diff --git a/docs/how-to/backend/fastify-example.mdx b/docs/how-to/backend/fastify-example.mdx index 0c55548d..93c32fb5 100644 --- a/docs/how-to/backend/fastify-example.mdx +++ b/docs/how-to/backend/fastify-example.mdx @@ -111,20 +111,18 @@ fastify.listen({ port: 3000 }); ## Listening to events -Fishjam instance is a stateful server that is emitting messages upon certain events. -You can listen for those messages and react as you prefer. -There are two options to obtain these. +Fishjam emits messages upon certain events — see [Listening to events](../../how-to/backend/server-setup#listening-to-events) for how delivery, batching, and signing work. ### Webhooks -To receive and parse the Fishjam protobuf messages, add a content type parser to your global (or scoped) Fastify instance. -Use `decodeServerNotifications` to decode the request body: it returns a list of typed notifications and transparently unwraps a [batch](../../how-to/backend/server-setup#webhooks), so rooms created with `batchWebhookNotifications: true` are handled the same as single notifications — a single notification simply arrives as a one-element list. -Then, you will be able to iterate over the parsed notifications at `request.body`. +Add a content type parser to your global (or scoped) Fastify instance that [verifies the webhook signature](../../how-to/backend/server-setup#verifying-webhook-signatures) and decodes the body into typed notifications. +You can then iterate over the parsed notifications at `request.body`. ```ts title='main.ts' import Fastify, { FastifyRequest } from "fastify"; import { decodeServerNotifications, + verifyWebhookSignature, ServerNotification, } from "@fishjam-cloud/js-server-sdk"; @@ -133,7 +131,18 @@ const fastify = Fastify(); fastify.addContentTypeParser( "application/x-protobuf", { parseAs: "buffer" }, - async (_: FastifyRequest, body: Buffer) => decodeServerNotifications(body), + async (request: FastifyRequest, body: Buffer) => { + const signature = request.headers["x-fishjam-signature-256"]; + if ( + typeof signature !== "string" || + !verifyWebhookSignature(body, signature, process.env.FISHJAM_WEBHOOK_SECRET!) + ) { + throw Object.assign(new Error("Invalid webhook signature"), { + statusCode: 401, + }); + } + return decodeServerNotifications(body); + }, ); fastify.post<{ Body: ServerNotification[] }>("/fishjam-webhook", (request) => { diff --git a/docs/how-to/backend/production-deployment.mdx b/docs/how-to/backend/production-deployment.mdx index aa188d5e..40fc0334 100644 --- a/docs/how-to/backend/production-deployment.mdx +++ b/docs/how-to/backend/production-deployment.mdx @@ -226,14 +226,17 @@ All you need for this is a single api endpoint: ### Webhook endpoint -Fishjam delivers notifications as a binary `application/x-protobuf` body. Read the raw body and decode it with the SDK, then react to the events you care about. +Fishjam delivers notifications as a binary `application/x-protobuf` body. Read the raw body, [verify the webhook signature](../../how-to/backend/server-setup#verifying-webhook-signatures), and decode it with the SDK, then react to the events you care about. Store your webhook secret as `FISHJAM_WEBHOOK_SECRET`. ```typescript import express from "express"; - import { decodeServerNotifications } from "@fishjam-cloud/js-server-sdk"; + import { + decodeServerNotifications, + verifyWebhookSignature, + } from "@fishjam-cloud/js-server-sdk"; const app = express(); const handlePeerConnected = {} as any; @@ -245,6 +248,17 @@ Fishjam delivers notifications as a binary `application/x-protobuf` body. Read t "/api/webhooks/fishjam", express.raw({ type: "application/x-protobuf" }), (req: express.Request, res: express.Response) => { + const signature = req.headers["x-fishjam-signature-256"] as string; + if ( + !verifyWebhookSignature( + req.body, + signature, + process.env.FISHJAM_WEBHOOK_SECRET!, + ) + ) { + return res.status(401).json({ error: "Invalid signature" }); + } + for (const { type, notification } of decodeServerNotifications(req.body)) { switch (type) { case "peerConnected": @@ -271,8 +285,9 @@ Fishjam delivers notifications as a binary `application/x-protobuf` body. Read t ```python - from fastapi import FastAPI, Request - from fishjam import decode_server_notifications + import os + from fastapi import FastAPI, HTTPException, Request + from fishjam import decode_server_notifications, verify_webhook_signature from fishjam.events import ( ServerMessagePeerConnected, ServerMessagePeerDisconnected, @@ -283,7 +298,12 @@ Fishjam delivers notifications as a binary `application/x-protobuf` body. Read t @app.post("/api/webhooks/fishjam") async def fishjam_webhook(request: Request): - for notification in decode_server_notifications(await request.body()): + raw_body = await request.body() + signature = request.headers.get("x-fishjam-signature-256", "") + if not verify_webhook_signature(raw_body, signature, os.environ["FISHJAM_WEBHOOK_SECRET"]): + raise HTTPException(status_code=401, detail="Invalid signature") + + for notification in decode_server_notifications(raw_body): match notification: case ServerMessagePeerConnected(): handle_peer_connected(notification) @@ -301,7 +321,7 @@ Fishjam delivers notifications as a binary `application/x-protobuf` body. Read t ### Enabling webhooks -Now, with your endpoint setup, all you need to do is supply your webhook endpoint to Fishjam when creating a room. We also recommend enabling `batchWebhookNotifications`, which delivers notifications faster and with fewer HTTP requests for a better backend response time under load: +Now, with your endpoint set up, register its URL in the **Webhooks** tab of the [Fishjam Dashboard](https://fishjam.io/app). Fishjam then delivers all notifications to that endpoint. We also recommend enabling [notification batching](../../how-to/backend/server-setup#webhooks) when creating rooms: @@ -311,10 +331,9 @@ Now, with your endpoint setup, all you need to do is supply your webhook endpoin const fishjamClient = {} as any; // ---cut--- - const createRoomWithWebhooks = async (roomType = "conference") => { + const createBatchedRoom = async (roomType = "conference") => { const room = await fishjamClient.createRoom({ roomType, - webhookUrl: `${process.env.BASE_URL}/api/webhooks/fishjam`, // Coalesce notifications into batches for faster delivery and fewer requests batchWebhookNotifications: true, }); @@ -336,10 +355,9 @@ Now, with your endpoint setup, all you need to do is supply your webhook endpoin management_token=os.environ["FISHJAM_MANAGEMENT_TOKEN"], ) - def create_room_with_webhooks(room_type="conference"): + def create_batched_room(room_type="conference"): options = RoomOptions( room_type=room_type, - webhook_url=f"{os.environ['BASE_URL']}/api/webhooks/fishjam", # Coalesce notifications into batches for faster delivery and fewer requests batch_webhook_notifications=True, ) diff --git a/docs/how-to/backend/server-setup.mdx b/docs/how-to/backend/server-setup.mdx index 92633fb1..f5ba15a5 100644 --- a/docs/how-to/backend/server-setup.mdx +++ b/docs/how-to/backend/server-setup.mdx @@ -226,9 +226,9 @@ There are two options to obtain these. ### Webhooks -Simply pass your webhook URL as a `webhookUrl` parameter when creating a room. +Configure your webhook URL in the **Webhooks** tab of the [Fishjam Dashboard](https://fishjam.io/app). Fishjam then delivers all notifications to that URL. -We recommend also enabling **notification batching** via `batchWebhookNotifications`. Fishjam then coalesces several notifications into a single request, delivering them faster and with fewer HTTP requests — which improves your backend's response time under load. +We recommend also enabling **notification batching** when creating a room. Fishjam then coalesces several notifications into a single request, delivering them faster and with fewer HTTP requests — which improves your backend's response time under load. @@ -241,8 +241,7 @@ We recommend also enabling **notification batching** via `batchWebhookNotificati managementToken: "bbb", }); // ---cut--- - const webhookUrl = "https://example.com/"; - await fishjamClient.createRoom({ webhookUrl, batchWebhookNotifications: true }); + await fishjamClient.createRoom({ batchWebhookNotifications: true }); ``` @@ -253,7 +252,6 @@ We recommend also enabling **notification batching** via `batchWebhookNotificati from fishjam import RoomOptions options = RoomOptions( - webhook_url="https://example.com/", batch_webhook_notifications=True, ) @@ -264,7 +262,7 @@ We recommend also enabling **notification batching** via `batchWebhookNotificati -On the receiving side, decode the raw request body with `decodeServerNotifications` (Node) or `decode_server_notifications` (Python), then iterate the result and react to the events you care about. Both decoders return a list of notifications and transparently unwrap a batch — a single notification simply comes back as a one-element list, so the same handler works whether or not batching is enabled. +On the receiving side, decode the raw request body with the SDK's decoder, then iterate the result and react to the events you care about. The decoder returns a list of notifications and transparently unwraps a batch — a single notification simply comes back as a one-element list, so the same handler works whether or not batching is enabled. @@ -319,6 +317,54 @@ On the receiving side, decode the raw request body with `decodeServerNotificatio +#### Verifying webhook signatures + +Every webhook request is signed so you can confirm it really came from Fishjam. Each delivery carries an `x-fishjam-signature-256: sha256=` header — an HMAC-SHA256 of the **raw request body**, keyed with your webhook secret. Find (and rotate) that secret in the **Webhooks** tab of the [Fishjam Dashboard](https://fishjam.io/app). + +Verify the header against the raw body **before** decoding, and reject mismatches with `401`. Verification needs the exact bytes Fishjam sent, so read the raw body before any parsing. + + + + + ```ts + declare const rawBody: Buffer; + declare const signatureHeader: string; + // ---cut--- + import { verifyWebhookSignature, decodeServerNotifications } from '@fishjam-cloud/js-server-sdk'; + + const secret = process.env.FISHJAM_WEBHOOK_SECRET!; + + // rawBody: Buffer, signatureHeader: req.headers['x-fishjam-signature-256'] + if (!verifyWebhookSignature(rawBody, signatureHeader, secret)) { + throw new Error('Invalid webhook signature'); // reject the request with 401 + } + + // signature is valid — safe to decode + const notifications = decodeServerNotifications(rawBody); + ``` + + + + + + ```python + import os + from fishjam import verify_webhook_signature, decode_server_notifications + + secret = os.environ["FISHJAM_WEBHOOK_SECRET"] + + # raw_body: bytes, signature_header: request.headers["x-fishjam-signature-256"] + if not verify_webhook_signature(raw_body, signature_header, secret): + raise PermissionError("Invalid webhook signature") # reject the request with 401 + + # signature is valid — safe to decode + notifications = decode_server_notifications(raw_body) + ``` + + + + + See the [Fastify](../../how-to/backend/fastify-example#webhooks) and [FastAPI](../../how-to/backend/fastapi-example#webhooks) examples for a full webhook handler wired into a web framework. ### SDK Notifier diff --git a/docs/tutorials/backend-quick-start.mdx b/docs/tutorials/backend-quick-start.mdx index 9bbe2d8c..d684afdd 100644 --- a/docs/tutorials/backend-quick-start.mdx +++ b/docs/tutorials/backend-quick-start.mdx @@ -562,7 +562,7 @@ Here's a complete working backend: Now that you have a working backend, explore these guides: - [How to set up a production deployment](../how-to/backend/production-deployment) -- [How to handle webhooks and events](../how-to/backend/server-setup) +- [How to handle webhooks and events](../how-to/backend/server-setup#webhooks) - [How to implement a FastAPI backend](../how-to/backend/fastapi-example) - [How to implement a Fastify backend](../how-to/backend/fastify-example) diff --git a/packages/js-server-sdk b/packages/js-server-sdk index c2dd85ec..c5bee41f 160000 --- a/packages/js-server-sdk +++ b/packages/js-server-sdk @@ -1 +1 @@ -Subproject commit c2dd85ecb40e54ce300b8c1b8f811dbdba726213 +Subproject commit c5bee41f041afb516dcc4ff13f79bdaa25d5ad97 diff --git a/packages/python-server-sdk b/packages/python-server-sdk index fdd55700..82f98978 160000 --- a/packages/python-server-sdk +++ b/packages/python-server-sdk @@ -1 +1 @@ -Subproject commit fdd5570033f84a4e8f49bf2ac7fedc019b5b99df +Subproject commit 82f9897861446a232ec1a0f36c80092a50a7095c From 278c1c5eceb800f67ab700c5870b76ce41d6eac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Ro=C5=BCnawski?= Date: Fri, 17 Jul 2026 09:55:19 +0200 Subject: [PATCH 2/3] Address Copilot review comments - Clarify that the 401 response in the signature snippets is framework-specific (see linked framework examples) - Type-guard the x-fishjam-signature-256 header in the Express example instead of casting --- docs/how-to/backend/production-deployment.mdx | 3 ++- docs/how-to/backend/server-setup.mdx | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/how-to/backend/production-deployment.mdx b/docs/how-to/backend/production-deployment.mdx index 40fc0334..5d8f5ca7 100644 --- a/docs/how-to/backend/production-deployment.mdx +++ b/docs/how-to/backend/production-deployment.mdx @@ -248,8 +248,9 @@ Fishjam delivers notifications as a binary `application/x-protobuf` body. Read t "/api/webhooks/fishjam", express.raw({ type: "application/x-protobuf" }), (req: express.Request, res: express.Response) => { - const signature = req.headers["x-fishjam-signature-256"] as string; + const signature = req.headers["x-fishjam-signature-256"]; if ( + typeof signature !== "string" || !verifyWebhookSignature( req.body, signature, diff --git a/docs/how-to/backend/server-setup.mdx b/docs/how-to/backend/server-setup.mdx index f5ba15a5..71dedaf1 100644 --- a/docs/how-to/backend/server-setup.mdx +++ b/docs/how-to/backend/server-setup.mdx @@ -336,7 +336,8 @@ Verify the header against the raw body **before** decoding, and reject mismatche // rawBody: Buffer, signatureHeader: req.headers['x-fishjam-signature-256'] if (!verifyWebhookSignature(rawBody, signatureHeader, secret)) { - throw new Error('Invalid webhook signature'); // reject the request with 401 + // respond with 401 here — how depends on your framework, see the examples linked below + throw new Error('Invalid webhook signature'); } // signature is valid — safe to decode @@ -355,7 +356,8 @@ Verify the header against the raw body **before** decoding, and reject mismatche # raw_body: bytes, signature_header: request.headers["x-fishjam-signature-256"] if not verify_webhook_signature(raw_body, signature_header, secret): - raise PermissionError("Invalid webhook signature") # reject the request with 401 + # respond with 401 here — how depends on your framework, see the examples linked below + raise PermissionError("Invalid webhook signature") # signature is valid — safe to decode notifications = decode_server_notifications(raw_body) From d91cb02d140522db745ede5eafa55016892c7a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Ro=C5=BCnawski?= Date: Fri, 17 Jul 2026 09:56:41 +0200 Subject: [PATCH 3/3] Fix prettier formatting in fastify-example --- docs/how-to/backend/fastify-example.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/how-to/backend/fastify-example.mdx b/docs/how-to/backend/fastify-example.mdx index 93c32fb5..15fe9fb8 100644 --- a/docs/how-to/backend/fastify-example.mdx +++ b/docs/how-to/backend/fastify-example.mdx @@ -135,7 +135,11 @@ fastify.addContentTypeParser( const signature = request.headers["x-fishjam-signature-256"]; if ( typeof signature !== "string" || - !verifyWebhookSignature(body, signature, process.env.FISHJAM_WEBHOOK_SECRET!) + !verifyWebhookSignature( + body, + signature, + process.env.FISHJAM_WEBHOOK_SECRET!, + ) ) { throw Object.assign(new Error("Invalid webhook signature"), { statusCode: 401,