# FeatureDrop FeatureDrop is an open-source product adoption toolkit that provides auto-expiring "New" badges, changelogs, product tours, onboarding checklists, spotlights, and feedback widgets. It runs entirely from your own codebase with a JSON manifest you control, requiring no external services or vendor accounts. The core library is under 3 kB gzipped with zero runtime dependencies. The toolkit supports multiple frameworks including React, Vue, Svelte, SolidJS, Preact, Angular, and vanilla JavaScript. Features automatically expire based on timestamps, and the system uses a dual-layer model combining server-side watermarks with client-side dismissals for optimal user experience across devices. All components are headless-capable via render props, making them compatible with any design system. ## Core API ### createManifest - Create Feature Manifest Creates an immutable feature manifest from an array of feature entries. Each entry defines a feature with release dates, expiration dates, targeting rules, and display options. ```typescript import { createManifest } from 'featuredrop' const features = createManifest([ { id: 'dark-mode', label: 'Dark Mode', description: 'Full dark theme support across every surface.', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z', type: 'feature', priority: 'high', category: 'ui', sidebarKey: '/settings', cta: { label: 'Try it', url: '/settings/appearance' }, image: '/images/dark-mode.png', audience: { plan: ['pro', 'enterprise'] } }, { id: 'ai-assistant', label: 'AI Assistant', description: 'Get intelligent suggestions powered by AI.', releasedAt: '2024-03-15T00:00:00Z', showNewUntil: '2024-04-15T00:00:00Z', type: 'feature', priority: 'critical', publishAt: '2024-03-20T00:00:00Z', // Scheduled publishing flagKey: 'ai-assistant-enabled', // Feature flag gating dependsOn: { seen: ['dark-mode'] } // Progressive disclosure } ]) ``` ### isNew - Check Feature Newness Determines if a single feature should display as "new" based on watermark, dismissals, time windows, audience rules, and dependencies. ```typescript import { isNew, MemoryAdapter } from 'featuredrop' const storage = new MemoryAdapter({ watermark: '2024-02-01T00:00:00Z' }) const feature = { id: 'dark-mode', label: 'Dark Mode', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z' } const watermark = storage.getWatermark() const dismissedIds = storage.getDismissedIds() const userContext = { plan: 'pro', role: 'admin', region: 'us' } const showAsNew = isNew( feature, watermark, dismissedIds, new Date(), userContext ) // Returns: true (feature released after watermark, not dismissed, within time window) ``` ### getNewFeatures - Get All New Features Returns all features currently showing as "new" for the user, applying all filtering logic. ```typescript import { getNewFeatures, createManifest, LocalStorageAdapter } from 'featuredrop' const manifest = createManifest([ { id: 'feature-1', label: 'Feature 1', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z' }, { id: 'feature-2', label: 'Feature 2', releasedAt: '2024-03-15T00:00:00Z', showNewUntil: '2024-04-15T00:00:00Z' } ]) const storage = new LocalStorageAdapter({ watermark: '2024-02-01T00:00:00Z' }) const userContext = { plan: 'pro' } const newFeatures = getNewFeatures(manifest, storage, new Date(), userContext) // Returns: Array of FeatureEntry objects that are currently new ``` ### getNewFeatureCount - Count New Features Returns the count of features currently showing as "new". ```typescript import { getNewFeatureCount, createManifest, MemoryAdapter } from 'featuredrop' const manifest = createManifest([ { id: 'f1', label: 'Feature 1', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z' }, { id: 'f2', label: 'Feature 2', releasedAt: '2024-03-15T00:00:00Z', showNewUntil: '2024-04-15T00:00:00Z' } ]) const storage = new MemoryAdapter() const count = getNewFeatureCount(manifest, storage) // Returns: 2 ``` ### hasNewFeature - Check Sidebar Key Checks if a specific navigation/sidebar key has any associated new features. ```typescript import { hasNewFeature, createManifest, LocalStorageAdapter } from 'featuredrop' const manifest = createManifest([ { id: 'dark-mode', label: 'Dark Mode', sidebarKey: '/settings', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z' } ]) const storage = new LocalStorageAdapter() const hasNew = hasNewFeature(manifest, '/settings', storage) // Returns: true if any new feature has sidebarKey === '/settings' ``` ### matchesAudience - Audience Targeting Evaluates whether a user matches audience targeting rules. Fields use AND logic between them, OR logic within each field's array. ```typescript import { matchesAudience } from 'featuredrop' const audience = { plan: ['pro', 'enterprise'], role: ['admin', 'editor'], region: ['us', 'eu'] } const userContext = { plan: 'pro', role: 'admin', region: 'us' } const matches = matchesAudience(audience, userContext) // Returns: true (user matches all criteria) const anotherUser = { plan: 'free', role: 'viewer', region: 'us' } const matches2 = matchesAudience(audience, anotherUser) // Returns: false (plan doesn't match) ``` ## Storage Adapters ### LocalStorageAdapter - Browser Storage Browser-based storage adapter using localStorage for dismissals and server-provided watermark for cross-device sync. ```typescript import { LocalStorageAdapter } from 'featuredrop' const storage = new LocalStorageAdapter({ prefix: 'myapp', // Key prefix (default: 'featuredrop') watermark: '2024-02-01T00:00:00Z', // Server-side watermark from user profile onDismissAll: async (now) => { // Sync watermark to server when user marks all as read await fetch('/api/user/watermark', { method: 'POST', body: JSON.stringify({ watermark: now.toISOString() }) }) } }) // Usage const watermark = storage.getWatermark() // Returns server watermark const dismissed = storage.getDismissedIds() // Returns Set from localStorage storage.dismiss('feature-id') // Add to localStorage dismissed set await storage.dismissAll(new Date()) // Clear localStorage + call server callback ``` ### MemoryAdapter - In-Memory Storage In-memory storage for testing, SSR, or environments without persistent storage. ```typescript import { MemoryAdapter } from 'featuredrop' const storage = new MemoryAdapter({ watermark: '2024-02-01T00:00:00Z' // Optional initial watermark }) storage.dismiss('feature-1') storage.dismiss('feature-2') const dismissed = storage.getDismissedIds() // Returns: Set { 'feature-1', 'feature-2' } await storage.dismissAll(new Date()) // Watermark updated, dismissed set cleared ``` ### IndexedDBAdapter - Offline-First Storage IndexedDB-based adapter for offline-first PWAs with larger storage capacity. ```typescript import { IndexedDBAdapter } from 'featuredrop/adapters' const storage = new IndexedDBAdapter({ dbName: 'myapp-features', watermark: '2024-02-01T00:00:00Z' }) ``` ### RemoteAdapter - Server-Backed Storage Server-backed adapter with retry logic and circuit-breaker pattern. ```typescript import { RemoteAdapter } from 'featuredrop/adapters' const storage = new RemoteAdapter({ userId: 'user-123', baseUrl: 'https://api.myapp.com/featuredrop', headers: { 'Authorization': 'Bearer token' }, retryAttempts: 3, circuitBreakerThreshold: 5 }) ``` ### HybridAdapter - Local + Remote Sync Combines local storage with batched remote sync for optimal UX and cross-device consistency. ```typescript import { HybridAdapter } from 'featuredrop/adapters' const storage = new HybridAdapter({ local: new LocalStorageAdapter(), remote: { baseUrl: 'https://api.myapp.com/featuredrop', userId: 'user-123' }, flushInterval: 5000, // Batch flush every 5 seconds maxBatchSize: 10 }) ``` ### Database Adapters Server-side adapters for various databases. ```typescript import { PostgresAdapter, RedisAdapter, MongoAdapter } from 'featuredrop/adapters' // PostgreSQL const pgAdapter = new PostgresAdapter({ userId: 'user-123', query: async (sql, params) => { const result = await pool.query(sql, params) return { rows: result.rows } }, tableName: 'featuredrop_state' }) // Redis const redisAdapter = new RedisAdapter({ userId: 'user-123', client: redisClient, keyPrefix: 'featuredrop:' }) // MongoDB const mongoAdapter = new MongoAdapter({ userId: 'user-123', collection: db.collection('featuredrop') }) ``` ## React Integration ### FeatureDropProvider - Context Provider Wraps your app to provide feature discovery state to all child components. ```tsx import { FeatureDropProvider } from 'featuredrop/react' import { LocalStorageAdapter, createManifest } from 'featuredrop' const features = createManifest([ { id: 'dark-mode', label: 'Dark Mode', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z' } ]) function App() { return ( analytics.track('feature_seen', { id: f.id }), onFeatureDismissed: (f) => analytics.track('feature_dismissed', { id: f.id }), onFeatureClicked: (f) => analytics.track('feature_clicked', { id: f.id }), onWidgetOpened: () => analytics.track('changelog_opened'), onWidgetClosed: () => analytics.track('changelog_closed'), onAllDismissed: () => analytics.track('all_features_dismissed') }} throttle={{ maxToastsPerSession: 3, minTimeBetweenModals: 30000, sessionCooldown: 5000, respectDoNotDisturb: true }} > ) } ``` ### NewBadge - Auto-Expiring Badge Component Displays "New" indicators that automatically expire based on the manifest configuration. ```tsx import { NewBadge, useNewFeature } from 'featuredrop/react' // Simple usage with sidebar key function SidebarNav() { return ( ) } // Controlled with hook function ControlledBadge() { const { isNew, dismiss } = useNewFeature('dark-mode') return ( ) } // Render prop for custom UI function CustomBadge() { return ( {({ isNew }) => isNew ? NEW : null} ) } ``` ### ChangelogWidget - Changelog Slide-Out Full-featured changelog widget with trigger button, unread count, and feed display. ```tsx import { ChangelogWidget } from 'featuredrop/react' // Default usage function Header() { return ( ) } // Custom entry renderer function CustomChangelog() { return ( (

{feature.label}

{feature.description}

{feature.cta && ( {feature.cta.label} )}
)} renderTrigger={({ count, onClick }) => ( )} /> ) } // Fully headless function HeadlessChangelog() { return ( {({ isOpen, toggle, features, count, dismiss, dismissAll }) => ( )} ) } ``` ### Tour - Guided Product Tours Multi-step guided tours with DOM targeting, keyboard navigation, and persistence. ```tsx import { Tour, useTour } from 'featuredrop/react' function OnboardingTour() { const steps = [ { id: 'welcome', target: '#dashboard-header', title: 'Welcome to the Dashboard', content: 'This is your command center for all activities.', placement: 'bottom' }, { id: 'sidebar', target: '.sidebar-nav', title: 'Navigation', content: 'Use the sidebar to navigate between different sections.', placement: 'right', highlightTarget: true }, { id: 'settings', target: '#settings-button', title: 'Settings', content: 'Customize your experience here.', placement: 'left', advanceOn: { selector: '#settings-button', event: 'click' } } ] return ( console.log('Tour started')} onTourCompleted={() => console.log('Tour completed')} onTourSkipped={(stepId) => console.log('Skipped at:', stepId)} onStepViewed={(step, index) => console.log('Viewing step:', index)} /> ) } // Programmatic control with hook function TourController() { const tour = useTour('onboarding') return (

Step {tour.stepIndex + 1} of {tour.totalSteps}

) } ``` ### Checklist - Onboarding Checklists Task checklists with progress tracking and completion states. ```tsx import { Checklist, useChecklist } from 'featuredrop/react' function OnboardingChecklist() { const tasks = [ { id: 'profile', label: 'Complete your profile', description: 'Add your name and avatar' }, { id: 'team', label: 'Invite team members', description: 'Collaboration is better together' }, { id: 'project', label: 'Create your first project', description: 'Get started with a new project' }, { id: 'integration', label: 'Connect an integration', description: 'Link your favorite tools' } ] return ( console.log('Completed:', taskId)} onAllComplete={() => console.log('All tasks complete!')} /> ) } // Programmatic control function ChecklistController() { const { tasks, completedCount, totalCount, completeTask, isComplete } = useChecklist('onboarding') return (

Progress: {completedCount}/{totalCount}

{tasks.map(task => (
completeTask(task.id)} /> {task.label}
))}
) } ``` ### Spotlight and SpotlightChain - Feature Highlights Pulsing beacons and chained spotlight walkthroughs. ```tsx import { Spotlight, SpotlightChain } from 'featuredrop/react' // Single spotlight function FeatureSpotlight() { return ( ) } // Chained spotlight tour function SpotlightTour() { const steps = [ { target: '#search', title: 'Search', content: 'Find anything instantly' }, { target: '#filters', title: 'Filters', content: 'Narrow down results' }, { target: '#export', title: 'Export', content: 'Download your data' } ] return ( console.log('Spotlight chain complete')} /> ) } ``` ### FeedbackWidget and Survey - User Feedback Collect user feedback and run NPS/CSAT surveys. ```tsx import { FeedbackWidget, Survey } from 'featuredrop/react' // Feedback widget function FeedbackButton() { return ( { await fetch('/api/feedback', { method: 'POST', body: JSON.stringify(payload) }) }} /> ) } // NPS Survey function NPSSurvey() { return ( { await fetch('/api/survey', { method: 'POST', body: JSON.stringify(response) }) }} /> ) } ``` ## Headless Hooks ### useChangelog - Changelog Data Hook Full changelog data and actions without any UI components. ```tsx import { useChangelog } from 'featuredrop/react/hooks' function CustomChangelog() { const { features, // All features from manifest newFeatures, // Only new/unread features newCount, // Count of new features newFeaturesSorted, // Sorted by priority then date dismiss, // Dismiss single feature dismissAll, // Mark all as read isNew, // Check if sidebar key is new markAllSeen, // Advance watermark getByCategory // Filter by category } = useChangelog() const uiFeatures = getByCategory('ui') return (

What's New ({newCount})

{newFeaturesSorted.map(feature => (

{feature.label}

{feature.description}

))}
) } ``` ### useFeatureDrop - Full Context Access Complete access to all FeatureDrop context values and actions. ```tsx import { useFeatureDrop } from 'featuredrop/react/hooks' function FeatureDropConsumer() { const { manifest, newFeatures, newCount, dismiss, dismissAll, isNew, getFeature, quietMode, setQuietMode, markFeatureSeen, markFeatureClicked, canShowModal, canShowTour, locale, direction, translations } = useFeatureDrop() return (

{translations.whatsNewTitle}: {newCount}

) } ``` ### useNewFeature - Single Feature State Check and control a single feature's "new" state. ```tsx import { useNewFeature } from 'featuredrop/react/hooks' function FeatureIndicator({ featureId }) { const { isNew, feature, dismiss } = useNewFeature(featureId) if (!isNew) return null return (
{feature.label}
) } ``` ### useTabNotification - Browser Tab Badge Update browser tab title with unread count. ```tsx import { useTabNotification } from 'featuredrop/react/hooks' function App() { useTabNotification({ enabled: true, template: '({count}) My App', // Shows "(3) My App" when 3 new features originalTitle: 'My App' }) return } ``` ## Notification Bridges ### SlackBridge - Slack Notifications Send feature announcements to Slack channels. ```typescript import { SlackBridge } from 'featuredrop/bridges' const feature = { id: 'dark-mode', label: 'Dark Mode', description: 'Full dark theme support.', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z', url: 'https://docs.myapp.com/dark-mode' } await SlackBridge.notify(feature, { webhookUrl: process.env.SLACK_WEBHOOK_URL, channel: '#product-updates', username: 'FeatureDrop Bot', iconEmoji: ':rocket:', timeoutMs: 5000, maxRetries: 3 }) ``` ### DiscordBridge - Discord Notifications Send feature announcements to Discord channels. ```typescript import { DiscordBridge } from 'featuredrop/bridges' await DiscordBridge.notify(feature, { webhookUrl: process.env.DISCORD_WEBHOOK_URL, username: 'Product Updates', avatarUrl: 'https://myapp.com/bot-avatar.png' }) ``` ### WebhookBridge - Generic Webhooks Send feature data to any webhook endpoint. ```typescript import { WebhookBridge } from 'featuredrop/bridges' await WebhookBridge.post(feature, { url: 'https://api.myapp.com/webhooks/features', event: 'feature.published', headers: { 'Authorization': 'Bearer token' }, body: { customField: 'value' } }) ``` ### EmailDigestGenerator - Email HTML Generator Generate HTML email digests from feature lists. ```typescript import { EmailDigestGenerator } from 'featuredrop/bridges' const features = [ { id: 'f1', label: 'Dark Mode', description: 'Full theme support', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z' }, { id: 'f2', label: 'AI Assistant', description: 'Smart suggestions', releasedAt: '2024-03-15T00:00:00Z', showNewUntil: '2024-04-15T00:00:00Z' } ] const html = EmailDigestGenerator.generate(features, { title: 'Weekly Product Updates', intro: 'Here are the latest features we shipped this week:', productName: 'MyApp', template: 'default' // 'default' | 'minimal' }) // Send via your email provider await sendEmail({ to: 'users@myapp.com', subject: 'Weekly Product Updates', html }) ``` ### RSSFeedGenerator - RSS Feed Generator Generate RSS 2.0 feeds from feature manifests. ```typescript import { RSSFeedGenerator } from 'featuredrop/bridges' const rss = RSSFeedGenerator.generate(manifest, { title: 'MyApp Changelog', link: 'https://myapp.com/changelog', description: 'Latest product updates and features' }) // Serve as RSS endpoint app.get('/changelog.rss', (req, res) => { res.type('application/rss+xml').send(rss) }) ``` ## Feature Flags ### createFlagBridge - Custom Flag Bridge Create a custom feature flag bridge for any flag service. ```typescript import { createFlagBridge } from 'featuredrop/flags' const flagBridge = createFlagBridge({ isEnabled: (flagKey, userContext) => { // Your custom flag evaluation logic return myFlagService.evaluate(flagKey, { userId: userContext?.traits?.id, plan: userContext?.plan }) }, defaultValue: false, onError: (error, context) => { console.error(`Flag evaluation failed: ${context.flagKey}`, error) } }) ``` ### LaunchDarklyBridge - LaunchDarkly Integration Integrate with LaunchDarkly for feature flag evaluation. ```typescript import { LaunchDarklyBridge } from 'featuredrop/flags' import * as LaunchDarkly from 'launchdarkly-node-server-sdk' const ldClient = LaunchDarkly.init('sdk-key') await ldClient.waitForInitialization() const flagBridge = new LaunchDarklyBridge(ldClient, { defaultValue: false, userResolver: (userContext) => ({ key: userContext?.traits?.id ?? 'anonymous', custom: { plan: userContext?.plan, role: userContext?.role } }) }) // Use in provider ``` ### PostHogBridge - PostHog Integration Integrate with PostHog for feature flag evaluation. ```typescript import { PostHogBridge } from 'featuredrop/flags' import { PostHog } from 'posthog-node' const posthog = new PostHog('api-key') const flagBridge = new PostHogBridge(posthog, { defaultValue: false, distinctIdResolver: (userContext) => userContext?.traits?.id }) ``` ## Schema Validation ### validateManifest - Manifest Validation Validate manifest structure with detailed error reporting. ```typescript import { validateManifest } from 'featuredrop/schema' const manifest = [ { id: 'dark-mode', label: 'Dark Mode', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z' }, { id: 'dark-mode', // Duplicate ID - will error label: 'Another Feature', releasedAt: 'invalid-date', // Invalid date - will error showNewUntil: '2024-04-01T00:00:00Z' } ] const result = validateManifest(manifest) if (!result.valid) { result.errors.forEach(error => { console.error(`${error.path}: ${error.message} (${error.code})`) // [1].id: Duplicate feature id "dark-mode" (duplicate_id) // [1].releasedAt: must be a valid date (invalid_date) }) } ``` ## CI Integration ### diffManifest - Compare Manifests Compare two manifest versions to identify changes. ```typescript import { diffManifest, generateChangelogDiff, generateChangelogDiffMarkdown } from 'featuredrop/ci' const before = [ { id: 'f1', label: 'Feature 1', releasedAt: '2024-01-01T00:00:00Z', showNewUntil: '2024-02-01T00:00:00Z' } ] const after = [ { id: 'f1', label: 'Feature 1 Updated', releasedAt: '2024-01-01T00:00:00Z', showNewUntil: '2024-02-01T00:00:00Z' }, { id: 'f2', label: 'Feature 2', releasedAt: '2024-03-01T00:00:00Z', showNewUntil: '2024-04-01T00:00:00Z' } ] const diff = diffManifest(before, after) // { added: [f2], removed: [], changed: [{ id: 'f1', changedFields: ['label'] }] } const summary = generateChangelogDiff(diff, { includeFieldChanges: true }) // "Added: Feature 2. Changed: Feature 1 Updated [label]" const markdown = generateChangelogDiffMarkdown(diff, { includeFieldChanges: true }) // Formatted markdown for PR comments ``` ### validateManifestForCI - CI Validation Validate manifests in CI pipelines. ```typescript import { validateManifestForCI } from 'featuredrop/ci' import fs from 'fs' const manifest = JSON.parse(fs.readFileSync('features.json', 'utf-8')) const result = validateManifestForCI(manifest) if (!result.valid) { console.error('Manifest validation failed:') result.errors.forEach(e => console.error(` ${e.path}: ${e.message}`)) process.exit(1) } console.log('Manifest is valid!') ``` ## CLI Commands ### Basic CLI Usage Command-line tools for manifest management and validation. ```bash # Initialize a new manifest npx featuredrop init # Add a new feature entry npx featuredrop add --label "Dark Mode" --category ui --type feature # Validate manifest npx featuredrop validate # Run doctor checks (security + best practices) npx featuredrop doctor # Show manifest statistics npx featuredrop stats # Build manifest from markdown files npx featuredrop build --pattern "features/**/*.md" --out featuredrop.manifest.json # Generate RSS feed npx featuredrop generate-rss --out changelog.rss.xml # Generate markdown changelog npx featuredrop generate-changelog --out CHANGELOG.generated.md # Migrate from other platforms npx featuredrop migrate --from beamer --input beamer-export.json --out features.json npx featuredrop migrate --from headway --input headway-export.json npx featuredrop migrate --from pendo --input pendo-export.json ``` ## Summary FeatureDrop provides a complete toolkit for product adoption and feature discovery, replacing expensive SaaS solutions like Beamer, Pendo, and Appcues with a free, self-hosted alternative. The library's core strength lies in its manifest-driven approach where features are defined declaratively with release dates, expiration windows, and targeting rules, enabling automatic badge expiration without manual intervention. The integration patterns support both simple drop-in components and fully headless architectures. For React applications, components like `ChangelogWidget`, `Tour`, `Checklist`, and `NewBadge` provide immediate value with minimal configuration, while hooks like `useChangelog` and `useFeatureDrop` enable complete UI customization with shadcn/ui or any design system. The storage adapter pattern allows flexible persistence from simple localStorage for single-device apps to database-backed adapters for enterprise deployments with cross-device sync. Notification bridges enable automated announcements to Slack, Discord, email, and webhooks, while CI utilities support manifest validation and diff generation in pull request workflows.