Agent Skills: Storage & Versioning

Storage adapters, version management, migrations, and persistence patterns for Flowsterix. Use when configuring tour state persistence, implementing custom storage adapters, handling version migrations, or managing flow state across sessions.

UncategorizedID: kvngrf/flowsterix/storage-and-versioning

Install this agent skill to your local

pnpm dlx add-skill https://github.com/kvngrf/flowsterix/tree/HEAD/packages/react/skills/storage-and-versioning

Skill Files

Browse the full folder contents for storage-and-versioning.

Download Skill

Loading file tree…

packages/react/skills/storage-and-versioning/SKILL.md

Skill Metadata

Name
storage-and-versioning
Description
Storage adapters, version management, migrations, and persistence patterns for Flowsterix. Use when configuring tour state persistence, implementing custom storage adapters, handling version migrations, or managing flow state across sessions.

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 state
  • PUT /api/tour-state/{key} - Save state
  • DELETE /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.