Persistence
Persistence allows Presage to remember user traits and behavioral signals across page reloads and browser sessions.
Built-in Drivers
Section titled “Built-in Drivers”localStorage Driver
Section titled “localStorage Driver”The recommended driver for browser-based SaaS applications:
import { createAdaptiveClient, createLocalStorageDriver } from '@presage-kit/core'
const client = createAdaptiveClient({ persistence: { driver: createLocalStorageDriver('my-app'), }, // ...rules})Data is stored in localStorage under namespaced keys:
my-app:traits— User traits (role, plan, etc.)my-app:signals— Behavioral signals
The prefix prevents collisions with other libraries or multiple Presage instances on the same domain.
Memory Driver
Section titled “Memory Driver”An in-memory driver for SSR environments and testing:
import { createAdaptiveClient, createMemoryDriver } from '@presage-kit/core'
const client = createAdaptiveClient({ persistence: { driver: createMemoryDriver(), }, // ...rules})Data lives only in memory and is lost when the process ends. This is useful for:
- Server-side rendering —
localStorageis not available on the server - Unit tests — No side effects between test runs
- Storybook — Isolated stories that don’t pollute browser storage
What Gets Persisted
Section titled “What Gets Persisted”| Key | Data | When updated |
|---|---|---|
traits | UserTraits object | On identify(), updateTraits(), and any state change |
signals | BehavioralSignals object | After signal recomputation (100ms after track()) |
Creating Custom Drivers
Section titled “Creating Custom Drivers”Implement the StorageDriver interface to store data anywhere:
interface StorageDriver { get<T>(key: string): T | null set<T>(key: string, value: T): void remove(key: string): void clear(): void}Example: Session Storage Driver
Section titled “Example: Session Storage Driver”import type { StorageDriver } from '@presage-kit/core'
function createSessionStorageDriver(prefix: string): StorageDriver { function prefixed(key: string): string { return `${prefix}:${key}` }
return { get<T>(key: string): T | null { const raw = sessionStorage.getItem(prefixed(key)) if (raw === null) return null try { return JSON.parse(raw) as T } catch { return null } },
set<T>(key: string, value: T): void { sessionStorage.setItem(prefixed(key), JSON.stringify(value)) },
remove(key: string): void { sessionStorage.removeItem(prefixed(key)) },
clear(): void { const toRemove: string[] = [] for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i) if (key?.startsWith(`${prefix}:`)) { toRemove.push(key) } } for (const key of toRemove) { sessionStorage.removeItem(key) } }, }}Example: Cookie Driver
Section titled “Example: Cookie Driver”import type { StorageDriver } from '@presage-kit/core'
function createCookieDriver(prefix: string): StorageDriver { return { get<T>(key: string): T | null { const name = `${prefix}_${key}` const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)) if (!match) return null try { return JSON.parse(decodeURIComponent(match[1])) as T } catch { return null } },
set<T>(key: string, value: T): void { const name = `${prefix}_${key}` const encoded = encodeURIComponent(JSON.stringify(value)) document.cookie = `${name}=${encoded}; path=/; max-age=31536000; SameSite=Lax` },
remove(key: string): void { const name = `${prefix}_${key}` document.cookie = `${name}=; path=/; max-age=0` },
clear(): void { // Clear all cookies with the prefix const cookies = document.cookie.split('; ') for (const cookie of cookies) { const name = cookie.split('=')[0] if (name.startsWith(`${prefix}_`)) { document.cookie = `${name}=; path=/; max-age=0` } } }, }}Anti-FOUC: Synchronous Initialization
Section titled “Anti-FOUC: Synchronous Initialization”A common problem with persisted state is the flash of unconditioned content (FOUC): the UI renders with default values, then flashes to the personalized state once data loads.
Presage avoids this because the StorageDriver.get() method is synchronous. When createAdaptiveClient() is called:
- The driver reads cached traits and signals synchronously
- Maturity is computed from cached signals
- The initial
UserContextis ready before the first render - Components receive the correct variant from the very first paint
// This is synchronous — no loading state neededconst client = createAdaptiveClient({ persistence: { driver: createLocalStorageDriver('my-app'), }, rules: [...],})
// client.getContext() already has cached dataconsole.log(client.getContext().traits) // { userId: 'user-123', role: 'admin', ... }This design is intentional. Both localStorage and sessionStorage are synchronous APIs, which makes them ideal for anti-FOUC. If you build a custom async driver (e.g., IndexedDB), you will need to handle the loading state yourself.
No Persistence
Section titled “No Persistence”If you do not configure a persistence driver, Presage still works — but the user context resets on every page load. This can be useful for:
- Prototyping and demos
- Server-rendered applications where context comes from the server
- Scenarios where you always
identify()from your auth system on mount
// No persistence — context resets on reloadconst client = createAdaptiveClient({ rules: [...],})