Storage & Versioning
Version Management
Flowsterix uses semantic versioning for flow definitions to handle storage migrations.
version: { major: number, minor: number }
- major: Increment for breaking changes (steps restructured, removed, IDs changed)
- minor: Increment for additive changes (content updates, new optional steps)
What Triggers Version Handling
| Stored Version | Current Version | Action |
|---------------|-----------------|--------|
| 1.0 | 1.0 | Resume from stored step |
| 1.0 | 1.1 | Minor bump - try step ID matching |
| 1.0 | 2.0 | Major bump - call migrate() or reset |
Migration Function
const flow = createFlow({
id: 'onboarding',
version: { major: 2, minor: 0 },
steps: [
{ id: 'welcome', ... },
{ id: 'new-feature', ... }, // New step in v2
{ id: 'finish', ... },
],
migrate: (ctx) => {
// ctx.oldState, ctx.oldVersion, ctx.newVersion
// ctx.stepIdMap: Map<stepId, newIndex>
// ctx.definition: Current flow definition
const oldStepId = ctx.oldState.stepId
if (oldStepId && ctx.stepIdMap.has(oldStepId)) {
return {
...ctx.oldState,
stepIndex: ctx.stepIdMap.get(oldStepId)!,
version: '2.0',
}
}
return null // Reset flow
},
})
Version Mismatch Callback
<TourProvider
flows={flows}
onVersionMismatch={(info) => {
// info.flowId, info.oldVersion, info.newVersion
// info.action: 'continued' | 'migrated' | 'reset'
console.log(`Flow ${info.flowId} migrated: ${info.action}`)
}}
/>
Storage Adapters
Memory Storage
import { MemoryStorageAdapter } from '@flowsterix/core'
<TourProvider
flows={flows}
storageAdapter={new MemoryStorageAdapter()}
/>
No persistence - state resets on page reload.
LocalStorage Adapter (Default)
import { createLocalStorageAdapter } from '@flowsterix/core'
<TourProvider
flows={flows}
storageAdapter={createLocalStorageAdapter()}
storageNamespace="my-app" // Key prefix: "my-app:flowId"
/>
API Storage Adapter
import { createApiStorageAdapter } from '@flowsterix/core'
const apiAdapter = createApiStorageAdapter({
baseUrl: '/api/tour-state',
getHeaders: () => ({
'Authorization': `Bearer ${getToken()}`,
}),
})
<TourProvider flows={flows} storageAdapter={apiAdapter} />
Expected API endpoints:
GET /api/tour-state/{key}- Retrieve statePUT /api/tour-state/{key}- Save stateDELETE /api/tour-state/{key}- Remove state
Custom Storage Adapter
interface StorageAdapter {
get(key: string): StorageSnapshot | null | Promise<StorageSnapshot | null>
set(key: string, value: StorageSnapshot): void | Promise<void>
remove(key: string): void | Promise<void>
subscribe?(listener: () => void): () => void
}
interface StorageSnapshot {
version: string // "major.minor"
value: FlowState
updatedAt: number // timestamp
}
// Example: IndexedDB adapter
const idbAdapter: StorageAdapter = {
async get(key) {
const db = await openDB()
return db.get('tours', key)
},
async set(key, value) {
const db = await openDB()
await db.put('tours', value, key)
},
async remove(key) {
const db = await openDB()
await db.delete('tours', key)
},
}
Event System
EventBus Usage
const { events } = useTour()
// Subscribe
const unsubscribe = events?.on('stepEnter', (payload) => {
console.log('Entered:', payload.currentStep.id)
})
// One-time listener
events?.once('flowComplete', (payload) => {
confetti()
})
// Unsubscribe
unsubscribe?.()
Typed Analytics
<TourProvider
flows={flows}
analytics={{
onFlowStart: ({ flow, state }) => {
analytics.track('tour_started', { flowId: flow.id, version: state.version })
},
onStepEnter: ({ currentStep, reason }) => {
analytics.track('step_viewed', { stepId: currentStep.id, reason })
},
onFlowComplete: ({ flow }) => {
analytics.track('tour_completed', { flowId: flow.id })
},
onFlowCancel: ({ flow, reason }) => {
analytics.track('tour_cancelled', { flowId: flow.id, reason })
},
}}
/>
Debug Mode
const { debugEnabled, toggleDebug } = useTour()
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
toggleDebug()
}
}, [])
Debug mode logs all state transitions and events to console.