@devx-retailos/feature-flags
v0.0.2
Published
Runtime feature-flag Medusa module for retailOS: registry-declared flags with global, per-organization, and per-store overrides.
Keywords
Readme
@devx-retailos/feature-flags
Runtime feature flags for POS backends: flags are declared in code via a registry, synced to the database at boot, and resolved with global, per-organization, and per-store overrides.
Part of retailOS, a Medusa v2 SDK for offline-store POS systems. Packages are installed independently and composed in a brand's Medusa backend.
Installation
npm install @devx-retailos/feature-flagsRequires @medusajs/framework and @medusajs/medusa ^2.13.0 as peer dependencies. Depends on @devx-retailos/core (which provides the FlagRegistry declaration helpers).
Setup
// medusa-config.ts
export default defineConfig({
// ...
plugins: [
{
resolve: "@devx-retailos/feature-flags",
options: {},
},
],
})The module registers under the feature_flags key (exported as FEATURE_FLAGS_MODULE).
Usage
Declaring flags
Flags are plain FlagRegistration[] objects from @devx-retailos/core. Sync them into the database at boot (e.g. in a startup script or loader):
import { syncAllFlags, type FlagRegistration } from "@devx-retailos/core"
import { FEATURE_FLAGS_MODULE, type FeatureFlagModuleService } from "@devx-retailos/feature-flags"
const FLAGS: FlagRegistration[] = [
{
key: "pos.split_payments",
description: "Allow splitting an order across multiple tenders",
default_value: false,
registered_by: "my-backend",
},
]
const flags = container.resolve<FeatureFlagModuleService>(FEATURE_FLAGS_MODULE)
await syncAllFlags(flags, FLAGS)
// or directly: await flags.syncFlagsFromRegistry(registry)Sync is idempotent — it creates missing flags and updates changed descriptions/defaults.
Checking flags
// Boolean check, scoped to the current org/store
const enabled = await flags.isEnabled("pos.split_payments", {
organization_id: "org_01",
store_id: "store_01",
})
// Full resolution — tells you which level decided the value
const res = await flags.resolve("pos.split_payments", { organization_id: "org_01" })
// → { key: "pos.split_payments", value: true, level: "organization" }
// All active flags resolved for a scope (e.g. to ship to a POS client)
const all = await flags.getResolvedFlags({ organization_id: "org_01", store_id: "store_01" })
// → { "pos.split_payments": true, ... }Overrides and precedence
Resolution order: store override → organization override → global override → registered default → false (level: "unknown" when the key was never registered and has no override).
// Global override: both scope ids null
await flags.setOverride({ key: "pos.split_payments", value: true })
// Organization override
await flags.setOverride({ key: "pos.split_payments", organization_id: "org_01", value: false })
// Store override (most specific — requires both ids)
await flags.setOverride({
key: "pos.split_payments",
organization_id: "org_01",
store_id: "store_01",
value: true,
})
// Remove an override at a given scope
await flags.removeOverride({ key: "pos.split_payments", organization_id: "org_01" })The FlagScope, FlagResolution, and FlagResolutionLevel types are exported from the package root for typing your own wrappers.
API routes
Authenticated admin routes shipped by the plugin:
| Method | Path | Description |
| --- | --- | --- |
| GET | /admin/retailos/feature-flags?organization_id=&store_id= | List definitions, overrides, and resolved values for a scope |
| POST | /admin/retailos/feature-flags | Set an override (key, scope ids, value in body) |
| POST | /admin/retailos/feature-flags/:key | Set an override for a flag key |
| DELETE | /admin/retailos/feature-flags/:key?organization_id=&store_id= | Remove the override at that scope |
Related packages
@devx-retailos/core—FlagRegistry,syncAllFlags,Logger,RetailOSError@devx-retailos/rbac— organizations and stores that overrides are scoped to@devx-retailos/discount— discount and coupon engine@devx-retailos/order— POS orders module@devx-retailos/sdk-client— typed frontend client and React hooks
License
MIT
