Skip to content

Provider Architecture

The SDK uses a provider pattern to decouple business logic from data sources. A provider is a plain object that implements one or more domain methods — placement resolution, entitlement checks, placement type persistence, and user identification.

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 FixedSurfaceSlot, AccessGateSurfaceSlot, MessageSurfaceSlot, and the headless API.

const analyticsProvider: RevTurbineSdkProvider = {
async getPlacement(config) {
// config.slotId — the slot requesting a placement
// config.surfaceType — the surface type (button, modal, banner, etc.)
// config.entitlementHandle — for access gate slots
// Example: fetch from your own decisioning API
const res = await fetch(`/api/placements/${config.slotId}`);
if (!res.ok) return null;
return res.json();
},
};

Input — RevTurbinePlacementRequestConfig:

FieldTypeDescription
slotIdstring?Slot identifier
surfaceTypestring?Surface 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 AccessGateSurfaceSlot and sdk.checkEntitlement().

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', 'denied', 'usage_capped', etc.
allowedbooleanWhether access is granted

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

const storageProvider: RevTurbineSdkProvider = {
async persistPlacementTypes(types) {
// types — array of { id, label, description, surfaceType, 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.

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

Pass your provider via the provider option:

import { RevTurbineProvider, RuntimeMode } from '@revturbine/sdk';
import type { RevTurbineSdkProvider } from '@revturbine/sdk';
import playbook from './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:

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:

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

Entitlement checks are fail-closed: a check that can’t produce an affirmative grant denies ({ allowed: false }). This governs <Slot>/placement rendering-on-failure separately — slots still render nothing.

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.

import { RevTurbineProvider, createAnalyticsProvider, RuntimeMode } from '@revturbine/sdk';
import playbook from './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:

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.

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

Segment:

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

Heap:

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

Amplitude:

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

PostHog (with prefix):

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