@strav/flag 0.4.30 → 1.0.0-alpha.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,65 +1,131 @@
1
1
  # @strav/flag
2
2
 
3
- Feature flags for the [Strav](https://www.npmjs.com/package/@strav/core) framework. Define, scope, and toggle features with database persistence, in-memory drivers, and per-user/team targeting.
3
+ Feature flags for [Strav 1.x](https://github.com/strav-dev/strav-1.x). Define
4
+ flags, scope them per user / team / tenant, persist resolutions in-memory or in
5
+ Postgres, and gate routes with an HTTP middleware. Mirrors the shape of
6
+ `@strav/cache` — kernel-free core in the root, drivers and integrations under
7
+ subpaths.
4
8
 
5
9
  ## Install
6
10
 
7
11
  ```bash
8
12
  bun add @strav/flag
9
- bun strav install flag
10
13
  ```
11
14
 
12
- Requires `@strav/core` as a peer dependency.
13
-
14
- ## Setup
15
+ ## Quick start
15
16
 
16
17
  ```ts
17
- import { FlagProvider } from '@strav/flag'
18
+ import { FlagManager, FlagProvider } from '@strav/flag'
18
19
 
19
- app.use(new FlagProvider())
20
- ```
20
+ // Wire the provider (in-memory store by default).
21
+ new FlagProvider({
22
+ define(flags) {
23
+ flags.define('beta-ui', (scope) => scope.startsWith('User:1'))
24
+ flags.define('upload-limit', 25)
25
+ },
26
+ })
21
27
 
22
- ## Usage
28
+ // Resolve and read.
29
+ const flags = app.resolve(FlagManager)
23
30
 
24
- ```ts
25
- import { flag } from '@strav/flag'
31
+ if (await flags.active('beta-ui', user)) { ... }
32
+ if (await flags.for(team).active('analytics')) { ... }
33
+
34
+ const limit = await flags.value('upload-limit') // 25
35
+ ```
36
+
37
+ ## Drivers
26
38
 
27
- // Check if a feature is active
28
- if (await flag.active('dark-mode')) {
29
- // feature is enabled
30
- }
39
+ | Subpath | Driver | Use case |
40
+ | ----------------------- | --------------------- | -------------------------------------- |
41
+ | (root) | `MemoryFlagStore` | dev / tests / single-process apps |
42
+ | `@strav/flag/postgres` | `PostgresFlagStore` | multi-node deployments (cross-process) |
31
43
 
32
- // Scoped to a user or team
33
- if (await flag.for(user).active('beta-dashboard')) {
34
- // enabled for this user
35
- }
44
+ Swap providers both register under the same `FlagManager` token:
36
45
 
37
- // Rich values
38
- const limit = await flag.value('upload-limit', 10)
46
+ ```ts
47
+ import { PostgresFlagProvider, applyFlagMigration } from '@strav/flag/postgres'
48
+
49
+ // Migration:
50
+ await applyFlagMigration(db)
51
+
52
+ // Provider:
53
+ providers: [
54
+ new PostgresFlagProvider({
55
+ define(flags) {
56
+ flags.define('beta-ui', (scope) => scope.startsWith('User:'))
57
+ },
58
+ }),
59
+ ]
39
60
  ```
40
61
 
41
- ## Middleware
62
+ ## HTTP guard
42
63
 
43
64
  ```ts
44
- import { ensureFeature } from '@strav/flag'
65
+ import { ensureFeature } from '@strav/flag/http'
45
66
 
46
- router.get('/beta', ensureFeature('beta-dashboard'), betaHandler)
67
+ router.get('/beta', ensureFeature('beta-ui'), betaHandler)
68
+ router.group(
69
+ { middleware: [authMiddleware(), ensureFeature('analytics')] },
70
+ (r) => r.get('/insights', insights),
71
+ )
47
72
  ```
48
73
 
49
- ## Drivers
50
-
51
- - **Database** — persistent feature flags in `_strav_features`
52
- - **Array** — in-memory driver for testing
74
+ Default scope is `ctx.auth?.user`. Pass `scopeFrom` for team / tenant / custom
75
+ subjects.
53
76
 
54
77
  ## CLI
55
78
 
56
79
  ```bash
57
- bun strav flag:setup # Create the features table
80
+ bun strav flag:list # list stored flags + values
81
+ bun strav flag:activate beta-ui # global on
82
+ bun strav flag:activate beta-ui '"control"' --scope User:42
83
+ bun strav flag:deactivate beta-ui --scope User:42
84
+ bun strav flag:purge beta-ui # drop stored values → re-resolve
58
85
  ```
59
86
 
60
- ## Documentation
87
+ Register the commands provider alongside the flag provider:
88
+
89
+ ```ts
90
+ import { FlagConsoleProvider } from '@strav/flag/console'
61
91
 
62
- See the full [Flag guide](../../guides/flag.md).
92
+ providers: [
93
+ new FlagProvider({ /* … */ }),
94
+ new FlagConsoleProvider(),
95
+ ]
96
+ ```
97
+
98
+ ## Strict scopes
99
+
100
+ ```ts
101
+ // config/flag.ts
102
+ export default { strictScopes: true }
103
+ ```
104
+
105
+ With `strictScopes: true`, reading a flag without a scope throws
106
+ `MissingScopeError` instead of silently evaluating the global value. Catches the
107
+ common bug where middleware forgets to pass `ctx.auth.user`. Writes keep the
108
+ loose semantics — `activateForEveryone()` makes a global write explicit.
109
+
110
+ ## Audit hook
111
+
112
+ `activate` / `deactivate` accept an optional `actor: { type, id }` that flows
113
+ into the `flag:updated` event payload alongside `previous` (the prior stored
114
+ value). Subscribe via `EventBus` to wire an audit log without coupling
115
+ `@strav/flag` to `@strav/audit`:
116
+
117
+ ```ts
118
+ app.resolve(EventBus).on('flag:updated', (e) => {
119
+ if (!e.actor) return
120
+ auditLog.record({
121
+ actor: e.actor,
122
+ on: 'feature_flag',
123
+ feature: e.feature,
124
+ scope: e.scope,
125
+ diff: { from: e.previous, to: e.value },
126
+ })
127
+ })
128
+ ```
63
129
 
64
130
  ## License
65
131
 
package/package.json CHANGED
@@ -1,33 +1,46 @@
1
1
  {
2
2
  "name": "@strav/flag",
3
- "version": "0.4.30",
3
+ "version": "1.0.0-alpha.42",
4
+ "description": "Strav feature-flag primitive — FlagManager + scope-aware resolution + in-memory store, with Postgres store, HTTP middleware, and CLI commands shipped under subpaths. Mirrors @strav/cache: kernel-free core in the root, every backend/integration under its own subpath.",
4
5
  "type": "module",
5
- "description": "Feature flags for the Strav framework",
6
- "license": "MIT",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
7
8
  "exports": {
8
9
  ".": "./src/index.ts",
9
- "./*": "./src/*.ts"
10
- },
11
- "strav": {
12
- "commands": "src/commands"
10
+ "./memory": "./src/drivers/memory/index.ts",
11
+ "./postgres": "./src/drivers/postgres/index.ts",
12
+ "./http": "./src/http/index.ts",
13
+ "./console": "./src/console/index.ts"
13
14
  },
14
15
  "files": [
15
- "src/",
16
- "stubs/",
17
- "package.json",
18
- "tsconfig.json"
16
+ "src",
17
+ "README.md"
19
18
  ],
19
+ "engines": {
20
+ "bun": ">=1.3.14"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "@strav/kernel": "1.0.0-alpha.42"
27
+ },
20
28
  "peerDependencies": {
21
- "@strav/kernel": "0.4.30",
22
- "@strav/database": "0.4.30",
23
- "@strav/http": "0.4.30",
24
- "@strav/cli": "0.4.30"
29
+ "@strav/cli": "1.0.0-alpha.42",
30
+ "@strav/database": "1.0.0-alpha.42",
31
+ "@strav/http": "1.0.0-alpha.42",
32
+ "@types/bun": ">=1.3.14"
25
33
  },
26
- "scripts": {
27
- "test": "bun test tests/",
28
- "typecheck": "tsc --noEmit"
34
+ "peerDependenciesMeta": {
35
+ "@strav/cli": {
36
+ "optional": true
37
+ },
38
+ "@strav/database": {
39
+ "optional": true
40
+ },
41
+ "@strav/http": {
42
+ "optional": true
43
+ }
29
44
  },
30
- "devDependencies": {
31
- "commander": "^14.0.3"
32
- }
45
+ "devDependencies": null
33
46
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `bun strav flag:activate <feature> [value] [--scope <Type:id>]` —
3
+ * turn a flag on, optionally with a rich JSON value, for a specific
4
+ * scope or globally (default).
5
+ *
6
+ * The scope format mirrors the on-disk serialization (`User:42`,
7
+ * `Team:7`) — passing one is sufficient because the operator pins the
8
+ * scope manually; the runtime resolver pulls scopes off live request
9
+ * state, where the type prefix is derived automatically.
10
+ */
11
+
12
+ import { Command, type ExecuteArgs, ExitCode } from '@strav/cli'
13
+ import { FlagError } from '../flag_error.ts'
14
+ import { FlagManager } from '../flag_manager.ts'
15
+
16
+ export class FlagActivate extends Command {
17
+ static signature = 'flag:activate {feature} {value?} {--scope=}'
18
+ static description = 'Turn a flag on, optionally with a JSON value, for a scope or globally.'
19
+ static providers = ['config', 'logger', 'flag']
20
+
21
+ override async execute({ args, flags: cli }: ExecuteArgs): Promise<number> {
22
+ const flags = this.app.resolve(FlagManager)
23
+ const feature = args.feature as string
24
+ const raw = args.value as string | undefined
25
+ const scope = cli.scope as string | undefined
26
+
27
+ const value = raw !== undefined ? parseValue(raw) : true
28
+
29
+ if (scope === undefined) {
30
+ await flags.activateForEveryone(feature, value)
31
+ this.success(`Activated "${feature}" globally${raw ? ` = ${raw}` : ''}.`)
32
+ } else {
33
+ await flags.activate(feature, value, scopeFromKey(scope))
34
+ this.success(`Activated "${feature}" for ${scope}${raw ? ` = ${raw}` : ''}.`)
35
+ }
36
+ return ExitCode.Success
37
+ }
38
+ }
39
+
40
+ function parseValue(raw: string): unknown {
41
+ try {
42
+ return JSON.parse(raw)
43
+ } catch {
44
+ return raw
45
+ }
46
+ }
47
+
48
+ // CLI helper: turn `User:42` into a fake Scopeable so the manager
49
+ // serializes it back to the same `User:42` key. The runtime
50
+ // resolution path always works on live objects with `id`; this is the
51
+ // inverse used only at the CLI boundary.
52
+ function scopeFromKey(key: string): { id: string | number; featureScope: () => string } {
53
+ const colon = key.indexOf(':')
54
+ if (colon <= 0) {
55
+ throw new FlagError(`--scope must look like "Type:id" (e.g. "User:42"); got "${key}".`)
56
+ }
57
+ const type = key.slice(0, colon)
58
+ const id: string | number = key.slice(colon + 1)
59
+ return { id, featureScope: () => type }
60
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `FlagConsoleProvider` — declares the `flag:*` console commands.
3
+ *
4
+ * Apps register it alongside the flag provider:
5
+ *
6
+ * providers: [
7
+ * new FlagProvider(), // or PostgresFlagProvider from @strav/flag/postgres
8
+ * new FlagConsoleProvider(),
9
+ * ]
10
+ *
11
+ * Separate provider so apps without a CLI don't pay the cost of
12
+ * resolving commands at boot — same pattern as `CacheConsoleProvider`
13
+ * and `QueueConsoleProvider`.
14
+ */
15
+
16
+ import { ConsoleProvider } from '@strav/cli'
17
+ import { FlagActivate } from './flag_activate.ts'
18
+ import { FlagDeactivate } from './flag_deactivate.ts'
19
+ import { FlagList } from './flag_list.ts'
20
+ import { FlagPurge } from './flag_purge.ts'
21
+
22
+ export class FlagConsoleProvider extends ConsoleProvider {
23
+ override readonly name = 'console.flag'
24
+ override readonly commands = [FlagList, FlagPurge, FlagActivate, FlagDeactivate] as const
25
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `bun strav flag:deactivate <feature> [--scope <Type:id>]` — turn a
3
+ * flag off for a specific scope or globally (default).
4
+ */
5
+
6
+ import { Command, type ExecuteArgs, ExitCode } from '@strav/cli'
7
+ import { FlagError } from '../flag_error.ts'
8
+ import { FlagManager } from '../flag_manager.ts'
9
+
10
+ export class FlagDeactivate extends Command {
11
+ static signature = 'flag:deactivate {feature} {--scope=}'
12
+ static description = 'Turn a flag off for a scope or globally.'
13
+ static providers = ['config', 'logger', 'flag']
14
+
15
+ override async execute({ args, flags: cli }: ExecuteArgs): Promise<number> {
16
+ const flags = this.app.resolve(FlagManager)
17
+ const feature = args.feature as string
18
+ const scope = cli.scope as string | undefined
19
+
20
+ if (scope === undefined) {
21
+ await flags.deactivateForEveryone(feature)
22
+ this.success(`Deactivated "${feature}" globally.`)
23
+ } else {
24
+ await flags.deactivate(feature, scopeFromKey(scope))
25
+ this.success(`Deactivated "${feature}" for ${scope}.`)
26
+ }
27
+ return ExitCode.Success
28
+ }
29
+ }
30
+
31
+ function scopeFromKey(key: string): { id: string | number; featureScope: () => string } {
32
+ const colon = key.indexOf(':')
33
+ if (colon <= 0) {
34
+ throw new FlagError(`--scope must look like "Type:id" (e.g. "User:42"); got "${key}".`)
35
+ }
36
+ return { id: key.slice(colon + 1), featureScope: () => key.slice(0, colon) }
37
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `bun strav flag:list` — list every feature with at least one stored
3
+ * value and its per-scope evaluations. Read-only.
4
+ */
5
+
6
+ import { Command, type ExecuteArgs, ExitCode } from '@strav/cli'
7
+ import { FlagManager } from '../flag_manager.ts'
8
+
9
+ export class FlagList extends Command {
10
+ static signature = 'flag:list'
11
+ static description = 'List stored feature flags and their per-scope values.'
12
+ static providers = ['config', 'logger', 'flag']
13
+
14
+ override async execute(_args: ExecuteArgs): Promise<number> {
15
+ const flags = this.app.resolve(FlagManager)
16
+ const names = await flags.stored()
17
+ if (names.length === 0) {
18
+ this.info('No stored feature flags.')
19
+ return ExitCode.Success
20
+ }
21
+ this.info(`Stored feature flags (${names.length}):`)
22
+ for (const name of names) {
23
+ const records = await flags.driver().allFor(name)
24
+ this.info(` ${name} (${records.length} scope${records.length === 1 ? '' : 's'})`)
25
+ for (const r of records) {
26
+ const printed = typeof r.value === 'boolean' ? String(r.value) : JSON.stringify(r.value)
27
+ this.info(` ${r.scope} → ${printed}`)
28
+ }
29
+ }
30
+ return ExitCode.Success
31
+ }
32
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `bun strav flag:purge [feature] [--all] [--force]` — drop stored
3
+ * flag values so resolvers re-evaluate. Without `--force`, prompts
4
+ * before running.
5
+ */
6
+
7
+ import { Command, type ExecuteArgs, ExitCode } from '@strav/cli'
8
+ import { FlagManager } from '../flag_manager.ts'
9
+
10
+ export class FlagPurge extends Command {
11
+ static signature = 'flag:purge {feature?} {--all} {--force}'
12
+ static description = 'Drop stored flag values (forces re-resolution on next read).'
13
+ static providers = ['config', 'logger', 'flag']
14
+
15
+ override async execute({ args, flags: cli }: ExecuteArgs): Promise<number> {
16
+ const flags = this.app.resolve(FlagManager)
17
+ const feature = args.feature as string | undefined
18
+ const all = cli.all === true || !feature
19
+
20
+ if (cli.force !== true) {
21
+ const target = all ? 'EVERY feature flag' : `feature "${feature}"`
22
+ const ok = await this.confirm(`Purge ${target}? This drops stored values for all scopes.`)
23
+ if (!ok) {
24
+ this.info('Aborted.')
25
+ return ExitCode.Success
26
+ }
27
+ }
28
+
29
+ if (all) {
30
+ await flags.purgeAll()
31
+ this.success('All feature flags purged.')
32
+ } else {
33
+ await flags.purge(feature as string)
34
+ this.success(`Feature "${feature}" purged.`)
35
+ }
36
+ return ExitCode.Success
37
+ }
38
+ }
@@ -0,0 +1,5 @@
1
+ export { FlagActivate } from './flag_activate.ts'
2
+ export { FlagConsoleProvider } from './flag_console_provider.ts'
3
+ export { FlagDeactivate } from './flag_deactivate.ts'
4
+ export { FlagList } from './flag_list.ts'
5
+ export { FlagPurge } from './flag_purge.ts'
@@ -0,0 +1 @@
1
+ export { MemoryFlagStore } from './memory_flag_store.ts'
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `MemoryFlagStore` — in-process feature flag store backed by a single
3
+ * `Map`. Right driver for dev / tests / per-process resolutions that
4
+ * don't need cross-process visibility. Wrong driver for multi-node
5
+ * deployments — each node sees its own copy of every flag.
6
+ */
7
+
8
+ import type { FeatureStore } from '../../feature_store.ts'
9
+ import type { ScopeKey, StoredFeature } from '../../types.ts'
10
+
11
+ interface Entry {
12
+ value: unknown
13
+ createdAt: Date
14
+ updatedAt: Date
15
+ }
16
+
17
+ export class MemoryFlagStore implements FeatureStore {
18
+ readonly name = 'memory'
19
+ private readonly data = new Map<string, Entry>()
20
+
21
+ private key(feature: string, scope: ScopeKey): string {
22
+ return `${feature}\0${scope}`
23
+ }
24
+
25
+ async get(feature: string, scope: ScopeKey): Promise<unknown | undefined> {
26
+ return this.data.get(this.key(feature, scope))?.value
27
+ }
28
+
29
+ async getMany(features: string[], scope: ScopeKey): Promise<Map<string, unknown>> {
30
+ const result = new Map<string, unknown>()
31
+ for (const f of features) {
32
+ const e = this.data.get(this.key(f, scope))
33
+ if (e !== undefined) result.set(f, e.value)
34
+ }
35
+ return result
36
+ }
37
+
38
+ async set(feature: string, scope: ScopeKey, value: unknown): Promise<void> {
39
+ const k = this.key(feature, scope)
40
+ const existing = this.data.get(k)
41
+ const now = new Date()
42
+ this.data.set(k, {
43
+ value,
44
+ createdAt: existing?.createdAt ?? now,
45
+ updatedAt: now,
46
+ })
47
+ }
48
+
49
+ async setMany(
50
+ entries: Array<{ feature: string; scope: ScopeKey; value: unknown }>,
51
+ ): Promise<void> {
52
+ for (const e of entries) await this.set(e.feature, e.scope, e.value)
53
+ }
54
+
55
+ async forget(feature: string, scope: ScopeKey): Promise<void> {
56
+ this.data.delete(this.key(feature, scope))
57
+ }
58
+
59
+ async purge(feature: string): Promise<void> {
60
+ const prefix = `${feature}\0`
61
+ for (const k of this.data.keys()) {
62
+ if (k.startsWith(prefix)) this.data.delete(k)
63
+ }
64
+ }
65
+
66
+ async purgeAll(): Promise<void> {
67
+ this.data.clear()
68
+ }
69
+
70
+ async featureNames(): Promise<string[]> {
71
+ const names = new Set<string>()
72
+ for (const k of this.data.keys()) {
73
+ const sep = k.indexOf('\0')
74
+ if (sep > 0) names.add(k.slice(0, sep))
75
+ }
76
+ return [...names].sort()
77
+ }
78
+
79
+ async allFor(feature: string): Promise<StoredFeature[]> {
80
+ const prefix = `${feature}\0`
81
+ const out: StoredFeature[] = []
82
+ for (const [k, e] of this.data) {
83
+ if (!k.startsWith(prefix)) continue
84
+ out.push({
85
+ feature,
86
+ scope: k.slice(prefix.length),
87
+ value: e.value,
88
+ createdAt: e.createdAt,
89
+ updatedAt: e.updatedAt,
90
+ })
91
+ }
92
+ return out
93
+ }
94
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `applyFlagMigration` — emit the DDL `PostgresFlagStore` requires.
3
+ *
4
+ * strav_flags (
5
+ * feature text NOT NULL,
6
+ * scope text NOT NULL,
7
+ * value jsonb NOT NULL,
8
+ * created_at timestamptz NOT NULL DEFAULT now(),
9
+ * updated_at timestamptz NOT NULL DEFAULT now(),
10
+ * PRIMARY KEY (feature, scope)
11
+ * )
12
+ *
13
+ * Not registered with the SchemaRegistry — the table shape (composite
14
+ * PK, jsonb value column) doesn't fit the schema DSL cleanly, and
15
+ * the table is package-internal infrastructure rather than an app
16
+ * model. Call from a migration `up()`; `DROP TABLE` in `down()`.
17
+ */
18
+
19
+ import type { DatabaseExecutor } from '@strav/database'
20
+
21
+ export async function applyFlagMigration(db: DatabaseExecutor): Promise<void> {
22
+ await db.execute(
23
+ `CREATE TABLE IF NOT EXISTS "strav_flags" (
24
+ "feature" text NOT NULL,
25
+ "scope" text NOT NULL,
26
+ "value" jsonb NOT NULL,
27
+ "created_at" timestamptz NOT NULL DEFAULT now(),
28
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
29
+ PRIMARY KEY ("feature", "scope")
30
+ )`,
31
+ )
32
+ }
@@ -0,0 +1,10 @@
1
+ export { applyFlagMigration } from './apply_flag_migration.ts'
2
+ export {
3
+ PostgresFlagProvider,
4
+ type PostgresFlagProviderOptions,
5
+ } from './postgres_flag_provider.ts'
6
+ export {
7
+ type PostgresFlagDatabase,
8
+ PostgresFlagStore,
9
+ type PostgresFlagStoreOptions,
10
+ } from './postgres_flag_store.ts'
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `PostgresFlagProvider` — wires `FlagManager` with `PostgresFlagStore`.
3
+ * Apps register this INSTEAD OF `FlagProvider` to swap the in-memory
4
+ * store for the cross-process Postgres backplane. Both providers
5
+ * register under the same `FlagManager` token, so app code injecting
6
+ * the manager doesn't change between dev and prod.
7
+ *
8
+ * Schema creation is migration-driven (`applyFlagMigration`); the
9
+ * provider does NOT auto-create on boot.
10
+ */
11
+
12
+ import { type Application, ConfigRepository, EventBus, ServiceProvider } from '@strav/kernel'
13
+ import { FlagManager } from '../../flag_manager.ts'
14
+ import type { FlagConfig } from '../../types.ts'
15
+ import { type PostgresFlagDatabase, PostgresFlagStore } from './postgres_flag_store.ts'
16
+
17
+ export interface PostgresFlagProviderOptions {
18
+ /** Run after the manager is created, before `boot` returns. Use for `define`. */
19
+ define?: (flags: FlagManager) => void | Promise<void>
20
+ }
21
+
22
+ export class PostgresFlagProvider extends ServiceProvider {
23
+ override readonly name = 'flag'
24
+ override readonly dependencies = ['config', 'database']
25
+
26
+ constructor(private readonly options: PostgresFlagProviderOptions = {}) {
27
+ super()
28
+ }
29
+
30
+ override register(app: Application): void {
31
+ app.singleton(FlagManager, (c) => {
32
+ // Resolve `Database` by string token to keep `@strav/database` an
33
+ // optional peer-dep — apps using `MemoryFlagStore` shouldn't
34
+ // pay for `@strav/database` in their bundle.
35
+ const db = c.resolve<PostgresFlagDatabase>('database' as never)
36
+ const cfg = (c.resolve(ConfigRepository).get('flag') as FlagConfig | undefined) ?? {}
37
+ return new FlagManager({
38
+ store: new PostgresFlagStore({ db }),
39
+ events: c.resolve(EventBus),
40
+ config: cfg,
41
+ })
42
+ })
43
+ }
44
+
45
+ override async boot(app: Application): Promise<void> {
46
+ const flags = app.resolve(FlagManager)
47
+ if (this.options.define) await this.options.define(flags)
48
+ }
49
+ }