Getting Started

This guide walks you through installing Flick, defining your first feature, and resolving it for a user.

Installation

Install and configure the package using the following command:

node ace add @foadonis/flick

Configure the driver

Flick stores resolved feature values in a driver. The memory driver is enough for development; switch to Redis (or your own driver) for production.

config/flick.ts
import { features } from "#generated/features";
import { defineConfig, drivers } from "@foadonis/flick";

const flickConfig = defineConfig({
  features,
  driver: "memory",
  drivers: {
    memory: drivers.memory(),
  },
});

export default flickConfig;

declare module "@foadonis/flick/types" {
  interface KnownFeatures extends InferFeatures<typeof flickConfig> {}
  interface KnownDriver extends InferFlickDriver<typeof flickConfig> {}
}

The declare module block registers your features with Flick's type system so feature names are autocompleted and typo-checked wherever you pass a feature name to the resolver.

The features import comes from #generated/features, a barrel file produced at build time by the indexFeatures() hook. It lists every *_feature.ts file under app/features/.

On a fresh install the barrel does not exist yet. Run node ace build (or start the dev server with node ace serve) once so the assembler hook generates .adonisjs/server/features.ts before TypeScript compiles config/flick.ts.

Make a model scopeable

Features are always resolved against a scope: a user, a tenant, a request, or any object you choose. The scope must implement FeatureScopeable so Flick knows how to identify it in its cache.

The easiest way is to apply the HasFeatures mixin to your Lucid model. It uses the model's primary key as the identifier:

app/models/user.ts
import { compose } from "@adonisjs/core/helpers";
import { BaseModel } from "@adonisjs/lucid/orm";
import { HasFeatures } from "@foadonis/flick";

export default class User extends compose(BaseModel, HasFeatures) {}

For non-Lucid scopes or custom identifiers, implement FeatureScopeable yourself. See Defining Features for details.

Define your first feature

Create a feature class under app/features/. The file must end with _feature.ts so the indexer picks it up.

app/features/new_checkout_feature.ts
import { BaseFeature } from "@foadonis/flick";
import User from "#models/user";

export default class NewCheckoutFeature extends BaseFeature<User> {
  async resolve(user: User) {
    if (user.isAdmin) return true;

    return Math.random() > 0.5;
  }
}

The resolve method receives the scope (a User here) and returns whatever value represents the flag's state. Admins always get the new checkout; everyone else has a 50/50 chance.

The feature is named after its filename with _feature stripped: new_checkout_feature.ts becomes the new_checkout flag.

Resolve the feature

Import the Flick service and resolve the feature for a user:

start/routes.ts
import router from "@adonisjs/core/services/router";
import flick from "@foadonis/flick/services/main";

router.get("/checkout", async ({ auth, view }) => {
  const user = auth.getUserOrFail();

  return flick.for(user).match("new_checkout", {
    active: () => view.render("checkout/new"),
    inactive: () => view.render("checkout/legacy"),
  });
});

That's it. Hit /checkout and Flick will route admins to the new checkout, and everyone else to the new or legacy checkout based on the coin flip.

The result is cached per scope identifier, so the same user keeps getting the same outcome until the cache entry is cleared. See Drivers for cache configuration.

What's next?

Now that you have a working flag, explore the guides:

On this page