Core Concepts
Presage is built around a small set of composable primitives. Understanding these concepts will help you design effective adaptive interfaces.
UserContext
Section titled “UserContext”The UserContext is the single source of truth for who the user is and how they behave. It combines three dimensions:
interface UserContext { traits: UserTraits // Static data you set (role, plan, company) signals: BehavioralSignals // Auto-computed from tracked events maturity: Maturity // Automatic segment: 'new' | 'onboarding' | 'active' | 'power' | 'dormant'}UserTraits
Section titled “UserTraits”Traits are static attributes you set explicitly — typically from your auth system or user profile.
interface UserTraits { userId?: string role?: string plan?: string signupDate?: string company?: string companySize?: number locale?: string [key: string]: unknown // Extensible with any custom field}Set traits with client.identify() or client.updateTraits().
BehavioralSignals
Section titled “BehavioralSignals”Signals are computed automatically from tracked events. You never set them directly.
interface BehavioralSignals { sessionCount: number totalEvents: number featureUsage: Record<string, number> lastSeenAt: string firstSeenAt: string currentSessionDuration: number daysSinceSignup: number clickMap: Record<string, number> customSignals: Record<string, number>}For example, after tracking client.track('feature_used', { featureId: 'export' }) three times, signals.featureUsage.export will equal 3.
Maturity
Section titled “Maturity”Maturity is a computed segment derived from behavioral signals. It classifies users into one of five levels:
| Segment | Meaning |
|---|---|
new | Few sessions (default: 3 or fewer) |
onboarding | Still learning (default: 4-10 sessions) |
active | Regular user beyond onboarding |
power | Uses many features (default: 5+ distinct features) |
dormant | Inactive for too long (default: 14+ days) |
Thresholds are configurable via MaturityConfig.
Adaptation Points
Section titled “Adaptation Points”An adaptation point is a place in your UI where different variants can be shown. It is defined by:
interface AdaptationPoint { id: string // Unique identifier (e.g. 'onboarding') variants: readonly string[] // Available variant IDs defaultVariant: string // Fallback when no rule matches strategy: Strategy // How to select (currently: { type: 'rules' })}In React, the <Adaptive> component creates an adaptation point implicitly from its props and children.
A rule maps a set of conditions to an action. When the conditions match the current UserContext, the action determines what happens at the adaptation point.
interface AdaptationRule { id: string // Unique identifier adaptationId: string // Which adaptation point this rule targets priority: number // Higher priority rules are evaluated first conditions: ConditionGroup // Boolean logic tree action: AdaptationAction // What to do when conditions match}Conditions
Section titled “Conditions”A Condition checks a single field in the user context:
interface Condition { field: string // Dot-path: 'traits.role', 'signals.sessionCount', 'maturity' operator: ConditionOperator // One of 14 operators value: unknown // Value to compare against}Condition Groups
Section titled “Condition Groups”Groups combine conditions with boolean logic:
interface ConditionGroup { all?: (Condition | ConditionGroup)[] // AND — all must match any?: (Condition | ConditionGroup)[] // OR — at least one must match not?: Condition | ConditionGroup // NOT — inverts the result}Groups can be nested arbitrarily deep.
Actions
Section titled “Actions”An action describes what happens when a rule matches:
type AdaptationAction = | { type: 'show'; variantId: string } // Show a specific variant | { type: 'hide' } // Hide the adaptation point entirely | { type: 'reorder'; order: string[] } // Reorder items (e.g. navigation) | { type: 'modify'; props: Record<string, unknown> } // Modify component propsStrategies
Section titled “Strategies”A strategy determines how variants are selected. Currently, only rule-based evaluation is available:
type Strategy = { type: 'rules' }Coming in v0.2: Multi-armed bandit strategies that automatically optimize variant selection based on conversion metrics.
Tracking
Section titled “Tracking”The tracker records user events and feeds the signal computation pipeline.
// Track events with optional propertiesclient.track('feature_used', { featureId: 'dashboard-export' })client.track('click', { elementId: 'nav-settings' })client.track('custom_signal', { signalId: 'engagement', value: 5 })Special event names trigger specific signal computations:
feature_usedwithfeatureId— incrementssignals.featureUsage[featureId]clickwithelementId— incrementssignals.clickMap[elementId]custom_signalwithsignalIdandvalue— incrementssignals.customSignals[signalId]
You can also plug in external analytics adapters (Segment, PostHog, etc.) to forward events.
Persistence
Section titled “Persistence”Persistence keeps traits and signals across page reloads. Two built-in drivers are provided:
createLocalStorageDriver(prefix)— Stores data inlocalStorageunder a namespaced keycreateMemoryDriver()— In-memory storage for SSR and testing
When a persistence driver is configured, the client synchronously reads cached data on initialization — this prevents a flash of default content (anti-FOUC).
Resolution Flow
Section titled “Resolution Flow”When a component requests a variant, here is what happens:
- The component declares an
AdaptationPoint(id, variants, default) - The rules engine filters rules by
adaptationId - Rules are evaluated in priority order (highest first)
- The first rule whose conditions match returns its action
- The action determines which variant renders
- If no rule matches, the
defaultVariantis used - An
presage:impressionevent is automatically tracked
This entire flow is synchronous and happens in a single render cycle.