npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@mgvdev/fanion-adonisjs

v0.1.4

Published

AdonisJS adapter for Fanion feature flagging library

Readme

AdonisJS Fanion 🏁

AdonisJS adapter for the Fanion feature flagging library

npm version npm downloads license

AdonisJS Fanion is the official AdonisJS adapter for the Fanion feature flagging library. It provides a seamless integration with AdonisJS applications, allowing you to control feature rollouts, conduct A/B tests, and manage application behavior dynamically.

Features

  • 🚀 AdonisJS Native - Built specifically for AdonisJS v6 with full IoC support
  • 🔧 TypeScript First - Complete type safety and IntelliSense support
  • 🏪 Multiple Storage Options - Memory, database, or custom storage providers
  • 🎯 Context-Aware - Automatic HTTP context integration
  • 🛡️ Middleware Support - Protect routes with feature flags
  • 🎨 Decorators - Use decorators in controllers for feature gating
  • 📊 A/B Testing - Built-in utilities for A/B testing and gradual rollouts
  • 🌍 Environment Support - Environment-based feature flags

Installation

npm i @mgvdev/fanion-adonisjs

Next, configure the package using the configure command:

node ace configure @mgvdev/fanion-adonisjs

This will:

  • Create a config/fanion.ts configuration file
  • Register the service provider
  • Set up environment variables
  • Optionally install database drivers

Quick Start

Basic Usage

// Define feature flags in config/fanion.ts
export default defineConfig({
  features: [
    {
      name: 'new-dashboard',
      description: 'Enable new dashboard design',
      check: () => true,
    },
    {
      name: 'beta-features',
      description: 'Enable beta features for specific users',
      check: (context) => context.user?.isBetaUser === true,
    },
  ],
})
// Use in controllers
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import type { FanionService } from '@fanion/adonisjs/types'

export default class DashboardController {
  @inject()
  async index({ response, auth }: HttpContext, fanion: FanionService) {
    const showNewDashboard = await fanion.activeForRequest('new-dashboard', { auth })

    if (showNewDashboard) {
      return response.ok({ message: 'New dashboard!' })
    }

    return response.ok({ message: 'Classic dashboard' })
  }
}

Using Middleware

// routes/web.ts
import router from '@adonisjs/core/services/router'
import { featureFlag } from '@fanion/adonisjs'

// Protect entire route
router.get('/beta', [featureFlag('beta-features')]).use(async ({ response }) => {
  return response.ok({ message: 'Beta feature accessed!' })
})

// Redirect when disabled
router.get('/new-ui', [featureFlagWithRedirect('new-ui', '/old-ui')]).use(async ({ response }) => {
  return response.ok({ message: 'New UI!' })
})

Using Decorators

import { requireFeature } from '@fanion/adonisjs'

export default class AdminController {
  @requireFeature('admin-panel')
  async index({ response }: HttpContext) {
    return response.ok({ message: 'Admin panel' })
  }

  @requireFeature('super-admin', {
    onDisabled: 'redirect',
    redirectTo: '/dashboard',
  })
  async users({ response }: HttpContext) {
    return response.ok({ users: [] })
  }
}

Configuration

Basic Configuration

// config/fanion.ts
import { defineConfig } from '@fanion/adonisjs/types'
import env from '#start/env'

export default defineConfig({
  // Enable debug logging
  debug: env.get('NODE_ENV') === 'development',

  // Auto-initialize storage on app start
  autoInit: true,

  // Default context provider
  defaultContextProvider: () => ({
    environment: env.get('NODE_ENV'),
    appVersion: env.get('APP_VERSION', '1.0.0'),
  }),
})

Database Storage

// config/fanion.ts
import { defineConfig } from '@fanion/adonisjs/types'
import db from '@adonisjs/lucid/services/db'

export default defineConfig({
  storageDriver: {
    type: 'knex',
    config: {
      connection: db.connection(),
      tableName: 'feature_flags',
      featureNameColumn: 'feature_name',
      valueColumn: 'value',
    },
  },
})

Feature Flag Definitions

// config/fanion.ts
export default defineConfig({
  features: [
    // Simple boolean flag (stored in database)
    {
      name: 'maintenance-mode',
      description: 'Enable maintenance mode',
      store: true,
      defaultValue: false,
    },

    // User-based flag
    {
      name: 'premium-features',
      description: 'Enable premium features',
      check: (context) => {
        return context.user?.plan === 'premium' && context.user?.verified
      },
    },

    // Percentage-based rollout
    {
      name: 'new-feature',
      description: 'Roll out new feature to 25% of users',
      check: (context) => {
        const userId = context.user?.id || 0
        return userId % 100 < 25
      },
    },

    // Environment-based flag
    {
      name: 'debug-mode',
      description: 'Enable debug mode in development',
      check: () => process.env.NODE_ENV === 'development',
    },
  ],
})

Advanced Usage

A/B Testing

import { ABTesting } from '@fanion/adonisjs'

// In config/fanion.ts
export default defineConfig({
  features: [
    {
      name: 'checkout-variant-a',
      description: 'A/B test for checkout flow - Variant A',
      check: ABTesting.createABTest('checkout', 50, (ctx) => ctx.user?.id || 0),
    },
  ],
})

// In your controller
export default class CheckoutController {
  @inject()
  async show({ response }: HttpContext, fanion: FanionService) {
    const showVariantA = await fanion.active('checkout-variant-a', { user: auth.user })

    return response.ok({
      variant: showVariantA ? 'A' : 'B',
      checkoutFlow: showVariantA ? 'simplified' : 'standard',
    })
  }
}

Custom Context

// Create custom context for feature evaluation
import { createFeatureContext } from '@fanion/adonisjs'

export default class ApiController {
  @inject()
  async data({ request, auth }: HttpContext, fanion: FanionService) {
    const context = createFeatureContext(
      { auth },
      {
        apiVersion: request.header('api-version'),
        clientType: request.header('client-type'),
        country: request.header('cf-ipcountry'), // Cloudflare country header
      }
    )

    const features = await fanion.activeMany(
      ['enhanced-api', 'geo-restrictions', 'rate-limiting'],
      context
    )

    return response.ok({ features })
  }
}

Environment-Based Flags

import { EnvironmentFlags } from '@fanion/adonisjs'

export default defineConfig({
  features: [
    {
      name: 'dev-tools',
      check: EnvironmentFlags.developmentOnly(),
    },
    {
      name: 'analytics',
      check: EnvironmentFlags.productionOnly(),
    },
    {
      name: 'staging-banner',
      check: EnvironmentFlags.createEnvironmentFlag(['staging', 'development']),
    },
  ],
})

Multiple Flag Checks

export default class DashboardController {
  @inject()
  async index({ auth }: HttpContext, fanion: FanionService) {
    const flags = await fanion.activeMany(
      ['new-dashboard', 'advanced-analytics', 'export-feature', 'real-time-updates'],
      { user: auth.user }
    )

    return response.ok({
      dashboard: {
        showNewDesign: flags['new-dashboard'],
        showAnalytics: flags['advanced-analytics'],
        allowExport: flags['export-feature'],
        realTimeUpdates: flags['real-time-updates'],
      },
    })
  }
}

Middleware Options

Basic Middleware

import { featureFlag, featureFlagWithRedirect, featureFlagWithHandler } from '@fanion/adonisjs'

// Simple abort on disabled
router.get('/feature', [featureFlag('my-feature')])

// Redirect when disabled
router.get('/feature', [featureFlagWithRedirect('my-feature', '/coming-soon')])

// Custom handler when disabled
router.get('/feature', [
  featureFlagWithHandler('my-feature', ({ response }) => {
    return response.status(503).json({ message: 'Feature temporarily unavailable' })
  }),
])

Advanced Middleware

import { createFanionMiddleware } from '@fanion/adonisjs'

// Custom middleware with context provider
const betaMiddleware = createFanionMiddleware({
  flag: 'beta-features',
  contextProvider: async ({ auth, request }) => ({
    user: auth.user,
    userAgent: request.header('user-agent'),
    isMobile: request.header('user-agent')?.includes('Mobile'),
  }),
  onDisabled: 'custom',
  customHandler: ({ response }) => {
    return response.status(404).json({
      error: 'Feature not found',
      message: 'This feature is not available in your current plan',
    })
  },
})

router.get('/beta', [betaMiddleware])

Helpers and Utilities

Global Helpers

import { isFeatureActive, getUserFeatures, ifFeatureActive } from '@fanion/adonisjs'

// Check feature globally
const isEnabled = await isFeatureActive('my-feature')

// Get all features for a user
const userFeatures = await getUserFeatures(user, ['feature1', 'feature2'])

// Conditional execution
await ifFeatureActive('email-notifications', async () => {
  await sendWelcomeEmail(user)
})

View Helpers

// In a service provider or preloader
import { ViewHelpers } from '@fanion/adonisjs'

export default class AppProvider {
  async boot() {
    const fanion = await this.app.container.make('fanion')
    const view = await this.app.container.make('view')

    // Add global view helpers
    view.global(ViewHelpers.createViewGlobals(fanion))
  }
}
{{-- In your Edge templates --}}
@if(await isFeatureActive('new-ui'))
  <div class="new-ui-component">
    <!-- New UI content -->
  </div>
@else
  <div class="legacy-ui-component">
    <!-- Legacy UI content -->
  </div>
@end

Database Schema

When using database storage, the following table structure is created:

CREATE TABLE feature_flags (
  feature_name VARCHAR PRIMARY KEY,
  value BOOLEAN NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

You can customize the table name and column names in the configuration:

export default defineConfig({
  storageDriver: {
    type: 'knex',
    config: {
      connection: db.connection(),
      tableName: 'app_feature_flags',
      featureNameColumn: 'flag_name',
      valueColumn: 'is_enabled',
    },
  },
})

Best Practices

1. Use Descriptive Names

// Good
'user-profile-redesign'
'checkout-express-shipping'
'admin-advanced-analytics'

// Avoid
'flag1'
'test'
'new-feature'

2. Document Your Flags

export default defineConfig({
  features: [
    {
      name: 'payment-method-apple-pay',
      description: 'Enable Apple Pay as payment method in checkout',
      check: (context) => {
        // Only enable for iOS users
        return context.userAgent?.includes('iPhone') || context.userAgent?.includes('iPad')
      },
    },
  ],
})

3. Use Kill Switches for Risky Features

// Store critical features in database for quick toggles
{
  name: 'payment-processing',
  description: 'Kill switch for payment processing',
  store: true,
  defaultValue: true,
}

4. Implement Gradual Rollouts

{
  name: 'new-search-algorithm',
  description: 'Gradual rollout of new search algorithm',
  check: (context) => {
    const userId = context.user?.id || 0
    const rolloutPercentage = 10 // Start with 10%
    return (userId % 100) < rolloutPercentage
  },
}

5. Handle Errors Gracefully

export default class FeatureController {
  @inject()
  async show({ response }: HttpContext, fanion: FanionService) {
    try {
      const isActive = await fanion.active('experimental-feature')
      // Use feature
    } catch (error) {
      // Log error and fall back to safe default
      console.error('Feature flag error:', error)
      // Provide fallback behavior
    }
  }
}

Testing

Testing Feature Flags

// tests/functional/feature_flags.spec.ts
import { test } from '@japa/runner'
import { FanionServiceImpl } from '@fanion/adonisjs'

test.group('Feature Flags', () => {
  test('should enable premium features for premium users', async ({ assert }) => {
    const fanion = new FanionServiceImpl()

    fanion.define('premium-features', (context) => {
      return context.user?.plan === 'premium'
    })

    const result = await fanion.active('premium-features', {
      user: { plan: 'premium' },
    })

    assert.isTrue(result)
  })
})

Testing with Middleware

test('should protect routes with feature flags', async ({ client }) => {
  const response = await client.get('/beta-feature')

  // Should return 404 if feature is disabled
  response.assertStatus(404)
})

Migration Guide

From Manual Feature Flags

If you're currently using manual feature flag implementations:

// Before
if (process.env.ENABLE_NEW_FEATURE === 'true') {
  // Feature logic
}

// After
if (await fanion.active('new-feature')) {
  // Feature logic
}

From Other Feature Flag Libraries

Most feature flag libraries can be migrated by:

  1. Defining your existing flags in config/fanion.ts
  2. Replacing flag checks with fanion.active()
  3. Updating middleware to use Fanion middleware
  4. Migrating stored flags to your database

Troubleshooting

Common Issues

Feature flag not found error

FeatureNotExistsError: Feature flag 'my-feature' is not defined

Solution: Make sure the feature is defined in your configuration or via fanion.define().

Storage provider not initialized

Error: No store provider defined

Solution: Configure a storage driver in your configuration or set autoInit: true.

Middleware not working

Error: Binding not found: fanion

Solution: Make sure the FanionProvider is registered in your adonisrc.ts providers array.

Debug Mode

Enable debug mode to see detailed logging:

export default defineConfig({
  debug: true,
})

This will log all feature flag evaluations to help with debugging.

API Reference

FanionService

  • define<T>(name: string, check?: (context: T) => boolean | Promise<boolean>): void
  • defineAndStore(name: string, defaultValue?: boolean): Promise<void>
  • active<T>(name: string, context?: T): Promise<boolean>
  • activeForRequest(name: string, ctx: HttpContext, additionalContext?: any): Promise<boolean>
  • activeMany<T>(flags: string[], context?: T): Promise<Record<string, boolean>>
  • getDefinedFlags(): string[]

Middleware

  • featureFlag(flagName: string, onDisabled?: 'abort' | 'next')
  • featureFlagWithRedirect(flagName: string, redirectTo: string)
  • featureFlagWithHandler(flagName: string, customHandler: Function)
  • createFanionMiddleware(options: FanionMiddlewareOptions)

Helpers

  • isFeatureActive<T>(flagName: string, context?: T): Promise<boolean>
  • isFeatureActiveForRequest(flagName: string, ctx: HttpContext, additionalContext?: any): Promise<boolean>
  • getUserFeatures(user: any, flagNames?: string[]): Promise<Record<string, boolean>>
  • ifFeatureActive<T>(flagName: string, callback: Function, context?: T): Promise<any>
  • requireFeature(flagName: string, options?: object): MethodDecorator

Utilities

  • ABTesting.createABTest(testName: string, percentage: number, identifier?: Function)
  • ABTesting.percentageRollout(identifier: number | string, percentage: number): boolean
  • EnvironmentFlags.developmentOnly(): () => boolean
  • EnvironmentFlags.productionOnly(): () => boolean
  • EnvironmentFlags.createEnvironmentFlag(environments: string[]): () => boolean

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

MIT © Maxence Guyonvarho

Related


Made with ❤️ by Maxence Guyonvarho