Live TV broadcast
Sign a 24/7 channel at the encoder. Each channel uses a dedicated StreamSdk instance owned by the encoder process. The Limbo signing service is invoked only at each init call (the first one and every key-refresh re-init), never per segment. Per-segment signing runs locally on the encoder host using session keys held in the customer's vault.
Encoder hook
import { SignerClient, StreamSdk } from "@limboai/verifox-sdk-issuer";
import { vault, ledger } from "./infra";
import { pushInit, pushSegment } from "./origin";
const client = await SignerClient.connect("https://api.trylimbo.com", {
headers: { authorization: `Bearer ${process.env.LIMBO_API_KEY}` },
});
const sdk = StreamSdk.create(client, ledger, vault);
const rotationPolicy = { validityPeriodSecs: 3600, refreshPercent: 70 };
const manifest = {
origin: { sourceType: "digitalCapture" },
};
let init = await sdk.init(channelId, initBody, channelId, { mode: "embedded" }, rotationPolicy, manifest);
await pushInit(init.signedInitBytes);
for await (const body of segments) {
const signed = await sdk.sign(channelId, body);
await pushSegment(signed.bytes);
if (signed.rotationDue) {
// Re-init under the same channelId: mints a fresh key, signs a new
// manifest, and forgets the previous key from the vault. Tell the
// delivery layer to signal a discontinuity so players re-fetch.
init = await sdk.init(channelId, initBody, channelId, { mode: "embedded" }, rotationPolicy, manifest);
await pushInit(init.signedInitBytes);
}
}
await sdk.stop(channelId);
Each init call returns a fresh signed init manifest, both the first one and every key-refresh re-init under the same channelId. Push it to the origin so viewers crossing into the new key window re-fetch it. sign returns the signed media segment together with rotationDue (true once refreshPercent% of the active key's validity has elapsed) and validUntilEpochSecs (when the key expires) so the encoder can schedule the next re-init.
Calls to sdk.sign for the same channelId must not interleave: each call does a get/put cycle against the ledger to advance the sequence number, and concurrent cycles would corrupt that state. The loop above is serial by construction. If the encoder parallelizes segment emission within a single channel, wrap calls in a per-channelId mutex.
Production storage choices
Why the vault and ledger are customer-operated, and why key refresh is encoder-driven, lives on the Stream storage page. This section covers the production-grade choices for this scenario.
Vault. Back the SigningVault with the system that already mints short-lived keys: AWS KMS data keys, an HSM partition with key derivation, or Vault Transit. The encoder never receives raw private bytes. mint returns a kid and the session public key, and the vault performs segment signing via sign(kid, payload).
Ledger. Back the SessionLedger with a transactional store: Redis (with persistence and replication configured for the broadcast's durability requirement), Postgres, or DynamoDB. The per-channelId get/put cycle must be atomic, so rely on Redis WATCH/MULTI, Postgres SERIALIZABLE, or DynamoDB conditional writes. If a pod terminates mid-broadcast, a replacement pod reads the ledger and resumes the same session from the persisted state.
Failover
For multi-region deployments, run primary and standby encoders. Both can hold a StreamSdk for the same channel against the same ledger, but only one signs at any given time. On failover, the standby reads the ledger and continues from the persisted state. The deployment, not the SDK, must exclude concurrent writers. See Single writer per stream id.
Viewer side
Players fetch segments from the CDN as they would for any other stream. The browser-side StreamSdk validates each segment against the init manifest. A tampered segment fails validation; the remainder of the stream continues to play.