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 grounded in the 2026 platform reality. Awaiting first customer-integration validation.

README

The developer-facing documentation in full

Rendered from packages/sdks/renderer-core/README.md at build time — the same source the package ships with.

@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.

Sandbox

Run this SDK against a real tenant

The hosted sandbox is the fastest way to verify Renderer Core SDK against a real EnfinitOS tenant before committing to a pilot. Launching Q4 2026.