Skip to content

Provider Architecture

By default the SDK evaluates placements and entitlements inside your app, from the Playbook — no provider is involved, and most apps never configure one. A provider is an escape hatch for teams that already have a placement or entitlement source of their own and want the SDK’s rendering, tracking, caps, and cooldowns on top of it. A provider is a plain object that implements one or more domain methods — placement resolution, entitlement checks, placement type persistence, and user identification.

TypeScript
interface RevTurbineSdkProvider {
getPlacement?: (config: RevTurbinePlacementRequestConfig) => Promise<PlacementOutput | null>;
checkEntitlement?: (handle: string, context?: RevTurbineEntitlementContext) => Promise<EntitlementResult>;
persistPlacementTypes?: (types: RevTurbinePlacementTypeEntity[]) => Promise<void>;
identify?: (userId: string, contextOrTraits?: UserContextInput | SdkTraits) => void;
}

Every method is optional. Implement only the domains you need to customize — the SDK falls back to its built-in local runtime for anything you don’t provide.

Resolves a placement for a given slot and surface type. Called by <Slot> (and its FixedSurfaceSlot / MessageSurfaceSlot variants), <Gate>, and the headless API.

TypeScript
const analyticsProvider: RevTurbineSdkProvider = {
async getPlacement(config) {
// config.slotId — the slot requesting a placement
// config.componentType — the component type (button, modal, banner, etc.)
// config.entitlementHandle — for access gate slots
// Example: a placement source you already run (escape hatch — the
// default path evaluates the Playbook locally and needs no provider)
const res = await fetch(`/api/placements/${config.slotId}`);
if (!res.ok) return null;
return res.json();
},
};

Input — RevTurbinePlacementRequestConfig:

FieldTypeDescription
slotIdstring?Slot identifier
componentTypeRevTurbineComponentType?Component type (button, modal, banner, etc.)
entitlementHandlestring?Entitlement to check (access gates)
planHandlestring?Plan-specific placements
placementHandlestring?Chaining from a prior CTA path

Return: PlacementOutput | null — the resolved placement, or null if no placement matches.

Checks whether the current user has access to a feature. Called by <Gate> and sdk.checkEntitlement().

TypeScript
const entitlementProvider: RevTurbineSdkProvider = {
async checkEntitlement(handle, context) {
// handle — e.g. "data_export", "brand_kit"
// context — optional entitlement context
const res = await fetch(`/api/entitlements/${handle}`);
const data = await res.json();
return {
status: data.allowed ? 'allowed' : 'denied',
allowed: data.allowed,
};
},
};

Return — EntitlementResult:

FieldTypeDescription
statusstring'allowed', 'limited', or 'denied'
allowedbooleanWhether access is granted

Stores placement type metadata. Called during SDK initialization to register built-in and custom slot types.

TypeScript
const storageProvider: RevTurbineSdkProvider = {
async persistPlacementTypes(types) {
// types — array of { id, label, description, componentType, priority }
await fetch('/api/placement-types', {
method: 'POST',
body: JSON.stringify(types),
});
},
};

Called when a user is identified or their context changes. Use this for analytics, session tracking, or syncing identity to your backend.

TypeScript
const identityProvider: RevTurbineSdkProvider = {
identify(userId, contextOrTraits) {
analytics.identify(userId, contextOrTraits);
},
};

Pass your provider via the provider option:

React
import { RevTurbineProvider, RuntimeMode } from '@revturbine/sdk';
import type { RevTurbineSdkProvider } from '@revturbine/sdk';
import playbook from './revturbine.playbook.json';
import { useMemo } from 'react';
// Typing the object as RevTurbineSdkProvider keeps `status` on the SDK's
// literal union — without it TypeScript widens it to `string` and the
// provider no longer satisfies the interface.
const myProvider: RevTurbineSdkProvider = {
async getPlacement(config) {
const res = await fetch(`/api/placements/${config.slotId}`);
if (!res.ok) return null;
return res.json();
},
async checkEntitlement(handle) {
const res = await fetch(`/api/entitlements/${handle}`);
const data = await res.json();
return { status: data.allowed ? 'allowed' : 'denied', allowed: data.allowed };
},
};
function App() {
const options = useMemo(() => ({
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook },
provider: myProvider,
}), []);
return (
<RevTurbineProvider options={options}>
{/* slots will call myProvider.getPlacement() */}
<YourApp />
</RevTurbineProvider>
);
}

If your provider needs access to the SDK’s init options, use a factory function:

TypeScript
const myProviderFactory = (options) => ({
async getPlacement(config) {
// options.localRuntime, options.user, etc. are available here
return null;
},
});
const options = {
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook },
provider: myProviderFactory,
};

The SDK supports a chain of providers. If the primary provider returns null or throws, the next one is tried:

React
const options = useMemo(() => ({
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook },
provider: apiProvider,
providerFallbacks: [cachedProvider, staticFallback],
}), []);

If all providers fail:

BehaviorResult
'invisible' (default)Slots render nothing
'placeholder'Slots show fallback content

An entitlement check that can’t justify a grant denies — here with { allowed: false, reason: 'sdk_disabled_provider_failure' }. Slot rendering on provider failure is governed separately, by the table above.

The SDK can forward all impressions, interactions, and lifecycle events to your analytics platform (Segment, Heap, Amplitude, PostHog, Mixpanel, or any custom sink). Use createAnalyticsProvider() to create a domain provider that receives every SDK event.

React
import { RevTurbineProvider, createAnalyticsProvider, RuntimeMode } from '@revturbine/sdk';
import playbook from './revturbine.playbook.json';
import { useMemo } from 'react';
function App() {
const options = useMemo(() => {
const analytics = createAnalyticsProvider({
handler: (eventName, properties) => {
// Push to your analytics platform
// `segment` here is your own analytics SDK (Segment, Amplitude, …).
segment.track(eventName, properties);
},
});
return {
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook },
domainProviders: [analytics],
};
}, []);
return (
<RevTurbineProvider options={options}>
{/* All impressions and interactions now flow to Segment */}
<YourApp />
</RevTurbineProvider>
);
}

The analytics provider receives every SDK event, including:

EventWhen
placement_interactionA placement is shown, clicked, or dismissed
placement_dismissedUser closes a placement
placement_convertedUser completes a CTA
placement_snoozedUser clicks “remind me later”
page_viewPage navigation
Trigger eventstrial_expiring, usage_limit_reached, etc.

Each event includes user_id, anonymous_id, session_id, placement_id, interaction_type, url, page_title, and the full event properties.

Only forward the events you care about:

TypeScript
const analytics = createAnalyticsProvider({
handler: (eventName, properties) => {
heap.track(eventName, properties);
},
filter: ['placement_interaction', 'placement_dismissed', 'placement_converted'],
});

Rename events, enrich properties, or drop events with a transform function. Return null to suppress an event.

TypeScript
const analytics = createAnalyticsProvider({
handler: (name, props) => posthog.capture(name, props),
transform: (name, props) => ({
eventName: `revturbine.${name}`,
properties: { ...props, source: 'revturbine-sdk' },
}),
});

Segment:

TypeScript
const analytics = createAnalyticsProvider({
handler: (eventName, properties) => {
segment.track(eventName, properties);
},
});

Heap:

TypeScript
const analytics = createAnalyticsProvider({
handler: (eventName, properties) => {
heap.track(eventName, properties);
},
filter: ['placement_interaction', 'placement_dismissed', 'placement_converted'],
});

Amplitude:

TypeScript
const analytics = createAnalyticsProvider({
handler: (eventName, properties) => {
amplitude.track(eventName, properties);
},
});

PostHog (with prefix):

TypeScript
const analytics = createAnalyticsProvider({
handler: (name, props) => posthog.capture(name, props),
transform: (name, props) => ({
eventName: `revturbine.${name}`,
properties: props,
}),
});