<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>FrameWorks Docs | Blog</title><description/><link>https://logbook.frameworks.network/</link><language>en</language><item><title>Restreams Send the Original Video Wherever It Fits</title><link>https://logbook.frameworks.network/blog/restream-video-selection/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/restream-video-selection/</guid><description>When one live stream goes to several destinations, each restream now gets the original video if the destination accepts it and a compatible rendition if it does not, without encoding again what already fits.

</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A broadcaster publishes one high-bitrate 1080p60 H.264 stream to FrameWorks and restreams it to two
places: YouTube and Kick. YouTube takes the stream as it is. Kick documents a hard ceiling of 8 Mbps
for video, and this stream is above it. One input, two destinations with different rules.&lt;/p&gt;
&lt;p&gt;Each restream now picks its video separately. YouTube gets the original video, untouched. Kick gets a
rendition that fits under its ceiling, taken from the renditions the stream already produces for
adaptive playback. Nothing is encoded a second time for either destination.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-passthrough-matters&quot;&gt;Why passthrough matters&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every encode between the broadcaster and a destination adds delay, costs compute, and loses some
quality, because a lossy encoder working from already-compressed video cannot get back what the first
encode dropped. When the original video already meets a destination’s rules, the best thing to send
is those exact bytes. For YouTube in this scenario that is what happens: the audience there sees what
the broadcaster’s encoder produced.&lt;/p&gt;
&lt;p&gt;The capped destination still needs something it will accept. A stream with a live video ladder is
already producing lower renditions so that viewers on slower connections can play it. Choosing one of
those for Kick means the restream costs no encode of its own. The decision is made per destination,
from the tracks the stream actually has and the limits that destination documents, when the restream
starts.&lt;/p&gt;
&lt;p&gt;Audio is chosen on its own. A browser publishing over WebRTC sends Opus audio, and RTMP destinations
expect AAC. The restream can take the original video and a converted AAC track together, so a
browser-published stream can still go out with its video untouched.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;when-a-stream-does-not-need-a-ladder&quot;&gt;When a stream does not need a ladder&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Some streams have no use for lower renditions, such as a stream that exists mainly to be restreamed
to destinations that accept the original. For those, the live video ladder can now be turned off per
stream. Audio conversion, thumbnails, and recording keep working, and the stream no longer produces
video renditions nobody watches.&lt;/p&gt;
&lt;p&gt;The trade-off is direct. With the ladder off, there is no rendition to fall back on, so every
destination has to accept the original video. In the scenario above, Kick would have nothing it can
take. If you need to be sure which kind of video a destination receives, each restream target can also
be set to require the original or require a rendition, instead of choosing automatically.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-does-not-check&quot;&gt;What it does not check&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Destination limits are applied where the platform documents a hard ceiling. For YouTube, Twitch, and
custom RTMP destinations, the restream checks that the codec is accepted but does not enforce their
recommended bitrates, because those are recommendations rather than limits. Whether a publisher’s
encoder keeps a constant bitrate or a fixed keyframe interval is not visible from the stream’s track
information, so a destination that requires either still depends on how the encoder is configured.
And a destination can refuse a publish for reasons of its own.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/builders/multistreaming&quot;&gt;multistreaming guide&lt;/a&gt; lists each preset’s limits, the per-target video
setting, and how to turn off the live video ladder for a stream.&lt;/p&gt;</content:encoded><category>announcements</category><category>engineering</category></item><item><title>Moving From Livepeer Studio: Webhooks, Playback Gates, and URL Import</title><link>https://logbook.frameworks.network/blog/studio-parity/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/studio-parity/</guid><description>We compared every Livepeer Studio webhook event, payload field, and playback-policy option against FrameWorks and closed the gaps that would force a Studio integration to change its logic, from playback gates that rejected a 204 to importing VOD assets from a URL.

</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Teams moving from Livepeer Studio told us two things take the longest to port: VOD and webhooks. We read
Studio’s source (it is open) and went through every webhook event, payload field, and playback-policy
option it offers, then compared each one against FrameWorks. This release closes the gaps that would have
forced a Studio integration to change its logic.&lt;/p&gt;
&lt;p&gt;The goal was narrow: a team should be able to point its existing webhook receiver and playback gate at
FrameWorks, change the parts that are named differently, and keep the logic behind them.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-204-used-to-lock-out-every-viewer&quot;&gt;A 204 used to lock out every viewer&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The clearest example came from the playback gate. A webhook playback policy asks your server whether a
viewer may watch: FrameWorks sends the request, and your answer decides. Studio treats any &lt;code dir=&quot;auto&quot;&gt;2xx&lt;/code&gt; as
“allow”. We only accepted &lt;code dir=&quot;auto&quot;&gt;200&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That difference is invisible until you migrate. Plenty of access endpoints answer &lt;code dir=&quot;auto&quot;&gt;204 No Content&lt;/code&gt;,
because there is nothing to say beyond yes. Pointed at FrameWorks, such an endpoint denied every viewer
of every gated stream, and nothing in its own logs looked wrong: it had answered yes each time. Any &lt;code dir=&quot;auto&quot;&gt;2xx&lt;/code&gt;
now allows.&lt;/p&gt;
&lt;p&gt;The same comparison turned up two smaller gaps in the gate. A policy can now store a small JSON context
that is sent back with every access request, which Studio calls &lt;code dir=&quot;auto&quot;&gt;webhookContext&lt;/code&gt;. It lets your endpoint
know which course or product a video belongs to without keeping its own lookup table. And a policy can
list the websites allowed to embed the content.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-an-origin-check-can-and-cannot-stop&quot;&gt;What an origin check can and cannot stop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Allowed origins stop a player copied onto someone else’s site. The check runs twice: when the player
looks up the stream, and again when the media session opens on the edge. For the second check, our
MistServer build now passes each viewer’s &lt;code dir=&quot;auto&quot;&gt;Origin&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;Referer&lt;/code&gt; to the access decision, and requests
from a different site get a separate session.&lt;/p&gt;
&lt;p&gt;It is not DRM. A client that is not a browser can send any &lt;code dir=&quot;auto&quot;&gt;Origin&lt;/code&gt; header it likes. RTMP and SRT players
send no origin at all, so they are refused unless the list allows every site. Edges running a MistServer
build from before this release cannot report the origin, so origin-restricted content does not play from
them until they update. If you run your own edges, update them before you turn this on.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;events-carry-what-studio-events-carried&quot;&gt;Events carry what Studio events carried&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Studio’s stream webhooks carry the stream’s playback ID, and ours now do too. Recordings now send an
event when capture starts and another when it stops, the two points Studio reports.&lt;/p&gt;
&lt;p&gt;One difference is deliberate. Clip, recording, and upload events do not carry a playback ID. A playback
ID can be rotated, so a copy inside an event that is retried or replayed hours later can already be out
of date. Key your records on the artifact ID and look up the current playback ID when you publish the
video.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/migrate/livepeer-studio&quot;&gt;migration guide&lt;/a&gt; maps each Studio event and field to its FrameWorks
equivalent, including the one habit that does not carry over directly: how a Studio player passes the
gate’s key.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;import-a-video-from-a-url&quot;&gt;Import a video from a URL&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Studio lets you create an asset from a URL, and so does FrameWorks now. You pass a public link and get
the asset and its playback ID back right away, with no upload step. The file is read from the source
server and processed like any upload, with the same upload events, so a receiver that already handles
uploads needs no new code.&lt;/p&gt;
&lt;p&gt;The fetch connects only to public addresses, checked again on every connection and redirect, so an
import URL cannot be used to reach a private network. The source server has to support range requests.
For IPFS or Arweave content, pass a public gateway’s https link. The
&lt;a href=&quot;https://logbook.frameworks.network/builders/recordings#import-a-video-from-a-url&quot;&gt;VOD guide&lt;/a&gt; has the details.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-stays-different&quot;&gt;What stays different&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Webhook signing uses &lt;a href=&quot;https://logbook.frameworks.network/builders/webhooks&quot;&gt;Standard Webhooks&lt;/a&gt; rather than Studio’s &lt;code dir=&quot;auto&quot;&gt;Livepeer-Signature&lt;/code&gt;
header, so a receiver needs a new verification step, which existing libraries cover. Retries run for
about three days instead of one. There is no Task API, no per-stream transcode profiles, and no
&lt;code dir=&quot;auto&quot;&gt;creatorId&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;userTags&lt;/code&gt; on streams yet. The
&lt;a href=&quot;https://logbook.frameworks.network/migrate/livepeer-studio#whats-not-yet-at-parity-and-whats-coming&quot;&gt;gap list&lt;/a&gt; in the migration guide
tracks what is still missing, and the &lt;a href=&quot;https://logbook.frameworks.network/builders/playback-access-control&quot;&gt;playback access guide&lt;/a&gt;
covers webhook policies and allowed origins.&lt;/p&gt;</content:encoded><category>announcements</category><category>engineering</category></item><item><title>SDKs for TypeScript, Go, and Python</title><link>https://logbook.frameworks.network/blog/sdks-typescript-go-python/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/sdks-typescript-go-python/</guid><description>FrameWorks now publishes SDKs for TypeScript, Go, and Python. All three are generated from the same GraphQL operations, share one version, and pass the same conformance fixtures, so a backend gets the same behavior in any of them.

</description><pubDate>Sat, 19 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Until now, a FrameWorks integration outside the browser started with a GraphQL client of your
choice and a copy of the schema. The player and StreamCrafter packages covered playback and
browser ingest, but creating streams, uploading files, signing viewer tokens, and checking webhook
signatures from a backend meant writing that code yourself. That changes today with three
packages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@livepeer-frameworks/api&lt;/code&gt; on npm, for Node 22 and later and for browsers.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;github.com/Livepeer-FrameWorks/sdk-go&lt;/code&gt; for Go.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;livepeer-frameworks&lt;/code&gt; on PyPI, with a synchronous and an asynchronous client.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;one-set-of-operations-three-languages&quot;&gt;One set of operations, three languages&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Each SDK generates its types from the public GraphQL schema and the same operation documents,
using a standard code generator per language. When an operation changes, CI regenerates all three
and fails if the committed code differs. The first release shipped 44 hand-written operations
covering streams, clips, DVR, VOD uploads, playback access control, multistreaming, and developer
tokens. The current 0.3 line adds a generated operation for every public root field that is not
deprecated, 227 in total, so the whole public API has typed methods.&lt;/p&gt;
&lt;p&gt;Creating a stream from a Node backend looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { createClient, CreateStreamDocument, expectResult } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;@livepeer-frameworks/api&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;createClient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;url: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;https://api.example.com/graphql&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;token: &lt;/span&gt;&lt;span&gt;process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;FRAMEWORKS_API_TOKEN&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;created&lt;/span&gt;&lt;span&gt; = await &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;(CreateStreamDocument&lt;/span&gt;&lt;span&gt;, { input: { name: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Launch stream&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; } }&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;expectResult&lt;/span&gt;&lt;span&gt;(created&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createStream&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Stream&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(stream&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;playbackId&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Most mutations return a union of the result and typed errors. &lt;code dir=&quot;auto&quot;&gt;expectResult&lt;/code&gt; either hands back the
stream or raises an error that names what went wrong and which input field caused it, so a
validation failure is something you can match on rather than a field that is quietly missing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-same-runtime-in-every-language&quot;&gt;The same runtime in every language&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Around the generated code, each SDK has a small hand-written runtime that does the same jobs:
retrying transient failures with backoff, raising errors with the same names in every language,
iterating over the API’s list shapes, and checking the server’s version before the first call so
an operation the server does not know yet fails early instead of being sent. A mutation that may
already have run is not retried behind your back.&lt;/p&gt;
&lt;p&gt;Three helpers cover work that is not GraphQL. The upload helper sends a large file in parts with
bounded concurrency and cleans up if anything fails. The playback token helper signs viewer tokens
for content protected by a JWT playback policy. Webhook verification wraps the official Standard
Webhooks library for each language, so the SDK carries no signature code of its own, and an event
type the SDK does not know yet comes back as a raw event rather than an error. A new event type
on the server never breaks an older SDK.&lt;/p&gt;
&lt;p&gt;Two gateway changes shipped with the SDKs so they could rely on them. Errors from backend services
now carry a machine-readable code, and a subscription with an expired token is closed before it
starts instead of continuing as an anonymous connection. The SDKs report that as an authentication
error at once.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;agreement-between-sdks-is-tested-not-assumed&quot;&gt;Agreement between SDKs is tested, not assumed&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Three runtimes written by hand drift apart unless something checks them. The repository has a
directory of language-neutral JSON fixtures covering retries, error shapes, pagination, version
checks, subscriptions, uploads, and webhooks, including the Standard Webhooks reference vector.
Each SDK’s test suite loads the same files, so a change that makes one language behave differently
fails CI.&lt;/p&gt;
&lt;p&gt;For playback tokens, agreement between the SDKs was not enough. Three SDKs can agree with a fixture
and all be wrong about what the edge accepts. So each SDK’s token for a shared test key is recorded,
and a Go test runs all three through the same policy evaluation the media edge runs when a viewer
connects. The first run of that test failed, and the fault was in the fixtures themselves: a
token’s not-before time was still in the future. Every SDK had produced exactly what the fixture
asked for, and the edge would have refused it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;versions-and-what-is-not-included&quot;&gt;Versions and what is not included&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The SDKs stay at 0.x until the platform reaches 1.0, and all three always share a version. Each
minor line names the oldest FrameWorks release it supports and supports every stable release from
there on; the 0.3 line needs v0.3.11 or later. Within a line, operations are only added, and a
breaking change starts a new line. On the server side, CI checks every operation of each supported
line against the schema of each supported release, so a schema change that would break a field an
SDK uses fails before it ships.&lt;/p&gt;
&lt;p&gt;There are no SDKs for PHP, Ruby, Elixir, Java, or C#. Those integrations keep using the GraphQL API
directly, with a Standard Webhooks library for signatures.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/builders/sdks&quot;&gt;SDK guide&lt;/a&gt; has install instructions, runnable examples in all three
languages, the full list of error types, and the version table.&lt;/p&gt;</content:encoded><category>announcements</category><category>engineering</category></item><item><title>Webhooks and a Public Event Catalog</title><link>https://logbook.frameworks.network/blog/webhooks-and-public-events/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/webhooks-and-public-events/</guid><description>FrameWorks now publishes a versioned catalog of account events, delivers it live over a GraphQL subscription, and sends it to your HTTPS endpoints as Standard Webhooks-signed requests you can verify with existing libraries.

</description><pubDate>Sat, 19 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Until now, a FrameWorks integration that needed to react to a stream going live or a clip becoming
ready had two options: keep a GraphQL subscription open, or poll. Neither works well for a
serverless function or a backend that restarts during deploys. Webhooks are the missing piece, and
before we could send them we needed a set of events that is safe to hand to a third-party server
and stable enough to write code against.&lt;/p&gt;
&lt;p&gt;Both now exist. Your account’s events are published as a versioned catalog, available live over a
subscription and as signed HTTPS requests to endpoints you register.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;an-event-is-a-promise-about-something-that-already-happened&quot;&gt;An event is a promise about something that already happened&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/builders/events&quot;&gt;event catalog&lt;/a&gt; covers streams going live or idle, stream keys being
rotated, clips, recordings and uploads becoming ready or failing, multistream target status,
API tokens, billing, account suspension, and custom domains. Each type has a protobuf message in a
versioned public package, and that message is the payload.&lt;/p&gt;
&lt;p&gt;Two rules make the catalog safe to build on. First, an event is written in the same database
transaction as the change it describes. A &lt;code dir=&quot;auto&quot;&gt;billing.invoice_paid&lt;/code&gt; event exists only if the invoice
really is paid; there is no window where your backend hears about a change that later rolled back.
Second, a payload carries only identifiers and values your account already sees through the API.
It does not carry internal stream names, node IDs, storage paths, IP addresses, or raw error text;
a failed clip reports a coarse reason rather than a stack trace.&lt;/p&gt;
&lt;p&gt;Within the &lt;code dir=&quot;auto&quot;&gt;v1&lt;/code&gt; package fields are only added. A breaking change ships as a &lt;code dir=&quot;auto&quot;&gt;v2&lt;/code&gt; message, and each
webhook endpoint stays on the version it was created with, so a receiver you wrote today keeps
parsing what it receives.&lt;/p&gt;
&lt;p&gt;The same events are available over the &lt;code dir=&quot;auto&quot;&gt;tenantEvents&lt;/code&gt; GraphQL subscription, filtered by type and
stream. The dashboard’s media library uses it to refresh when an artifact finishes processing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-delivery-looks-like&quot;&gt;What a delivery looks like&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Under &lt;strong&gt;Developer → Webhooks&lt;/strong&gt; you add HTTPS endpoints and pick the event types each one receives.
Each event becomes a &lt;code dir=&quot;auto&quot;&gt;POST&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;webhook-id: 0199a3c2-7e41-7d2a-9b3e-5f8c1d2e4a67&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;webhook-timestamp: 1789826608&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;webhook-signature: v1,NSZG8yLlem8faHh7EcOnDIbFIp4Wa/3hhM320Atl5LM=&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&quot;id&quot;:&quot;0199a3c2-7e41-7d2a-9b3e-5f8c1d2e4a67&quot;,&quot;type&quot;:&quot;clip.ready&quot;,&quot;api_version&quot;:&quot;v1&quot;,&quot;created_at&quot;:&quot;2026-09-19T14:03:27.412Z&quot;,&quot;data&quot;:{&quot;artifact&quot;:{&quot;artifactId&quot;:&quot;c7f3a9e2b41d4f6a8e0b5d2c9a1f7e34&quot;,&quot;kind&quot;:&quot;ARTIFACT_KIND_CLIP&quot;,&quot;streamId&quot;:&quot;5b0e6a2c-3f1d-4c8e-9a7b-2d4f6e8a0c13&quot;,&quot;playbackId&quot;:&quot;k3v9x2m7q4w8&quot;},&quot;durationMs&quot;:&quot;30000&quot;,&quot;sizeBytes&quot;:&quot;7340032&quot;}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That is a real request body and signature, produced by the delivery code with the example secret
&lt;code dir=&quot;auto&quot;&gt;whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw&lt;/code&gt;. The &lt;code dir=&quot;auto&quot;&gt;data&lt;/code&gt; object uses the protobuf JSON mapping, so
64-bit values such as &lt;code dir=&quot;auto&quot;&gt;sizeBytes&lt;/code&gt; are strings.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-we-did-not-invent-a-signature-scheme&quot;&gt;Why we did not invent a signature scheme&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The signature follows the &lt;a href=&quot;https://www.standardwebhooks.com/&quot;&gt;Standard Webhooks&lt;/a&gt; specification:
HMAC-SHA256 over &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;webhook-id&gt;.&amp;#x3C;webhook-timestamp&gt;.&amp;#x3C;body&gt;&lt;/code&gt;. We chose it over a scheme of our own
because verification libraries already exist for most languages. In Node:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Webhook } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;standardwebhooks&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;event&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Webhook&lt;/span&gt;&lt;span&gt;(process&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;FRAMEWORKS_WEBHOOK_SECRET&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;verify&lt;/span&gt;&lt;span&gt;(rawBody&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;headers);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;We checked the example above against the official TypeScript, Go, and Python libraries; all three
accept it. The timestamp is part of the signed content, so a captured request cannot be replayed
against you days later.&lt;/p&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;webhook-id&lt;/code&gt; is the event ID, and it stays the same across every attempt, every replay, and
every endpoint. That is the one field a receiver needs to handle duplicates: store the IDs you have
processed and ignore a repeat. Failed deliveries are retried for about three days, every attempt is
logged for 30 days, and you can replay one delivery or a whole time range after an outage on your
side; the &lt;a href=&quot;https://logbook.frameworks.network/builders/webhooks&quot;&gt;webhooks guide&lt;/a&gt; has the schedule and the replay calls.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-slow-receiver-only-delays-itself&quot;&gt;A slow receiver only delays itself&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Delivery runs apart from the systems that validate stream keys and serve playback. A customer
endpoint that hangs or returns errors delays its own deliveries and nobody else’s.&lt;/p&gt;
&lt;p&gt;Endpoint URLs must resolve to public addresses, and that check runs again on every connection. A DNS
record changed to point at an internal address after the endpoint was created is refused at send
time, so a webhook cannot be turned into a way to reach our private network.&lt;/p&gt;
&lt;p&gt;The limit worth knowing up front: an endpoint receives events from the moment it is created, with
no backfill, and events carry no ordering guarantee across retries. Design the receiver to look up
current state when order matters.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/builders/webhooks&quot;&gt;webhooks guide&lt;/a&gt; covers endpoints, verification in TypeScript, Go, and
Python, retries, auto-disable, secret rotation, and replay. The &lt;a href=&quot;https://logbook.frameworks.network/builders/events&quot;&gt;events reference&lt;/a&gt;
lists every event type, when it fires, and its payload fields. Agents connected over
&lt;a href=&quot;https://logbook.frameworks.network/agents/mcp&quot;&gt;MCP&lt;/a&gt; can list, test, and replay deliveries with an explicit confirmation.&lt;/p&gt;</content:encoded><category>announcements</category><category>engineering</category><category>architecture</category></item><item><title>Incidents: One Failure, One Thing to Work On</title><link>https://logbook.frameworks.network/blog/lookout-incidents/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/lookout-incidents/</guid><description>Platform alerts now become incidents: one record per failure with a timeline, acknowledgement, assignment and live updates, visible to the tenant who owns the cluster and reachable through the dashboard, GraphQL and MCP.

</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A server in one of your clusters loses power. It was running five services, and every one of them stops answering health checks at the same moment. Each produces its own “service unreachable” alert. Handled as plain notifications, that is five near-identical messages at once, the same messages again every few hours while the machine stays down, and no shared place to record that someone is already on it.&lt;/p&gt;
&lt;p&gt;FrameWorks now turns alerts into incidents. That power loss becomes one incident for the cluster, listing every service that went dark, with a timeline, an owner and a status that stays correct until the last alert clears.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-node-failure-looks-like-now&quot;&gt;What the node failure looks like now&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The failing services still fire individually, and Alertmanager still does what it is good at: evaluating rules, grouping related alerts, applying silences, and retrying delivery. Alerts of the same kind in the same cluster and region arrive as one group, and each group becomes one incident.&lt;/p&gt;
&lt;p&gt;Alertmanager waits briefly for related alerts, then sends the five “service unreachable” alerts as one group. One incident opens with five firing alerts. When Alertmanager repeats the group a few hours later because the machine is still down, the existing incident is updated rather than a new one opened. An on-call engineer acknowledges it, which records who took it without silencing anything; if a sixth service fails, the incident still shows it. Notes and assignment build a timeline as the work happens. When the server comes back and every alert in the group resolves, the incident resolves itself.&lt;/p&gt;
&lt;p&gt;If someone resolves an incident by hand, repeats of the same alerts do not reopen it. A genuinely new alert opens a new incident. That keeps a closed incident closed without hiding a second failure.&lt;/p&gt;
&lt;p&gt;Every state change is pushed through a live subscription, so the dashboard updates without polling. After a reconnect, the incident itself is the durable record to read, not the stream of changes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;incidents-belong-to-whoever-owns-the-cluster&quot;&gt;Incidents belong to whoever owns the cluster&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A tenant running their own cluster should see incidents about their infrastructure and nobody else’s. So each incident is scoped to the tenant that owns the cluster the alert came from, and a tenant can only read or change incidents on clusters they own. Asking for another tenant’s incident returns the same answer as asking for one that does not exist.&lt;/p&gt;
&lt;p&gt;Ownership can change while alerts are arriving, and the system that records ownership can itself be part of the outage. The rule for that case is simple: when ownership cannot be confirmed, the incident stays visible to platform operators only. A tenant might see their incident late, but never someone else’s, and operators still see the outage while the control plane is down.&lt;/p&gt;
&lt;p&gt;Resolved incidents stay with the tenant that owned the cluster when they happened. Moving a cluster to a new owner does not hand over its history.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;delivery-keeps-working-during-the-failure-it-reports&quot;&gt;Delivery keeps working during the failure it reports&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;An incident system is most needed when things are broken, which is also when it is most likely to be affected. We designed delivery around that.&lt;/p&gt;
&lt;p&gt;Platform incidents notify operators by email, Slack and Discord. Tenant incidents feed the tenant’s live dashboard and Skipper. An incident and the obligation to announce it are saved together, so a notification service that is briefly down delays the message and retries it later rather than losing it. When more than one copy of the incident service is running, each delivery is claimed by one copy at a time, so a restart or a failover does not leave a message unsent.&lt;/p&gt;
&lt;p&gt;Critical alerts also have a second path that bypasses the incident system entirely and goes straight to email. If the incident service is the thing that failed, the page about it still goes out. And an alert that fires permanently is sent to an external heartbeat monitor once a minute; if the alerting pipeline itself stops, the heartbeat goes quiet and the external monitor raises the alarm.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-same-workflow-for-people-and-agents&quot;&gt;The same workflow for people and agents&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Tenants work incidents under &lt;strong&gt;Infrastructure → Incidents&lt;/strong&gt;. Platform operators have a cross-tenant view under &lt;strong&gt;Platform Admin → Incidents&lt;/strong&gt;. The same actions are available through GraphQL and the MCP tools: list, inspect, acknowledge, assign, resolve and add a note.&lt;/p&gt;
&lt;p&gt;Skipper reads the tenant incident feed and can attach an investigation report to the timeline. It never acknowledges or resolves anything on its own; analysis and state changes stay separate, and a person decides when an incident is over.&lt;/p&gt;
&lt;p&gt;Which alert rules exist is still an operator decision, and incidents do not merge alerts of different kinds into one record: a failed server that also trips a second, different alert rule produces two incidents, one per rule.&lt;/p&gt;
&lt;p&gt;Start with the &lt;a href=&quot;https://logbook.frameworks.network/builders/incidents&quot;&gt;incidents API guide&lt;/a&gt;. Operators deploying incidents should read &lt;a href=&quot;https://logbook.frameworks.network/operators/alerting&quot;&gt;Alerting and Incidents&lt;/a&gt;, which covers notification channels, the heartbeat and an end-to-end delivery test.&lt;/p&gt;</content:encoded><category>announcements</category><category>operations</category><category>observability</category></item><item><title>Your Stream, Your Edge: Media Placement Policy</title><link>https://logbook.frameworks.network/blog/your-stream-your-edge/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/your-stream-your-edge/</guid><description>Where a request is answered, where a stream originates, and where a viewer receives it are three different places. Placement policy lets you state which capacity your streams may use and in what order, such as your own servers first, and enforces it where media starts.

</description><pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When a broadcaster goes live in Europe and someone opens the stream in the United States, that viewer should be served from a healthy US edge. Until this release, the answer depended partly on which part of the platform happened to receive the viewer’s request: arrive at a European coordinator, and “nearby” was measured from Europe.&lt;/p&gt;
&lt;p&gt;Media placement treats three locations separately: where a request is answered, where a stream originates, and where a viewer receives it. The viewer’s distance is measured from the viewer, and accounts can now state their own rules about which capacity their streams may use.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;nearby-means-near-the-viewer&quot;&gt;Nearby means near the viewer&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://logbook.frameworks.network/blog/federation-went-live&quot;&gt;Federation&lt;/a&gt; gave clusters a way to find streams and source them from one another. Placement decides where that capability should be used.&lt;/p&gt;
&lt;p&gt;Every eligible edge is compared on the same facts: health, usable capacity, distance to the viewer, and whether the stream is already there or needs a source connection. A cluster boundary is not a distance penalty. An edge in Virginia does not become less attractive to a viewer in Virginia because the request reached a coordinator in the EU.&lt;/p&gt;
&lt;p&gt;When the chosen edge does not have the stream yet, it is pulled there. The first viewer may pay some setup latency; later viewers reuse the copy. If the chosen edge cannot accept the work, the next permitted candidate is tried, rather than whatever node sits next to the coordinator that answered.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;my-own-servers-first&quot;&gt;My own servers first&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Nearby is only part of the answer. Some accounts run their own edge servers and want the platform to treat them as the first choice, not one option among many.&lt;/p&gt;
&lt;p&gt;Take a broadcaster who operates two edge servers in their own facility in Frankfurt and also has access to platform capacity. Their account rule says: serve viewers from my servers first, and use platform capacity only when mine are full or the viewer is too far away.&lt;/p&gt;
&lt;p&gt;A viewer in Berlin is served from the broadcaster’s own servers, as they would expect. A viewer in Chicago is beyond the distance threshold the broadcaster configured, so the rule permits spillover and that viewer is served from a platform edge in the US, which pulls the stream across once. During a large event the Frankfurt servers fill up, and new European viewers spill onto platform capacity while existing sessions stay where they are.&lt;/p&gt;
&lt;p&gt;The preview for that rule shows which capacity groups qualify, why a candidate was excluded, and which condition would permit spillover. It is careful about evidence: a self-hosted cluster that cannot be reached is not treated as proof that it is full, and a missing price is not treated as free.&lt;/p&gt;
&lt;p&gt;The same rule could be stricter. “Prefer” and “never” mean different things: a preference can fall back, a prohibition cannot. If an account says never to use platform capacity and its own servers have no room, the stream is reported unavailable rather than moved onto paid capacity. Ingest and serving have separate rules, because the best place for a broadcaster to publish is not necessarily the best place for every viewer to watch. Account defaults cover the ordinary case; stream-specific rules can narrow them.&lt;/p&gt;
&lt;p&gt;The choice is two-sided: a cluster owner can offer viewer capacity without accepting publishers, and your preference for a cluster does not grant you access to it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;enforced-where-media-actually-starts&quot;&gt;Enforced where media actually starts&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Placement is enforced at the two points where media starts: when a publisher connects and when a viewer connects.&lt;/p&gt;
&lt;p&gt;For publishers, the policy is checked against the protocol the edge actually observed accepting the connection, never against the URL the encoder sent, because the encoder controls that URL and RTMP lets a client label a connection however it likes. A publisher that lands on a node the policy forbids is refused, and nothing is held for a session that never starts.&lt;/p&gt;
&lt;p&gt;For viewers, the same policy that chose the destination admits the connection at the edge. A denied HLS request gets a complete refusal and a closed connection instead of a response that never finishes, so players fail over instead of stalling.&lt;/p&gt;
&lt;p&gt;The rules reach the edge as signed authority, which matters during outages. A control-plane outage does not turn a deny into an allow. A publisher who is already live stays live, and edges keep enforcing the last rules they received. During an outage, availability can be limited, but policy is never widened.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-does-not-cover-yet&quot;&gt;What it does not cover yet&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Saving a rule is not the same as every connection following it. A new rule takes effect once every cell that holds your content confirms it enforces placement, and the rollout status shows where that stands. A publisher who is already live stays on their current ingest cluster; changing the rule does not migrate them.&lt;/p&gt;
&lt;p&gt;This release covers ingest and viewer delivery. Where recordings are stored and where processing jobs run are separate decisions on the roadmap, and viewer-serving rules do not control them today.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/builders/media-placement&quot;&gt;media placement guide&lt;/a&gt; covers writing, reviewing and previewing rules, including the self-hosted-first example in the API Explorer demo. Cluster owners should read &lt;a href=&quot;https://logbook.frameworks.network/operators/media-placement&quot;&gt;media placement controls&lt;/a&gt; for capacity permissions and rollout status.&lt;/p&gt;</content:encoded><category>engineering</category><category>architecture</category><category>federation</category><category>releases</category></item><item><title>FrameWorks 0.3.0</title><link>https://logbook.frameworks.network/blog/frameworks-0-3-0/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/frameworks-0-3-0/</guid><description>FrameWorks 0.3.0 adds platform admin, analytics, playback that survives control-plane blips, media placement policies, safer agent workflows, and the real-time foundation for chat and conferencing.

</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;FrameWorks 0.3.0 is out. Until this release, running a FrameWorks platform meant answering many everyday questions from private dashboards and a database console. 0.3.0 gives operators and tenants a supported view of what is happening, keeps viewers watching through control-plane blips, lets you say where your streams may run, and puts guardrails around what AI agents can do. Operators upgrading self-hosted edges should read the note at the end first.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;operators-and-tenants-can-see-what-is-happening&quot;&gt;Operators and tenants can see what is happening&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Platform admin is the most direct change. A cross-tenant admin API and admin console give operators one supported place to inspect tenants, clusters, billing, and content. Access to it is an explicit grant, and every use lands in an audit log. That matters for hosted operations and for anyone running more than one cluster.&lt;/p&gt;
&lt;p&gt;Analytics is moving out of the internal-only bucket. Playback quality of experience, tenant activity, operator activity, stream monitoring controls, and a consolidated set of dashboard charts put the platform’s state in front of the people who can act on it. The goal is answering normal operational questions without opening three private dashboards and a SQL console. For programmatic access, see the &lt;a href=&quot;https://logbook.frameworks.network/builders/analytics-api&quot;&gt;analytics API&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;playback-and-placement-hold-up-under-failure&quot;&gt;Playback and placement hold up under failure&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Viewers who are already watching keep watching when the control plane is briefly unreachable. New viewers are admitted from signed answers the media cluster already holds, so a short central outage does not turn into refused playback. One viewer’s approval is never reused for another: an earlier yes says nothing about the new viewer’s location, their permission, or a policy that changed since. &lt;a href=&quot;https://logbook.frameworks.network/blog/live-playback-control-blips&quot;&gt;Viewers keep watching when the control plane blips&lt;/a&gt; explains the rule and its limits. Alongside it, clusters tell each other which streams they can serve, so a viewer can be served through a peer cluster, and native edge installs on Linux and macOS are smoother.&lt;/p&gt;
&lt;p&gt;Media placement is the other routing change. Where a request is answered, where a stream originates, and where a viewer receives it are three different places, and 0.3.0 stops treating a cluster boundary as a distance penalty. On top of that sits a placement policy per account and per stream: prefer your own capacity, spill over only when you run out of capacity or have no coverage in a region, never use platform-official capacity, or rank marketplace capacity by effective price. A policy only takes effect in a cluster that has confirmed it enforces it. &lt;a href=&quot;https://logbook.frameworks.network/blog/your-stream-your-edge&quot;&gt;Your stream, your edge&lt;/a&gt; tells the full story, and the &lt;a href=&quot;https://logbook.frameworks.network/builders/media-placement&quot;&gt;media placement docs&lt;/a&gt; show how to preview a policy. Storage and processing placement are not in this release.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;agents-can-act-with-guardrails&quot;&gt;Agents can act, with guardrails&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Skipper and MCP ship in 0.3.0 because AI agents are becoming part of how people operate streams. The useful work is diagnosis from real stream state, schema-aware API help, a billing preflight before expensive actions, confirmation before anything changes, and labels that say whether an answer came from docs, schema, metrics, or model inference. Billing changes and private-infrastructure tools now require an owner or admin identity as well as the matching API token scope, so an agent linked to a regular member account no longer sees those tools at all. The &lt;a href=&quot;https://logbook.frameworks.network/agents/mcp&quot;&gt;MCP docs&lt;/a&gt; cover connecting an agent.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-is-not-in-030&quot;&gt;What is not in 0.3.0&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Chat and conferencing are next, because FrameWorks already has pieces of the real-time stack: support conversations, message lifecycle events, live subscriptions, and Skipper conversations. Conferencing still needs room state, participant identity, moderation, recording rules, pricing, and media routing built around active participants. 0.3.0 tightens the real-time and operator foundations that work depends on.&lt;/p&gt;
&lt;p&gt;Composer stays on the roadmap. Overlays, lower thirds, server-side ad insertion, transcription, object detection, picture-in-picture, and automated clipping are among the more interesting media features we could build, and each carries a product contract: API shape, placement, lifecycle, billing meters, permissions, dashboard controls, and MCP access. It ships once those contracts are defined.&lt;/p&gt;
&lt;p&gt;Bring-your-own-compute also gets a clearer boundary. You can run your own edge nodes with MistServer, and FrameWorks keeps running routing, federation, transcoding, storage, DNS, analytics, billing, and the control plane. There is no deeper tier where a customer runs part of that coupled stack. Teams that want all of it self-host the entire stack as an independent platform.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;operator-note-self-hosted-edges-must-be-redeployed&quot;&gt;Operator note: self-hosted edges must be redeployed&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This release raises the minimum protocol version between edges and the control plane, with no window where old and new versions work together. The control plane upgrades first. From that moment, edges on the older protocol are refused, drop out of routing, and cannot receive the automatic update that would fix them.&lt;/p&gt;
&lt;p&gt;If you run edge nodes yourself, redeploy them with the 0.3.0 edge image after the control plane rollout. The same applies to an older edge that was offline during the rollout and comes back later: it cannot reconnect until it has been updated. This one-time interruption was a deliberate trade: an edge that cannot show it speaks the new protocol is refused rather than trusted. The &lt;a href=&quot;https://logbook.frameworks.network/operators/running-upgrades#edge-nodes&quot;&gt;upgrade guide&lt;/a&gt; has the redeploy command, and the &lt;a href=&quot;https://logbook.frameworks.network/operators/selfhosted-edge-rollout&quot;&gt;self-hosted edge rollout runbook&lt;/a&gt; covers release order and recovery.&lt;/p&gt;</content:encoded><category>announcements</category><category>releases</category><category>roadmap</category></item><item><title>Viewers Keep Watching When the Control Plane Blips</title><link>https://logbook.frameworks.network/blog/live-playback-control-blips/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/live-playback-control-blips/</guid><description>A brief control-plane outage no longer decides who can watch. Viewers who are already watching keep watching, new viewers are admitted from signed answers the media cluster already holds, and one viewer&apos;s approval is never reused for another.

</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Picture a stream in the middle of a broadcast. The encoder is pushing, the edge nodes are serving video, and people are watching. Then the control plane becomes unreachable for a few seconds: a deploy, a database failover, a network partition between regions. Nothing about the media changed. What matters is what happens to the people already watching, and to the people who press play during those seconds.&lt;/p&gt;
&lt;p&gt;The rule FrameWorks follows: viewers who are already watching keep watching when the control plane is briefly unreachable. New viewers are admitted from a signed, durable answer the media cluster already holds. If no valid answer exists, the new viewer is refused cleanly. A missing answer is never treated as permission.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-failure-to-answer-is-not-an-answer&quot;&gt;A failure to answer is not an answer&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every new playback request on an edge node goes through a hook: before MistServer starts serving, it asks the FrameWorks edge agent what the requested playback ID resolves to and whether this viewer may have it. The agent asks the cluster’s regional load balancer, which holds the policy.&lt;/p&gt;
&lt;p&gt;The weak spot was what happened when that question could not be answered. MistServer’s hook interface had a single fallback value for “the handler failed”, and for playback that fallback was a string that read as a stream name. It could not tell an explicit denial apart from a handler that never replied, and different call sites interpreted the same fallback differently. A control-plane problem could reach the viewer as an arbitrary answer rather than as a failure.&lt;/p&gt;
&lt;p&gt;Our MistServer fork now accepts an explicit action for each hook. For playback, a handler that cannot answer produces a denial, and an authoritative rejection produces the same denial. No sentinel value is involved. The edge agent also keeps the hook’s definition repaired continuously, because a hook that has gone missing skips the decision entirely instead of failing it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;each-media-cluster-holds-its-own-signed-answers&quot;&gt;Each media cluster holds its own signed answers&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Refusing new viewers is still a poor outcome when the media cluster already knows the right answer. So the control plane distributes signed, versioned authority for tenants and media to every cluster that serves them. The cluster verifies each update and commits it to its own database before acknowledging it.&lt;/p&gt;
&lt;p&gt;A decision past its soft expiry keeps serving while the cluster requests a refresh. Hard expiry, a signed revocation, or a failed signature check never turns into an allow. Because the answers live in the cluster’s database, restarting the regional load balancer does not force a round trip to the central control plane before viewers can be admitted again.&lt;/p&gt;
&lt;p&gt;The same signed authority covers ingest ownership, pull sources, processing policy, multistream targets, and restart configuration. Changes and revocations reach each cluster independently. What still needs a live control plane is new management work, such as creating a stream, and new x402 settlement. Media that is already authorized does not.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-an-earlier-viewers-approval-is-not-reused&quot;&gt;Why an earlier viewer’s approval is not reused&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;There is a tempting shortcut: when the regional load balancer is unreachable, remember the last successful answer for a playback ID and hand it to the next viewer who asks for the same one. We do not do this.&lt;/p&gt;
&lt;p&gt;An approval for an earlier viewer says nothing about the new viewer’s location, about whether the destination that viewer would be sent to still permits them, or about a tenant policy that changed since. Consider a geofenced event, or a signing key that was just revoked. Replaying an earlier yes in those cases would admit exactly the viewers the policy exists to stop. Every new viewer gets a fresh decision from the regional load balancer, based on the signed authority it holds.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-still-fails&quot;&gt;What still fails&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Signed local authority protects a media cluster from outages further upstream. Losing every reachable regional load balancer for a cluster is a different failure. New requests then have to reconnect or fail over before they can get a decision, and if no valid answer exists, MistServer applies the explicit denial.&lt;/p&gt;
&lt;p&gt;That affects the admission of new viewers only. Playback that is already established is not terminated because the control plane went quiet. The honest boundary is expiry: stored authority serves through its signed validity window, and never beyond it.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/operators/architecture#multi-cluster-topology&quot;&gt;architecture overview&lt;/a&gt; describes how signed authority reaches each cluster, and &lt;a href=&quot;https://logbook.frameworks.network/builders/playback-access-control&quot;&gt;playback access control&lt;/a&gt; covers the policies it carries.&lt;/p&gt;</content:encoded><category>architecture</category><category>playback</category><category>edge</category></item><item><title>DVR, Clips, and VOD: What Happens After the Live Edge</title><link>https://logbook.frameworks.network/blog/dvr-clips-vod-pipeline/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/dvr-clips-vod-pipeline/</guid><description>A live stream is a ring buffer on an edge node. A channel that streams for 90 days straight is still one recording, replayable in chapters, and a plan change halfway through never shortens an archive that is already recording.

</description><pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A live stream, at the edge, is a ring buffer. MistServer keeps a bounded window of segments on disk, old ones fall off the back, and when the stream ends the buffer eventually stops mattering. Everything that outlives the broadcast (the seekable archive, the highlight clip, the replay a viewer opens next week) has to be copied out of that buffer before it disappears.&lt;/p&gt;
&lt;p&gt;Short streams make that easy to get roughly right. Long ones do not. This post follows one always-on channel through three months of recording, a plan change and a highlight clip.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-channel-that-never-stops-is-one-recording&quot;&gt;A channel that never stops is one recording&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Picture a 24/7 channel, such as a venue camera that nobody turns off. It goes live on the first of the month and stays live for 90 days.&lt;/p&gt;
&lt;p&gt;On FrameWorks that is one recording. A session produces one DVR artifact with no per-artifact length cap, so a channel that streams for 90 days straight is one artifact. If the stream drops and comes back an hour later, that is a new session and a new recording.&lt;/p&gt;
&lt;p&gt;We got there by separating three clocks that look related but are not:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;live DVR window&lt;/strong&gt; is how far back a viewer can scrub while the stream is up. It is sized by plan and kept short so live playlists stay small.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;session&lt;/strong&gt; is the recording itself, as long as the ingest stays live.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retention&lt;/strong&gt; is how long the archive survives after the stream ends, counted from the moment the session finishes. An active recording is invisible to retention: a channel on day 89 cannot lose day 1 to an expiry timer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Underneath, every recorded segment is tracked with its position in stream time and where it is stored. Playlists are generated from that record when something needs one, so there are no manifest files in object storage that can drift out of step with the media they describe.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;replaying-three-months-without-downloading-three-months&quot;&gt;Replaying three months without downloading three months&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Nobody wants to open a 90-day file. So replay works through chapters: bounded slices of the recording, each finalized into its own ordinary VOD asset. Chapters can match the length of the live window or follow fixed UTC intervals such as hourly or daily, so an application that wants “yesterday afternoon” converts that to a UTC range and lists the chapters in it. Each chapter keeps wall-clock timing, so a player can show the real time of day for any position.&lt;/p&gt;
&lt;p&gt;A chapter becomes playable as soon as it is finalized, and is later uploaded to object storage, after which the temporary per-segment copies are removed. If a chapter cannot be built because its source segments were lost, it is marked failed rather than published as a shorter file that pretends to be complete.&lt;/p&gt;
&lt;p&gt;Because chapters are ordinary VOD, they follow the same playback path, the same access policy and the same rules across clusters as any uploaded video. A remote cluster never asks for “the 90-day archive”; it asks for a chapter. If that chapter is already in object storage, the viewer gets a signed download from storage. If it is not uploaded yet but the node that recorded it still has the complete file, it is served from there. If neither is true yet, the request is refused with a retryable answer, and a later attempt succeeds once the upload finishes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-plan-change-does-to-an-archive-in-progress&quot;&gt;What a plan change does to an archive in progress&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;On day 40, the channel’s owner changes plan. The obvious question is what happens to the recording that has been running for 40 days.&lt;/p&gt;
&lt;p&gt;Nothing, until it ends. The retention that applies to a recording is resolved when the recording starts and stored with it. A downgrade halfway through never quietly shortens an archive that is already recording, and the plan in force when the stream finally stops is never looked up again to recompute it. The new plan applies from the next session.&lt;/p&gt;
&lt;p&gt;Re-resolving retention at the end would let a plan change reach back and shorten weeks of footage someone was counting on, so we chose to resolve it once, at the start. If you want a particular archive kept longer, you can set a retention override on that finalized asset afterwards, within your plan’s cap.&lt;/p&gt;
&lt;p&gt;The same rule applies to chapter settings: changing how a stream cuts chapters takes effect from the next broadcast, and a recording in progress keeps the setting it started with.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;clips-come-from-the-same-place&quot;&gt;Clips come from the same place&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;On day 63 something worth sharing happens. A clip can be cut from the live buffer, from the rolling DVR window, or from a chapter that has already been finalized. It goes through the same processing path as chapters and produces the same kind of VOD asset. A clip that fails is marked failed and shows up that way in the dashboard, instead of a spinner that never resolves.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-costs&quot;&gt;What it costs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A long recording is only as durable as its path to object storage. Edges evict local segments under disk pressure, oldest first, because the durable copy lives in object storage. Every cleanup step deletes only after that durable copy is confirmed, so a crash in the middle of cleanup leaves extra data behind, never missing data.&lt;/p&gt;
&lt;p&gt;The live window is also not the archive. A viewer who joins on day 90 can scrub back only as far as the live window allows; everything older is reached through chapters.&lt;/p&gt;
&lt;p&gt;Docs: &lt;a href=&quot;https://logbook.frameworks.network/builders/dvr-chapters&quot;&gt;DVR chapters&lt;/a&gt;, &lt;a href=&quot;https://logbook.frameworks.network/builders/clips&quot;&gt;clips&lt;/a&gt;, &lt;a href=&quot;https://logbook.frameworks.network/builders/recordings&quot;&gt;recordings&lt;/a&gt;, &lt;a href=&quot;https://logbook.frameworks.network/builders/storage-and-retention&quot;&gt;storage and retention&lt;/a&gt;, and the operator’s view in &lt;a href=&quot;https://logbook.frameworks.network/operators/dvr&quot;&gt;DVR operations&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>engineering</category><category>architecture</category><category>dvr</category></item><item><title>Going Multi-Region: Local Latency, One Bill</title><link>https://logbook.frameworks.network/blog/going-multi-region/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/going-multi-region/</guid><description>FrameWorks now runs an EU and a US region. Viewers and broadcasters are handled entirely in their own region, while analytics and billing converge in one place, so a tenant gets one bill that counts every region exactly once.

</description><pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A viewer in Virginia opens a stream. In a single-region platform hosted in Europe, the routing decision, the session bookkeeping and the event traffic for that viewer all cross the Atlantic before the first frame arrives.&lt;/p&gt;
&lt;p&gt;FrameWorks now runs two regions, eu-west and us-east. That viewer in Virginia gets a US edge, a US routing decision, and a US event pipeline absorbing the telemetry their session produces. The tenant who owns the stream still gets one dashboard and one bill covering both regions. Most of the design work went into keeping that second sentence true.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;nothing-on-the-viewers-path-crosses-the-ocean&quot;&gt;Nothing on the viewer’s path crosses the ocean&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Each region is a complete media cell. It has its own edge nodes, its own routing and load balancing, its own transcoding capacity, its own cache, its own realtime layer and its own Kafka cluster. A broadcaster pushing into us-east and a viewer watching from us-east never wait on a request to Europe: stream startup, routing and playback admission are all answered inside the region.&lt;/p&gt;
&lt;p&gt;What stays central is the state that has to be single: the control plane (accounts, provisioning, billing, DNS), the database behind it, and ClickHouse for analytics. None of those sit on the path between a viewer pressing play and video arriving.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-events-converge-in-one-region&quot;&gt;Why events converge in one region&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The media is regional, but the questions a tenant asks are not. “How many people watched last week?” and “what do I owe?” need every region’s data in one place.&lt;/p&gt;
&lt;p&gt;We use Kafka’s MirrorMaker 2 to copy each region’s durable event streams (analytics, service events, billing usage reports and the dead-letter queue) into a single aggregator cluster in the EU, where ClickHouse and billing already live. A narrower link in the other direction carries only the realtime events that dashboards subscribe to, so a GraphQL subscription stays connected to its nearest region and still sees changes from both. Mirroring durable data toward the consumers that need it was a much smaller commitment than teaching every consumer about every region.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;graph LR
    subgraph &quot;us-east&quot;
        USV[&quot;Viewers and broadcasters&quot;] --&gt; USE[&quot;Edges and routing&quot;]
        USE --&gt; USK[&quot;Regional Kafka&quot;]
    end
    subgraph &quot;eu-west (aggregator)&quot;
        EUK[&quot;Aggregator Kafka&quot;] --&gt; CH[(&quot;Analytics&quot;)]
        EUK --&gt; BILL[&quot;Billing&quot;]
    end
    USK -- &quot;durable + realtime events&quot; --&gt; EUK
    EUK -- &quot;realtime only&quot; --&gt; USK&lt;/pre&gt;
&lt;p&gt;Producers need no knowledge of the topology. A service in us-east writes to its regional pipeline and is done; the mirror moves the event later. The mesh is declared explicitly in our &lt;a href=&quot;https://github.com/Livepeer-FrameWorks/gitops&quot;&gt;public cluster manifest&lt;/a&gt;, and the CLI refuses a manifest with a missing or misplaced link. Two regions make a full mesh cheap. Before a third region, we will measure realtime volume and decide whether broadcasting every realtime event everywhere still makes sense.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;at-least-once-mirrors-exactly-once-bills&quot;&gt;At-least-once mirrors, exactly-once bills&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Mirroring is at-least-once. Regions can partition. A mirror can deliver the same event twice after a restart. Billing sits downstream of all of it, and a tenant should never pay twice for one viewer session.&lt;/p&gt;
&lt;p&gt;Every event carries a unique ID and the region and cluster it came from. Analytics deduplicates on that ID, so a replayed or double-mirrored event collapses into the row that already exists. Events that record a state change, such as billing usage or an artifact being created, are written in the same database transaction as the change itself and shipped afterwards, so an outage in the regional event pipeline delays them without losing them. Loss-tolerant telemetry such as load-balancing samples skips that step and accepts a small loss window.&lt;/p&gt;
&lt;p&gt;The finalized facts billing depends on, like a completed viewer session, needed one more change, and it came from a race we found rather than one we predicted. ClickHouse offers table engines that deduplicate rows in the background, which looks like exactly the right tool. It is not. When a Kafka consumer group rebalances, two parser instances can briefly process the same partition. Combine that with the timing of background merges, and a finalized fact can disappear without any error being raised.&lt;/p&gt;
&lt;p&gt;So those facts are stored append-only, and the current truth is computed when it is read. When two writers disagree about the value of a billed fact, the disagreement is written to an audit table and raised as a metric, and supported cases become explicit corrections on the bill. Nothing is silently overwritten, and every change to what a tenant owes can be traced back to the events that caused it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-costs-and-what-it-does-not-do&quot;&gt;What it costs and what it does not do&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Cross-region dashboard updates arrive a little later than local ones, because they travel over the mirror. Analytics for a US stream reaches ClickHouse after the mirror delivers it, not the instant it happens. Keeping one central control plane means an EU-side control-plane outage is felt in both regions for account and billing operations, although viewers already watching keep watching.&lt;/p&gt;
&lt;p&gt;In return, a tenant sees one bill that covers usage in every region, counted once, and viewers see latency set by their own region. Viewers whose stream lives in the other region are served through &lt;a href=&quot;https://logbook.frameworks.network/blog/federation-went-live&quot;&gt;federation&lt;/a&gt;: a nearby edge pulls the stream across once, and later viewers watch it locally.&lt;/p&gt;
&lt;p&gt;The operator’s view, including how regions and mirror links are declared, is in &lt;a href=&quot;https://logbook.frameworks.network/operators/multi-cluster&quot;&gt;multi-cluster operations&lt;/a&gt; and the &lt;a href=&quot;https://logbook.frameworks.network/operators/cluster-manifest&quot;&gt;cluster manifest reference&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>engineering</category><category>architecture</category><category>kafka</category></item><item><title>An AI Agent Walks Into a Streaming Platform</title><link>https://logbook.frameworks.network/blog/agent-native-access/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/agent-native-access/</guid><description>No signup form, no credit card, no human: an agent signs in with a wallet, gets a payment request when its balance runs out, pays in USDC, and carries on. How FrameWorks treats agents as first-class users.

</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most video platforms assume a person is at the keyboard. Signup wants an email address and a click on a verification link, billing wants a card, and when something goes wrong the answer is a support ticket. An AI agent can do none of those things. It can hold a wallet, sign messages, call an API, and read structured errors.&lt;/p&gt;
&lt;p&gt;FrameWorks lets an agent become a paying customer with only those abilities. It signs in with a wallet, sets up what it needs at zero balance, and pays for usage in USDC when a billable call asks for it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-agents-first-run&quot;&gt;One agent’s first run&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Say an agent has been asked to publish a finished video on FrameWorks. It has never used the platform and has no account.&lt;/p&gt;
&lt;p&gt;It starts with an EVM wallet. It asks FrameWorks for a one-time login message, signs it with the wallet’s key, and sends the signature back. That signature is the whole signup. The first successful sign-in creates an account tied to that wallet, with no email address and a balance of zero.&lt;/p&gt;
&lt;p&gt;At zero balance the agent can already do a lot: read its account status, create API credentials, configure a stream, and look up what the platform offers. None of that costs anything, so none of it needs money.&lt;/p&gt;
&lt;p&gt;Then it asks to create the upload. Processing a video is billable work, and the balance is empty, so the call comes back with HTTP 402 Payment Required and a payment offer bound to this account: the amount, the token, and the networks it can pay on.&lt;/p&gt;
&lt;p&gt;The agent signs a USDC transfer authorization for that amount and submits it as a top-up. FrameWorks relays the transfer, so the agent does not need to hold gas, waits until the chain confirms it, and credits the balance. The agent retries the upload, the usual balance and account checks run again, and the upload is created.&lt;/p&gt;
&lt;p&gt;No step in that run needed a person. The agent never read an email, typed a card number, or waited for support.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-payment-is-part-of-the-api&quot;&gt;Why payment is part of the API&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.x402.org/&quot;&gt;x402&lt;/a&gt; protocol gives the 402 status code a machine-readable body: what to pay, how, and where. An agent that hits a billable call without funds gets a structured answer that names the fix and carries the offer to pay it, so it does not have to parse an error message written for humans.&lt;/p&gt;
&lt;p&gt;We settle on Base and Arbitrum. We skipped Ethereum mainnet, because a few dollars of gas per transaction makes no sense for topping up a streaming balance. Agents, or people, who prefer a plain transfer can use a deposit flow instead: ask for a quote, get a deposit address, and send ETH or USDC to it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-wallet-only-account-can-and-cannot-do&quot;&gt;What a wallet-only account can and cannot do&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A wallet costs nothing to create, so a wallet-only account is always prepaid: it has to fund its balance before it consumes anything billable. That is what stops someone from minting thousands of wallets and running work on credit. Adding and verifying an email address later opens the other billing options.&lt;/p&gt;
&lt;p&gt;Payment and identity stay separate. Paying never signs an agent in, and a payment does not override a suspended account; after the top-up, the retried call goes through the same checks as any other request. Usage is metered and itemized the same way whether the caller is a person or an agent.&lt;/p&gt;
&lt;p&gt;The cost of this model for the agent’s operator is that the wallet is the account. Whoever holds the key controls the account and its balance, so the key belongs somewhere the agent can use it and nothing else can read it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-mcp-surface-for-the-whole-platform&quot;&gt;One MCP surface for the whole platform&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Once signed in, the agent works through the &lt;a href=&quot;https://logbook.frameworks.network/agents/mcp&quot;&gt;MCP server&lt;/a&gt;: tools for streams, clips, DVR, VOD, playback, quality diagnostics, billing, and support, plus read-only resources for account and stream state. A small part of it is public, covering sign-in and playback lookup, because an agent has to be able to find out how to become a customer before it is one.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://logbook.frameworks.network/agents/skipper&quot;&gt;Skipper&lt;/a&gt;, the AI video consultant, is available on the same surface for questions such as why a stream is buffering. It can search documentation and run diagnostics but cannot change anything on that path, so asking it a question never deletes a stream as a side effect. Changes go through the dedicated tools, each with its own authorization. We wrote about Skipper &lt;a href=&quot;https://logbook.frameworks.network/blog/skipper-launch&quot;&gt;when it launched&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/agents/overview&quot;&gt;agents overview&lt;/a&gt; is the place to start. &lt;a href=&quot;https://logbook.frameworks.network/agents/wallet-auth&quot;&gt;Wallet authentication&lt;/a&gt; and &lt;a href=&quot;https://logbook.frameworks.network/agents/payments&quot;&gt;agent payments&lt;/a&gt; cover this post’s flow step by step, including the payment networks, the deposit flow, and the errors an agent should expect.&lt;/p&gt;</content:encoded><category>engineering</category><category>ai</category><category>payments</category></item><item><title>Trusting an Edge Node on Hardware We Don&apos;t Own</title><link>https://logbook.frameworks.network/blog/sovereign-mesh-pki/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/sovereign-mesh-pki/</guid><description>A FrameWorks edge can run on a server we have never seen. How it proves who it is without a shared secret, why it never holds our internal certificate authority, and why we run our own mesh instead of Tailscale.

</description><pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With &lt;a href=&quot;https://logbook.frameworks.network/hybrid/edge-nodes&quot;&gt;bring-your-own-compute&lt;/a&gt;, the machine that delivers your viewers’ video may sit in your rack, a colocation cage, or a rented server we have never logged into. That machine still has to talk to the FrameWorks control plane, and the control plane has to believe it is the edge it claims to be. The obvious shortcuts each carry a risk. A secret baked into every edge image leaks the first time one box is compromised. Handing every edge our internal certificate root turns the anchor for service-to-service traffic into a file anyone with a VM can copy. A hosted mesh coordinator puts our network topology and the power to add peers in someone else’s hands.&lt;/p&gt;
&lt;p&gt;The design we settled on keeps two trust roots apart. Provider services trust each other through a FrameWorks-owned internal CA. Anything that faces the internet, edges included, uses public web PKI. An edge verifies the control plane with a certificate its operating system already trusts, and identifies itself with a token minted for one tenant.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;an-edge-you-dont-own-never-sees-the-internal-root&quot;&gt;An edge you don’t own never sees the internal root&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every TLS listener in FrameWorks belongs to exactly one of two issuers. The internal CA covers gRPC between provider services on the private network. Public ACME certificates cover everything a browser, a viewer, or an edge node touches, including per-tenant custom domains, which get the same issue-and-renew lifecycle.&lt;/p&gt;
&lt;p&gt;The control-plane listener that edges connect to presents a publicly trusted certificate for the cluster. A customer edge therefore needs no FrameWorks CA material at all: it does not receive the provider trust root, does not join the backend mesh, and holds no federation credentials. That listener must never present the internal certificate, so in production the service refuses to start if the public certificate is unavailable, rather than serve the wrong identity on a public port.&lt;/p&gt;
&lt;p&gt;Clients on our side are just as strict. Each one dials with the address to connect to and the identity the server must prove as two separate inputs. A DNS change can send a client somewhere else, but it cannot make the client accept a certificate for a different name.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-an-edge-proves-who-it-is&quot;&gt;How an edge proves who it is&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A new edge presents an enrollment token. The token is minted for one tenant, can be scoped to a specific cluster, and can carry a usage limit for provisioning many nodes at once. Tokens are consumed atomically, so a single-use token that has already been spent cannot be replayed by whoever finds it later. A node that has enrolled before is recognized from its network identity and hashed hardware identifiers and does not need a new token.&lt;/p&gt;
&lt;p&gt;After enrollment, every call from the edge carries a credential that the control plane checks and logs. Authentication rides on top of server-authenticated TLS rather than inside it. The only unauthenticated surfaces are health checks and the enrollment entry point itself, which validates the token before doing anything else.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/hybrid/connecting&quot;&gt;edge connection guide&lt;/a&gt; walks through creating a token and enrolling a node.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-we-did-not-use-tailscale&quot;&gt;Why we did not use Tailscale&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Inside the provider plane, backend nodes talk over a WireGuard mesh. The question was who coordinates it. The mesh coordinator decides which keys count as trusted peers, which makes it part of the trust root: whoever runs it can add a node to the network.&lt;/p&gt;
&lt;p&gt;We looked at Tailscale first. Its coordination plane is a hosted service, so our topology would be visible to a third party, and pricing is per device. Headscale removes the hosted dependency but places an external project at the center of our trust model. Both would have put the decision about who joins our network outside the platform operator’s control.&lt;/p&gt;
&lt;p&gt;So mesh coordination is first-party. A small agent on every backend node builds the WireGuard configuration from three sources: the host’s static identity in the deployment manifest, a set of seed peers for bootstrap, and peers, mesh DNS, and certificates synced from the control plane. It caches the last known mesh locally, so a node that reboots while the control plane is down comes back with the peers it had. Hand-written tunnel configs are not a supported input, so every peer traces back to the manifest or the control plane. The manifest itself is &lt;a href=&quot;https://logbook.frameworks.network/blog/public-gitops&quot;&gt;public&lt;/a&gt;, with host addresses and keys encrypted.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-this-model-does-not-do-yet&quot;&gt;What this model does not do yet&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Authentication is tokens over TLS, not transport mTLS. We have not built per-edge client certificates or SPIFFE-style workload identity; per-edge client certificates are an open design item. What exists today is encrypted transport with a pinned root on the inside, publicly verifiable certificates on the outside, and a credential on every call that can be checked and logged.&lt;/p&gt;
&lt;p&gt;Shared and dedicated provider capacity use the same architecture. Teams that self-host the entire stack run their own CA, mesh, DNS, and control plane as an independent platform with its own trust root; that platform does not join the FrameWorks network.&lt;/p&gt;
&lt;p&gt;Operators can find the setup details in &lt;a href=&quot;https://logbook.frameworks.network/operators/mesh-bootstrap&quot;&gt;mesh networking&lt;/a&gt;, &lt;a href=&quot;https://logbook.frameworks.network/operators/dns&quot;&gt;DNS and certificates&lt;/a&gt;, and the &lt;a href=&quot;https://logbook.frameworks.network/operators/architecture&quot;&gt;architecture overview&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>engineering</category><category>architecture</category><category>security</category></item><item><title>Our Production Topology Is Public on Purpose</title><link>https://logbook.frameworks.network/blog/public-gitops/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/public-gitops/</guid><description>The FrameWorks gitops repo is public: every service, database, and broker placement in plaintext, with only host addresses and credentials encrypted. What we encrypt, what we don&apos;t, and the pre-commit hooks that enforce the boundary.

</description><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Last week we merged our private gitops repo into the public one. The commit message is one line: “merge gitops-private for deployment transparency.” Since then, the &lt;a href=&quot;https://github.com/Livepeer-FrameWorks/gitops&quot;&gt;FrameWorks gitops repo&lt;/a&gt; has contained our actual production deployment: every service, database, message broker, cache, and host-to-role mapping, in plaintext, for anyone to read.&lt;/p&gt;
&lt;p&gt;The position follows from how we operate the product. FrameWorks publishes the topology of its managed provider plane rather than hiding how production is wired. The cluster manifest that drives our deployment is published as-is, and each cluster in it carries an explicit &lt;code dir=&quot;auto&quot;&gt;public_topology: true&lt;/code&gt; flag. The same &lt;code dir=&quot;auto&quot;&gt;frameworks&lt;/code&gt; CLI and GitOps layout are what anyone self-hosting the entire stack uses to deploy and upgrade their own independent platform.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-boundary&quot;&gt;The boundary&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The repo splits into two kinds of files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plaintext:&lt;/strong&gt; the cluster manifest (topology, service placement, database and broker wiring, per-service configuration), the edge-node topology, the non-secret operator environment, and the release manifests with their channel pointers. SOPS-encrypted files also keep their YAML keys and structure visible, so you can see &lt;em&gt;that&lt;/em&gt; there’s a WireGuard private key per host and what the file layout is, just not the value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Encrypted:&lt;/strong&gt; the hosts file (server addresses, SSH targets, WireGuard private keys) and the secrets env files (API keys, passwords, tokens). Everything encrypted uses &lt;a href=&quot;https://github.com/getsops/sops&quot;&gt;SOPS&lt;/a&gt; with &lt;a href=&quot;https://github.com/FiloSottile/age&quot;&gt;age&lt;/a&gt; keys.&lt;/p&gt;
&lt;p&gt;The interesting property is what the plaintext half enables. Anyone evaluating FrameWorks can answer “how is this actually deployed” from the source instead of from a marketing diagram. An operator setting up their own cluster has our production manifest as a working reference for the &lt;a href=&quot;https://logbook.frameworks.network/operators/cluster-manifest&quot;&gt;manifest format&lt;/a&gt;. And our own deployment discussions happen against a file everyone can see.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-guards&quot;&gt;The guards&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A public repo with an encryption boundary is one careless commit away from being a public repo without one. Three pre-commit hooks enforce it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No plaintext secrets.&lt;/strong&gt; Staged secrets files must actually be SOPS-encrypted: env files need the SOPS version marker, encrypted YAML needs its &lt;code dir=&quot;auto&quot;&gt;sops:&lt;/code&gt; metadata block. A decrypted file that someone forgot to re-encrypt fails the commit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No leaked addresses.&lt;/strong&gt; Staged plaintext manifests are grepped for IP addresses that belong in the encrypted hosts file, with only documentation ranges and loopback allowed through.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No age keys.&lt;/strong&gt; Any staged file containing an age secret-key prefix fails the commit, so a private key can’t ride along in a config or a note.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Secret edits go through a small wrapper script that decrypts to a temp file, applies the change, and re-encrypts. Nobody hand-edits encrypted files. Key management is documented in the repo itself: adding a teammate is a keypair plus &lt;code dir=&quot;auto&quot;&gt;sops updatekeys&lt;/code&gt;; a compromised key means removing it &lt;em&gt;and&lt;/em&gt; rotating every value it could decrypt, on the theory that once the ciphertext is public, revoking the key alone protects nothing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-costs&quot;&gt;What it costs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The downside: our infrastructure is easier to case. An attacker can read which services exist, where the trust boundaries are, and what a valuable target would be. We accepted that because most of it is discoverable anyway: port scans, DNS, TLS certificates, and job postings reconstruct a topology diagram slowly but reliably, while the part that gives an attacker traction (addresses, credentials, keys) stays encrypted. What we got in return is a deployment reference that can’t go stale, since the deploy tooling reads the same file you do.&lt;/p&gt;
&lt;p&gt;FrameWorks infrastructure operators start from the &lt;a href=&quot;https://logbook.frameworks.network/operators/deployment&quot;&gt;deployment guide&lt;/a&gt;; customer edge operators should use the &lt;a href=&quot;https://logbook.frameworks.network/hybrid/overview&quot;&gt;hybrid edge guide&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>engineering</category><category>operations</category><category>sovereignty</category></item><item><title>Federation: Serving a Stream From a Cluster That Doesn&apos;t Have It</title><link>https://logbook.frameworks.network/blog/federation-went-live/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/federation-went-live/</guid><description>Clusters can now source live streams from one another. A viewer near a cluster that never received the broadcast is served from a nearby edge after a single server-to-server pull, instead of being sent somewhere worse.

</description><pubDate>Wed, 18 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A FrameWorks stream is pushed into exactly one cluster. Viewers show up wherever they are. For a long time those two facts forced an awkward choice whenever a viewer’s best edge lived in a cluster that had never seen the stream: send the viewer somewhere worse, or copy the stream somewhere it might not be needed.&lt;/p&gt;
&lt;p&gt;As of the v0.1.0 cycle, clusters that serve the same tenant talk to each other directly. Each one knows which live streams its peers hold, and when a viewer needs a stream that lives elsewhere, the nearby cluster pulls it once and serves it locally. The stream is copied only when someone is watching it there.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-viewer-two-clusters&quot;&gt;One viewer, two clusters&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Take a broadcaster in Amsterdam who pushes over RTMP into a cluster in Europe. Their tenant also has access to a second cluster in North America. A viewer in Toronto opens the stream.&lt;/p&gt;
&lt;p&gt;The North American cluster does not have the stream, but it already knows who does. Peer clusters continuously advertise the live streams they hold, so the viewer’s cluster can answer “where is this stream” from its own memory rather than by asking a central service on every play.&lt;/p&gt;
&lt;p&gt;Before it uses that answer, the viewer’s cluster checks that the origin is still a peer the tenant is allowed to use. That check is made fresh, so a cluster whose access was revoked cannot keep supplying streams on the strength of an old record.&lt;/p&gt;
&lt;p&gt;It then asks the European cluster for a good source edge. The origin scores its own edges and replies with candidates. One of the North American edges opens a pull from that source over DTSC, MistServer’s native server-to-server transport. The control plane only decides which node pulls from which source; the media moves directly between MistServer instances.&lt;/p&gt;
&lt;p&gt;The viewer in Toronto waits for that pull to start, which is the setup cost of the first cross-cluster view. The second viewer in Toronto, and the hundredth, are served from the same North American edge with no additional traffic across the ocean.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;graph LR
    subgraph &quot;Cluster A (origin, Europe)&quot;
        PROD[&quot;Broadcaster&quot;] --&gt; EA[&quot;Edge A&quot;]
    end
    subgraph &quot;Cluster B (North America)&quot;
        EB[&quot;Edge B&quot;]
        V[&quot;Viewers in Toronto&quot;]
    end
    EA -. &quot;stream advertisements&quot; .-&gt; EB
    EB -- &quot;one DTSC pull&quot; --&gt; EA
    EB -- &quot;HLS / DASH / WebRTC&quot; --&gt; V&lt;/pre&gt;
&lt;div&gt;&lt;h2 id=&quot;a-copy-is-made-once-from-the-origin-only&quot;&gt;A copy is made once, from the origin only&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The obvious failure of a system like this is a cascade: every cluster opening its own pull, or clusters pulling from each other’s copies until nobody knows where the original is.&lt;/p&gt;
&lt;p&gt;Two rules prevent that. When a cluster starts pulling a stream, it tells its peers, so other clusters can see that a copy exists and where it came from. And replication is strictly single-hop: an edge pulls from the origin cluster, never from another cluster’s replica. A pull chain has no path along which to loop.&lt;/p&gt;
&lt;p&gt;Federation state is also built to expire rather than to be cleaned up. If a peer crashes or is cut off, its advertisements stop and its streams drop out of the directory, so nobody is routed toward a copy that no longer exists. When a peer is unreachable, requests to it fail quickly with an explicit error and the caller falls back to whatever local answer it has, instead of blocking a viewer on a dead connection.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;who-pays-for-the-extra-hop&quot;&gt;Who pays for the extra hop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Cross-cluster serving raises an accounting question that had to be answered precisely. Every event is stamped with the cluster that did the work and with the stream’s origin cluster. Delivery is billed to the cluster that actually served the viewer, so a tenant’s bill reflects where their audience watched. The DTSC traffic between clusters is our infrastructure cost and never appears as a tenant line item. If a rated usage record ever arrives without a cluster, billing refuses to guess and fails closed.&lt;/p&gt;
&lt;p&gt;Routing decisions land in analytics with the serving and origin clusters recorded, so cross-cluster serves are visible in the dashboard rather than hidden inside an aggregate.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-federation-does-not-decide&quot;&gt;What federation does not decide&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Federation answers “can this cluster get the stream”. It does not by itself decide which edge a viewer should use.&lt;/p&gt;
&lt;p&gt;The limits are straightforward. The first viewer in a new cluster pays the setup latency of one pull. Federation only runs between clusters that share a tenant, so a stream cannot spread into a cluster its owner has no relationship with. And because replication is single-hop, every cluster that needs the stream pulls from the origin, which carries one outgoing copy per serving cluster.&lt;/p&gt;
&lt;p&gt;How edges are chosen across clusters has changed since this post. Media placement now compares eligible edges in every permitted cluster by health, capacity and distance to the viewer, and lets accounts state rules such as “my own servers first”. &lt;a href=&quot;https://logbook.frameworks.network/blog/your-stream-your-edge&quot;&gt;Your Stream, Your Edge&lt;/a&gt; explains that change.&lt;/p&gt;
&lt;p&gt;For the operator’s view of peering, artifact routing across clusters and billing attribution, see &lt;a href=&quot;https://logbook.frameworks.network/operators/multi-cluster&quot;&gt;multi-cluster operations&lt;/a&gt;. For how routing choices are made and previewed, see the &lt;a href=&quot;https://logbook.frameworks.network/builders/media-placement&quot;&gt;media placement guide&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>engineering</category><category>architecture</category><category>federation</category></item><item><title>Skipper: An AI Video Consultant With Real Diagnostics</title><link>https://logbook.frameworks.network/blog/skipper-launch/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/skipper-launch/</guid><description>Skipper answers streaming questions from curated docs, runs diagnostics against your live streams, and acts through the same MCP gateway your own agents use. Every part of an answer says how sure it is.

</description><pubDate>Sat, 07 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A question we see often, in some form, is “my viewers keep buffering, what’s wrong?” No docs page answers that. Answering it means looking at the stream (rebuffer ratio, packet loss, which edges the viewers landed on, whether the encoder is keeping up) as well as the documentation.&lt;/p&gt;
&lt;p&gt;Skipper does both. It is an AI video consultant built into FrameWorks: it searches curated streaming documentation, runs diagnostics against your live streams, generates API operations from the current schema, and, in authenticated dashboard chat, can manage streams and billing for you.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-question-answered-with-your-streams-data&quot;&gt;One question, answered with your stream’s data&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Take the buffering question. Ask Skipper “why is stream X buffering” and it pulls the stream’s real numbers: rebuffering patterns, packet loss, buffer health, and where viewers were routed. The diagnosis that takes 30 minutes of dashboard-hopping by hand comes back as one answer with the evidence attached, so you can see whether the problem is the encoder’s uplink, one region’s viewers, or a player falling behind.&lt;/p&gt;
&lt;p&gt;When the answer is a configuration change, Skipper looks it up before it tells you. Knowledge questions go to a curated corpus that covers FrameWorks, MistServer, FFmpeg, OBS, SRT, HLS, DASH, WebRTC, nginx-rtmp, and Livepeer, and source-backed answers include citations. Topics the corpus does not cover fall back to web search where one is configured.&lt;/p&gt;
&lt;p&gt;Ask “give me a Python script that creates a stream” and it reads the live GraphQL schema at query time, generates the operation, and wraps it in code. The output matches your current API rather than an API shape from the model’s training data.&lt;/p&gt;
&lt;p&gt;In authenticated dashboard chat it can also act: create streams, refresh keys, clip the last 30 seconds of a broadcast, start a DVR recording, upload VOD assets, check your balance, or start a supported crypto top-up. Each action is a call to the same GraphQL API you would use directly, and each one shows up in the chat as a card with what ran and what came back.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;it-tells-you-when-it-is-guessing&quot;&gt;It tells you when it is guessing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A consultant that sounds equally sure about everything is hard to use, because you cannot tell which part to check. Skipper tags each section of an answer with one of four levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Verified&lt;/strong&gt;: grounded in the curated knowledge base, with source links.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sourced&lt;/strong&gt;: backed by live web search results.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best guess&lt;/strong&gt;: from the model’s general knowledge, not checked against a source.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unknown&lt;/strong&gt;: nothing supporting it was found.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mixed answers are the common case. In that Python snippet, the GraphQL operation comes from the real schema, while the Python around it is model-written glue code and is tagged as a best guess. The tag tells you where to spend your review time. Skipper is also instructed never to guess CLI flags, codec parameters, or configuration values without a source, which is where a confident wrong answer does the most damage.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;your-agents-get-the-same-access-and-no-more&quot;&gt;Your agents get the same access, and no more&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Skipper connects through the same &lt;a href=&quot;https://logbook.frameworks.network/agents/mcp&quot;&gt;MCP gateway&lt;/a&gt; that external agents use: the same GraphQL API, the same tool catalog, the same authorization checks. It has no private path into the platform. Anything it can do in your account, your own agents can do too, and anything your account cannot do, Skipper cannot do either.&lt;/p&gt;
&lt;p&gt;That boundary shapes where it runs and what it may change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard&lt;/strong&gt;: authenticated chat with account-scoped diagnostics and management actions where enabled.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docs site&lt;/strong&gt;: a chat panel on every page (Cmd/Ctrl+J) that can explain and inspect but never modifies anything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your own agents&lt;/strong&gt;: any MCP client can call &lt;code dir=&quot;auto&quot;&gt;ask_consultant&lt;/code&gt; for the full answer with confidence tags. Changes are blocked on that path; an agent that intends to create or delete something calls the dedicated platform tools, which carry their own authorization.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keeping changes out of the consultant path means an agent asking Skipper a question cannot end up altering your account as a side effect of the answer.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;watching-streams-when-nobody-is-asking&quot;&gt;Watching streams when nobody is asking&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Where it is enabled, Skipper also checks the health of your active streams and infrastructure on a schedule. It learns what normal looks like for your account and runs a full investigation only when something departs from it; the report comes back with a root cause and recommendations over email or the channels you have configured. Accounts whose streams are healthy use no model calls at all during these checks, so monitoring does not turn into a background token bill.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-does-not-do&quot;&gt;What it does not do&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Skipper is a consultant, not an operator with its own authority. It can only act where your account can, on the dashboard, and with the tier and rate limits of your plan. Standard usage billing applies to stream operations it performs on your behalf. Its answers are only as current as the knowledge base and the schema it reads, which is why the confidence tags exist: verify a best-guess section before acting on it.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://logbook.frameworks.network/agents/skipper&quot;&gt;Skipper guide&lt;/a&gt; covers where to find it, what it can do, and how agents call it. Operators running their own deployment can pick models, add knowledge sources, and tune retrieval and monitoring in the &lt;a href=&quot;https://logbook.frameworks.network/operators/skipper&quot;&gt;operator guide&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>announcements</category><category>skipper</category><category>ai</category></item><item><title>What Seven AI Agents Found in Our Streaming Platform</title><link>https://logbook.frameworks.network/blog/agentic-audit-pipeline/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/agentic-audit-pipeline/</guid><description>We used a multi-agent review process to audit FrameWorks across routing, ingest, billing, analytics, auth, and more. The useful part was forcing every claim to come with evidence.

</description><pubDate>Sat, 31 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Live video systems collect awkward edge cases. A viewer connects from a location that sits on the wrong side of a geofence boundary. A DVR recording starts at the same moment a stream is shutting down. Two usage events for the same tenant arrive close enough together that billing code has to prove it is actually idempotent. None of those cases are hard to understand on their own. The failures come from these cases interacting across services.&lt;/p&gt;
&lt;p&gt;FrameWorks is split across routing, ingest, VOD, edge orchestration, analytics, billing, auth, MCP agent access, DNS, Skipper, WebSocket routing, and tenant management. Each subsystem has tests and reviews, but most of the failures we found sit between them.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-finding-without-evidence-did-not-count&quot;&gt;A finding without evidence did not count&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;We ran the first audit pass by hand: seven roles, 26 batches, 90 tasks, 12 subsystems. One agent traced a domain, another checked the report, another wrote a fix if the finding held up, another reviewed the patch. CI and a human reviewer still owned the merge.&lt;/p&gt;
&lt;p&gt;Every finding had to include evidence. A report had to name the files and functions, describe the failing input or interleaving, explain the tenant or operator impact, and say what test would prove the fix. “This might race” went nowhere. “Two prepaid usage reports can reserve against the same tenant balance before the ledger state is visible” was at least reviewable.&lt;/p&gt;
&lt;p&gt;That rule removed most of the noise. Agents had to keep reading until they could point at the code instead of reporting general impressions of the codebase.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;most-failures-sat-between-subsystems&quot;&gt;Most failures sat between subsystems&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The findings were mostly the kind you expect after a platform gets broad enough:&lt;/p&gt;


















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Category&lt;/th&gt;&lt;th&gt;Count&lt;/th&gt;&lt;th&gt;Example&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Race conditions&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;Prepaid balance deduction under concurrent Kafka messages&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tenant isolation&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;Stream context cache keyed by &lt;code dir=&quot;auto&quot;&gt;internal_name&lt;/code&gt; only, missing &lt;code dir=&quot;auto&quot;&gt;tenant_id&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Data loss risks&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;Event ingest batch flush interrupted by producer crash&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Stale cache behavior&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;GeoIP cache stampede under bursty traffic&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Protocol edge cases&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;Player protocol blacklist leading to “no playable protocol” dead-end&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Storage consistency&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;S3 upload succeeds but local delete fails, leaving duplicate artifacts&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Auth bypass vectors&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;GraphQL complexity bypass through deep nesting with small page sizes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DNS propagation&lt;/td&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;Stale DNS pointing at decommissioned nodes&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Some of the best results were not bug fixes. A reviewer would chase a claim and find the lower layer already enforced the invariant. That turned a hidden assumption into something we could preserve during the next change.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;agents-traced-well-and-overstated-severity&quot;&gt;Agents traced well and overstated severity&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The agents were good at long, boring traces. GraphQL to gRPC to a service event. Kafka to ClickHouse to the billing ledger. Load balancer state to the edge’s playback rewrites to the player fallback path. A person can do that too, but it is easy to stop when the first function looks fine; the agents kept following each path to the end.&lt;/p&gt;
&lt;p&gt;The weak point was severity. LLMs like to make findings sound bigger than they are. The review pass had to push hard on impact: can a tenant hit this, does it cross a trust boundary, is there data loss, is it only a noisy log, is the bad state already repaired one layer later?&lt;/p&gt;
&lt;p&gt;The first run also taught us what a finding report should look like. Early reports came padded with process notes and architecture recaps, which did not help decide whether a bug was real. The format we settled on has one paragraph for the claim, the exact files and functions, the failing interleaving or input, the tenant impact, the smallest reasonable fix, and the test that proves it. Batch size mattered too. Three tasks per batch kept the human reviewer ahead of the queue; bigger batches just moved the bottleneck to whoever had to merge.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-changed-in-our-reviews&quot;&gt;What changed in our reviews&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The lasting change was in review questions. We now ask more often whether a cache key includes &lt;code dir=&quot;auto&quot;&gt;tenant_id&lt;/code&gt;, whether a handler survives Kafka retry, whether stream lifecycle events can arrive out of order, and whether an edge can keep serving a previously approved playback path when the control plane is temporarily out.&lt;/p&gt;
&lt;p&gt;Next up is lightweight automation: track when each subsystem was last audited, weight the riskier areas like billing and auth, and open work only when there is a useful review to run. Most audit cycles should work like &lt;a href=&quot;https://logbook.frameworks.network/agents/skipper&quot;&gt;Skipper’s automated monitoring&lt;/a&gt;: check the current state and open work only when something needs attention.&lt;/p&gt;</content:encoded><category>engineering</category><category>ai</category><category>architecture</category></item><item><title>Welcome to the FrameWorks Blog</title><link>https://logbook.frameworks.network/blog/welcome/</link><guid isPermaLink="true">https://logbook.frameworks.network/blog/welcome/</guid><description>Why we&apos;re writing about video infrastructure in public, and what to expect from this blog.

</description><pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most streaming platforms keep their architecture behind closed doors. You get API docs, a dashboard, and a support email. How the ingest pipeline actually works, how viewer routing decisions are made, how billing enforcement interacts with stream lifecycle: that stays internal.&lt;/p&gt;
&lt;p&gt;We publish ours. If you run live video on a platform, you should be able to see how it works: the API surface and the design decisions underneath it.&lt;/p&gt;
&lt;p&gt;This blog is where we write about that. Expect architecture deep-dives into how subsystems are designed and the trade-offs behind them, technical explanations of what shipped and how it works under the hood, and notes on how we test, audit, and operate a multi-tenant streaming platform.&lt;/p&gt;
&lt;p&gt;We publish when something ships or when a design decision needs explaining.&lt;/p&gt;</content:encoded><category>announcements</category></item></channel></rss>