EnfinitOSEnfinitOS
DevelopersRenderer core
Production-ready scaffold

Renderer Core SDK

The substrate-agnostic foundation. Every render SDK on this page composes Renderer Core. Composition, not inheritance.

@enfinitos/sdk-renderer-coreSubstrate AllTypeScript
Install

Get the SDK

npm install @enfinitos/sdk-renderer-core

About this status badge

Typed, tested, documented, and wired to the EnfinitOS platform endpoints that exist today. Vendor-side SDK integrations (Broadsign / VIOOH / DJI / Tizen / Alexa / Twilio / Stripe / etc.) land per-customer at pilot integration time — those bring the renderer/transport/exchange-specific code; the EnfinitOS half is ready.

README

The developer-facing documentation in full

The same README the SDK package ships with — rendered here at build time so what you read matches exactly what you install.

@enfinitos/sdk-renderer-core

EnfinitOS substrate-agnostic renderer SDK — the foundation every substrate-specific renderer (DOOH player, mobile, CTV, streaming, AR / glasses, HUD, smart-home, wearables, audio, messaging, hologram) wraps.

Today the platform supports 24 substrate kinds (DOOH plus 22 sequenced on the post-DOOH roadmap, plus a substrate-agnostic catch-all). Each substrate has its own delivery primitive — a DOOH player drives a video panel on a bus shelter; a mobile SDK drives in-app overlays; an audio SDK drives an Alexa skill insertion. But all of them need the same five primitives against the platform:

  1. Connect to the platform with a device-scoped token.
  2. Resolve what to show in a slot.
  3. Deliver the content (substrate-specific — not in this SDK).
  4. Report what happened (proof-of-play events).
  5. Report device health (heartbeat).

This package implements primitives 1, 2, 4, 5 once. Every substrate-specific renderer takes a dependency on this package and adds only its own delivery primitive (primitive 3) on top.

Platform-side counterpart. This SDK consumes the runtime plane endpoints in apps/api/src/modules/runtime/*resolve, grant, event-ingest, contract, and health-ingest — plus /v1/rights/consent/check from the rights module. Those endpoints exist on the platform today. The SDK is the device-side counterpart; substrate-specific wrappers depend on this package and on whichever delivery primitive their substrate uses.

Architecture

                          ┌──────────────────────────┐
                          │   EnfinitOS Platform     │
                          │  (runtime, rights,       │
                          │   audit, fleet health)   │
                          └────────────┬─────────────┘
                                       │  HTTPS REST
                                       │  + optional WS push
                                       │  (JWT in handshake)
                                       │
   ┌── (1) resolve+grant ──────────────▼───────────────┐
   │                                                   │
   │    ┌─────────────────────────────────────┐        │
   │    │    @enfinitos/sdk-renderer-core     │        │
   │    │                                     │        │
   │    │   ┌──────────┐    ┌──────────┐      │        │
   │    │   │ResolveLoop│    │  Event   │      │        │
   │    │   │           │    │ Reporter │      │        │
   │    │   └──────────┘    └──────────┘      │        │
   │    │                                     │        │
   │    │   ┌──────────┐    ┌──────────┐      │        │
   │    │   │  Health  │    │ Consent  │      │        │
   │    │   │ Reporter │    │  Client  │      │        │
   │    │   └──────────┘    └──────────┘      │        │
   │    │                                     │        │
   │    │   ┌──────────────────────────────┐  │        │
   │    │   │ Transport (HTTPS or WS)      │  │        │
   │    │   └──────────────────────────────┘  │        │
   │    └─────────────────────────────────────┘        │
   │                                                   │
   └──────────────────────────────────────────────────┘
                   │                  ▲
   (2) play       │                  │ (3) push directives
       events      │                  │     (WS substrates only)
   (4) health      │                  │
                   ▼                  │
   ┌──────────────────────────────────┴───────────────┐
   │      Substrate-specific renderer wrapper          │
   │      (DOOH / mobile / CTV / AR / audio / etc.)    │
   └───────────────────────────────────────────────────┘
                              │
                              ▼
                    Substrate's delivery primitive
                    (video panel / mobile view /
                     CTV ad-pod inserter / etc.)

Why this package exists

Without a shared core, every substrate-specific SDK would have to implement the five primitives independently. The team surveyed that path and rejected it for three reasons:

  1. Consistency at the audit layer. Proof-of-play events are the billing stream. If a DOOH SDK and a CTV SDK shape their events differently, the reconciliation logic on the platform side becomes a per-substrate mess. By pooling the event shape here, every substrate ships compatible events.
  2. Compliance reuse. Pre-render consent gating (/v1/rights/consent/check) applies to seven of the 24 substrates (mobile, social, audio, wearables, neural, …). The wire-shape for that call is non-trivial; building it once is correct.
  3. Operability. A single health-heartbeat shape across all substrates means the SRE dashboard speaks one schema. Per- substrate health envelopes would require a fan-in normaliser the platform side doesn't want to own.

Getting started

Install

pnpm add @enfinitos/sdk-renderer-core

Two classes — pick one

Two public classes live in this package; they share the same five subsystems, they just differ in API shape:

- EnfinitOSRendererClient — the recommended class for new code. Async-shaped report* methods, an optional in-process cache via cache, a standalone grant(assetId) for two-phase substrates, and the brief's canonical API surface (reportClick, reportConversion). - EnfinitOSRenderer — the original class. Synchronous report* methods, no built-in cache, resolve+grant folded into resolveNext. Kept for callers that wired against the v1 SDK.

Both compose the same subsystems and consume the same platform endpoints. Substrate-specific wrappers should prefer EnfinitOSRendererClient; substrate wrappers built against the v1 SDK keep working unchanged.

Five-minute hello-world

import { EnfinitOSRendererClient } from "@enfinitos/sdk-renderer-core";

const renderer = new EnfinitOSRendererClient({
  apiBaseUrl: "https://api.enfinitos.com",
  deviceId: process.env.DEVICE_ID!,
  substrate: "DOOH",                     // or CTV, MOBILE, AUDIO, …
  slotPositionHint: "placement_paddington_1",
  authToken: process.env.DEVICE_JWT!,
  cache: { defaultTtlMs: 30_000 },       // optional: 30s offline-blip cache
});

// 1) connect
await renderer.start();

// 2) resolve one asset
const asset = await renderer.resolveNext({
  location: { lat: 51.5151, lng: -0.1410 },
});

if (asset) {
  // 3) deliver (substrate-specific — your code, not the SDK's)
  const startedAt = new Date();
  await myPlayer.show(asset.assetUrl, asset.renderSpec);
  await renderer.reportPlayStarted(asset, startedAt);

  const dwellMs = Date.now() - startedAt.getTime();
  await renderer.reportPlayEnded(asset, new Date(), dwellMs);

  // 3b) on viewer tap-through:
  await renderer.reportClick(asset, new Date());

  // 3c) on post-render attribution match:
  await renderer.reportConversion(asset, "purchase", new Date());
}

// 4) optional: heartbeat every 30 s
renderer.startHealthHeartbeat(30_000, () => ({
  state: "OK",
  subsystems: { decoder: "nominal", network: "ok" },
}));

// 5) on shutdown
await renderer.stop();

Standalone grant() for two-phase substrates

CTV ad-pods and streaming SSAI use a two-phase flow: resolve a pod ahead of the cue, grant per-slot at the cue point. Use the standalone primitive for that:

import { EnfinitOSRendererClient } from "@enfinitos/sdk-renderer-core";

const renderer = new EnfinitOSRendererClient({ /* ... */ });
await renderer.start();

// Resolve discovers a pod with 3 candidates (assetIds: a, b, c).
// At the cue point, the player picks one and grants it:
const granted = await renderer.grant("asset_a", { assetVersion: 7 });
await myPlayer.show(granted.assetUrl, granted.renderSpec);

30-line minimal renderer for a generic substrate

import {
  EnfinitOSRendererClient,
  type RightSubstrate,
  type ResolvedAsset,
} from "@enfinitos/sdk-renderer-core";

export async function runOneSlot(
  substrate: RightSubstrate,
  deliver: (a: ResolvedAsset) => Promise<void>,
) {
  const renderer = new EnfinitOSRendererClient({
    apiBaseUrl: process.env.ENFINITOS_API!,
    deviceId: process.env.DEVICE_ID!,
    authToken: process.env.DEVICE_JWT!,
    substrate,
    cache: { defaultTtlMs: 30_000 },
    onError: (e) => console.error("[sdk]", e),
  });
  try {
    await renderer.start();
    const asset = await renderer.resolveNext();
    if (!asset) return;
    const startedAt = new Date();
    await renderer.reportPlayStarted(asset, startedAt);
    try {
      await deliver(asset);
      await renderer.reportPlayEnded(
        asset, new Date(), Date.now() - startedAt.getTime(),
      );
    } catch (e) {
      await renderer.reportPlayError(asset, e as Error);
    }
  } finally {
    await renderer.drainEvents();
    await renderer.stop();
  }
}

Substrate matrix

Each substrate-specific renderer wraps this core. The "wrap" is just a thin package that:

- chooses the transport (HTTPS REST or WebSocket push); - chooses the resolve cadence (poll, ad-cue-driven, navigation- driven); - implements the substrate-specific delivery primitive (video panel, audio playback, in-app overlay, AR scene); - calls the renderer-core's resolveNext / reportPlay* / reportHealth / checkConsent as appropriate.

SubstrateTransportConsent gateStatus
DOOHHTTPSnoshipped (pilot)
CTVHTTPSmaybepost-DOOH
MOBILEHTTPSyespost-DOOH
STREAMINGWS (SSAI cues)maybepost-DOOH
AUDIOHTTPSyespost-DOOH
AR_CONTACTS / GLASSESHTTPSviewer-attestedpost-DOOH
HUD / AUTOMOTIVEHTTPSn/a (vehicle bus)post-DOOH
SMART_HOMEHTTPSyespost-DOOH
WEARABLESHTTPSyespost-DOOH
MESSAGINGHTTPSyespost-DOOH
HOLOGRAM / VOLUMETRICWSvariespost-DOOH
ROBOTICS(uses @enfinitos/sdk-robotics instead)n/ashipped
DRONE / SATELLITE / AVIATION / MARITIMEvariesn/apost-DOOH
NEURALHTTPSalwaysfar-future

See docs/launch/substrate-readiness-matrix.md for the per-substrate institutional-grade tracker.

Transport guidance

Two transports ship in the package.

HTTPS REST (default)

Pick when:

- The renderer is fundamentally request/response. - The slot cadence is predictable (DOOH ~30 s, CTV per-break, mobile per-navigation). - The substrate has no need for unsolicited platform → device pushes.

How to choose:

new EnfinitOSRenderer({
  // ... (defaults to HTTPS)
});

WebSocket push

Pick when:

- The platform needs to push to the device at unpredictable times (live-event hologram, streaming SSAI cue injection). - The substrate's latency budget can't afford a polling round-trip. - The substrate model is "stay connected, accept directives".

How to choose:

new EnfinitOSRenderer({
  // ...
  transport: { kind: "ws" },
});

The WS transport speaks an envelope-shaped sub-protocol on top of the WebSocket (request, response, push). The platform's WS endpoint is /runtime/connect derived from apiBaseUrl.

Note: the WS transport in this SDK is distinct from the @enfinitos/sdk-robotics WS transport. Robotics has its own tagged-union wire shape for control-plane messages; renderer-core speaks an HTTP-style envelope so the same call sites work on either transport. They are not interchangeable.

Event-queue semantics & delivery guarantees

Every reportPlay* and reportInteraction call enqueues an event and returns synchronously. A background drain ships batches of events to /runtime/event-ingest with:

- Bounded backlog. Default 10 000 events per device. Configurable. Beyond the bound, oldest events drop first and droppedEventCount increments — surfaced in the next heartbeat so the SRE dashboard sees the loss. - Idempotent eventIds. Every event carries a deterministic id derived from (substrate, assetId, assetVersion, occurredAt, kind, seq). A retried event collides with the original on the platform side and is de-duped. The renderer can safely retry after a transient transport failure. - Exponential backoff with jitter. Retryable failures (5xx, 429, network) → exponential backoff capped at 30 s. Non- retryable failures (4xx) drop the batch and increment droppedEventCount. - Batched. Up to 50 events per drain pass (configurable). Reduces the platform's ingest QPS during recovery from a network drop.

This gives an at-least-once guarantee for events: every event either lands on the platform OR is counted as dropped. There are no silent losses.

The same machinery does NOT apply to health heartbeats — health is best-effort by design. A missed heartbeat at t=12 doesn't matter if t=13 lands.

How to write a substrate-specific wrapper

The recommended pattern is composition, not inheritance. Inherit from EnfinitOSRenderer only if you need to override createDefaultTransport (e.g. to add a substrate-specific custom transport).

// packages/sdks/dooh-player-ts/src/index.ts

import {
  EnfinitOSRenderer,
  type EnfinitOSRendererOptions,
  type ResolvedAsset,
} from "@enfinitos/sdk-renderer-core";

export type DoohPlayerOptions = Omit<EnfinitOSRendererOptions, "substrate"> & {
  /** DOOH-specific: the video element / canvas to render into. */
  surface: HTMLVideoElement | OffscreenCanvas;
  /** DOOH-specific: dwell estimator (a camera-based or proximity-
   *  based metric). */
  dwellEstimator?: () => number;
};

export class DoohPlayer {
  private readonly core: EnfinitOSRenderer;
  private readonly surface: HTMLVideoElement | OffscreenCanvas;
  private readonly dwellEstimator: () => number;

  constructor(opts: DoohPlayerOptions) {
    this.core = new EnfinitOSRenderer({
      ...opts,
      substrate: "DOOH",
    });
    this.surface = opts.surface;
    this.dwellEstimator = opts.dwellEstimator ?? (() => 0);
  }

  async start(): Promise<void> {
    await this.core.start();
    // Optional: start the DOOH player's substrate-specific
    // scheduling loop here (typically 30s slot cadence with a
    // safety-margin re-resolve at ~25s).
  }

  async playOne(): Promise<void> {
    const asset = await this.core.resolveNext();
    if (!asset) return;
    const startedAt = new Date();
    this.core.reportPlayStarted(asset, startedAt);
    try {
      await this.renderTo(asset);
      this.core.reportPlayEnded(asset, new Date(), this.dwellEstimator());
    } catch (e) {
      this.core.reportPlayError(asset, e as Error);
    }
  }

  private async renderTo(asset: ResolvedAsset): Promise<void> {
    // DOOH-specific delivery — load the asset URL into the video
    // surface, await playback completion, etc. This is the bit
    // the renderer-core doesn't know about.
  }
}

A typical wrapper adds 100–300 lines on top of this core — most of it substrate-specific delivery code. The shared primitives stay right here.

Error model

Every public API throws / rejects with RendererError. Each RendererError carries:

- code — greppable platform error code or one of the RENDERER_ERROR_CODES. - domain — coarse failure category (auth, resolve, grant, ingest, health, consent, transport, contract, config). - httpStatus — original HTTP status if the error came from a server response. - retryable — default per-domain; overridable.

import { isRendererError } from "@enfinitos/sdk-renderer-core";

try {
  await renderer.resolveNext();
} catch (e) {
  if (isRendererError(e)) {
    if (e.domain === "auth") {
      // refresh the device JWT
    } else if (e.retryable) {
      // schedule a retry
    } else {
      // permanent — show a fallback
    }
  }
}

Endpoint mapping

Renderer-core callPlatform endpointModule
resolveNext() (resolve phase)POST /runtime/resolveapps/api/src/modules/runtime/resolveRoutes.ts
resolveNext() (grant phase)POST /runtime/grantapps/api/src/modules/runtime/grantRoutes.ts
reportPlay* (drain)POST /runtime/event-ingestapps/api/src/modules/runtime/eventIngestRoutes.ts
getContract()GET /runtime/contractapps/api/src/modules/runtime/contractRoutes.ts
checkConsent()POST /v1/rights/consent/checkapps/api/src/modules/rights/contracts/consent.ts
reportHealth() / heartbeatPOST /runtime/health-ingest(paired with the existing runtime module)

Each endpoint can be overridden via the endpoints constructor option for customers running the platform on a non-default path.

See also

  • packages/sdks/robotics-ts/README.md — the reference SDK whose composition + reconnection + queueing pattern this package mirrors for the renderer substrates.
  • apps/api/src/modules/runtime/* — platform-side counterparts to the five renderer-core primitives.
  • apps/api/src/modules/rights/contracts/scope.tsRIGHT_SUBSTRATES source of truth on the platform side.
  • docs/launch/substrate-readiness-matrix.md — per-substrate institutional-grade readiness tracker. Today: DOOH shipped, ROBOTICS shipped, every other substrate has type-system support; substrate-specific wrappers sit on the post-DOOH roadmap.
API reference

Hit the HTTP surface directly

The Renderer Core SDK is a thin client over the same governed HTTP API every other SDK calls. The full OpenAPI 3.1 reference lives on the docs site, published alongside the April 2027 platform launch.

Sandbox

Run this SDK against a real tenant

The browser demo at enfinitos.com/sandbox runs today against a shared synthetic tenant. The dedicated developer sandbox — your own persistent tenant, API keys, full HTTP-contract coverage — opens ahead of the April 2027 platform launch.