@mergedapp/feature-flags 0.1.3

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.
Files changed (114) hide show
  1. package/README.md +651 -0
  2. package/dist/cjs/cli/audit.js +117 -0
  3. package/dist/cjs/cli/cleanup.js +105 -0
  4. package/dist/cjs/cli/config-loader.js +102 -0
  5. package/dist/cjs/cli/generate.js +194 -0
  6. package/dist/cjs/cli/parse-args.js +18 -0
  7. package/dist/cjs/cli.js +46 -0
  8. package/dist/cjs/client.js +505 -0
  9. package/dist/cjs/errors.js +24 -0
  10. package/dist/cjs/index.js +13 -0
  11. package/dist/cjs/jwt.js +85 -0
  12. package/dist/cjs/nestjs/bindings.js +36 -0
  13. package/dist/cjs/nestjs/constants.js +7 -0
  14. package/dist/cjs/nestjs/context.js +28 -0
  15. package/dist/cjs/nestjs/decorators.js +50 -0
  16. package/dist/cjs/nestjs/errors.js +25 -0
  17. package/dist/cjs/nestjs/evaluator.js +87 -0
  18. package/dist/cjs/nestjs/guard.js +67 -0
  19. package/dist/cjs/nestjs/interceptor.js +56 -0
  20. package/dist/cjs/nestjs/module.js +70 -0
  21. package/dist/cjs/nestjs/service.js +54 -0
  22. package/dist/cjs/nestjs/types.js +2 -0
  23. package/dist/cjs/nestjs.js +26 -0
  24. package/dist/cjs/openfeature/context.js +166 -0
  25. package/dist/cjs/openfeature/hooks.js +31 -0
  26. package/dist/cjs/openfeature/server-provider.js +107 -0
  27. package/dist/cjs/openfeature/server.js +13 -0
  28. package/dist/cjs/openfeature/shared.js +83 -0
  29. package/dist/cjs/openfeature/web-provider.js +156 -0
  30. package/dist/cjs/openfeature/web.js +13 -0
  31. package/dist/cjs/package.json +3 -0
  32. package/dist/cjs/persistence.js +249 -0
  33. package/dist/cjs/react/hooks.js +86 -0
  34. package/dist/cjs/react/provider.js +106 -0
  35. package/dist/cjs/react.js +7 -0
  36. package/dist/cjs/remote-evaluator.js +162 -0
  37. package/dist/cjs/types.js +2 -0
  38. package/dist/cli/audit.d.ts +3 -0
  39. package/dist/cli/audit.js +114 -0
  40. package/dist/cli/cleanup.d.ts +3 -0
  41. package/dist/cli/cleanup.js +102 -0
  42. package/dist/cli/config-loader.d.ts +26 -0
  43. package/dist/cli/config-loader.js +66 -0
  44. package/dist/cli/generate.d.ts +3 -0
  45. package/dist/cli/generate.js +191 -0
  46. package/dist/cli/parse-args.d.ts +1 -0
  47. package/dist/cli/parse-args.js +15 -0
  48. package/dist/cli.d.ts +1 -0
  49. package/dist/cli.js +45 -0
  50. package/dist/client.d.ts +67 -0
  51. package/dist/client.js +501 -0
  52. package/dist/errors.d.ts +15 -0
  53. package/dist/errors.js +18 -0
  54. package/dist/index.cjs +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +3 -0
  57. package/dist/jwt.d.ts +20 -0
  58. package/dist/jwt.js +78 -0
  59. package/dist/nestjs/bindings.d.ts +5 -0
  60. package/dist/nestjs/bindings.js +33 -0
  61. package/dist/nestjs/constants.d.ts +4 -0
  62. package/dist/nestjs/constants.js +4 -0
  63. package/dist/nestjs/context.d.ts +12 -0
  64. package/dist/nestjs/context.js +24 -0
  65. package/dist/nestjs/decorators.d.ts +4 -0
  66. package/dist/nestjs/decorators.js +45 -0
  67. package/dist/nestjs/errors.d.ts +12 -0
  68. package/dist/nestjs/errors.js +20 -0
  69. package/dist/nestjs/evaluator.d.ts +17 -0
  70. package/dist/nestjs/evaluator.js +83 -0
  71. package/dist/nestjs/guard.d.ts +19 -0
  72. package/dist/nestjs/guard.js +63 -0
  73. package/dist/nestjs/interceptor.d.ts +10 -0
  74. package/dist/nestjs/interceptor.js +53 -0
  75. package/dist/nestjs/module.d.ts +6 -0
  76. package/dist/nestjs/module.js +67 -0
  77. package/dist/nestjs/service.d.ts +30 -0
  78. package/dist/nestjs/service.js +51 -0
  79. package/dist/nestjs/types.d.ts +100 -0
  80. package/dist/nestjs/types.js +1 -0
  81. package/dist/nestjs.cjs +1 -0
  82. package/dist/nestjs.d.ts +10 -0
  83. package/dist/nestjs.js +9 -0
  84. package/dist/openfeature/context.d.ts +10 -0
  85. package/dist/openfeature/context.js +160 -0
  86. package/dist/openfeature/hooks.d.ts +6 -0
  87. package/dist/openfeature/hooks.js +27 -0
  88. package/dist/openfeature/server-provider.d.ts +20 -0
  89. package/dist/openfeature/server-provider.js +102 -0
  90. package/dist/openfeature/server.cjs +1 -0
  91. package/dist/openfeature/server.d.ts +3 -0
  92. package/dist/openfeature/server.js +3 -0
  93. package/dist/openfeature/shared.d.ts +37 -0
  94. package/dist/openfeature/shared.js +74 -0
  95. package/dist/openfeature/web-provider.d.ts +27 -0
  96. package/dist/openfeature/web-provider.js +151 -0
  97. package/dist/openfeature/web.cjs +1 -0
  98. package/dist/openfeature/web.d.ts +3 -0
  99. package/dist/openfeature/web.js +3 -0
  100. package/dist/persistence.d.ts +39 -0
  101. package/dist/persistence.js +203 -0
  102. package/dist/react/hooks.d.ts +52 -0
  103. package/dist/react/hooks.js +78 -0
  104. package/dist/react/provider.d.ts +71 -0
  105. package/dist/react/provider.js +99 -0
  106. package/dist/react.cjs +1 -0
  107. package/dist/react.d.ts +2 -0
  108. package/dist/react.js +2 -0
  109. package/dist/remote-evaluator.d.ts +28 -0
  110. package/dist/remote-evaluator.js +158 -0
  111. package/dist/types.d.ts +56 -0
  112. package/dist/types.js +1 -0
  113. package/featureflags.config.schema.json +38 -0
  114. package/package.json +107 -0
package/README.md ADDED
@@ -0,0 +1,651 @@
1
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
2
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
3
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
4
+
5
+ - [@mergedapp/feature-flags](#mergedappfeature-flags)
6
+ - [Installation](#installation)
7
+ - [Quick Start](#quick-start)
8
+ - [1. Generate Typed Flags](#1-generate-typed-flags)
9
+ - [2. Create the Client](#2-create-the-client)
10
+ - [Using a Config File](#using-a-config-file)
11
+ - [Code Generation](#code-generation)
12
+ - [`FLAGS`](#flags)
13
+ - [`FlagValues`](#flagvalues)
14
+ - [`createClient(config)`](#createclientconfig)
15
+ - [Evaluation Model](#evaluation-model)
16
+ - [Durable snapshots](#durable-snapshots)
17
+ - [OpenFeature Integration](#openfeature-integration)
18
+ - [Web provider](#web-provider)
19
+ - [Server provider](#server-provider)
20
+ - [NestJS Integration](#nestjs-integration)
21
+ - [`createTypedHooks<FlagValues>()`](#createtypedhooksflagvalues)
22
+ - [Turbo Pipeline](#turbo-pipeline)
23
+ - [React Integration](#react-integration)
24
+ - [React Subpath](#react-subpath)
25
+ - [SSR Hydration](#ssr-hydration)
26
+ - [Configuration Options](#configuration-options)
27
+ - [Error Handling](#error-handling)
28
+ - [Audit & Cleanup](#audit--cleanup)
29
+ - [Audit](#audit)
30
+ - [Cleanup](#cleanup)
31
+ - [API Reference](#api-reference)
32
+ - [`MergedFeatureFlags<TFlags>`](#mergedfeatureflagstflags)
33
+ - [React Hooks (Generated)](#react-hooks-generated)
34
+ - [OpenFeature](#openfeature)
35
+ - [NestJS](#nestjs)
36
+ - [CLI Commands](#cli-commands)
37
+
38
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
39
+
40
+ # @mergedapp/feature-flags
41
+
42
+ > **Warning**
43
+ > This SDK is not ready for public use yet.
44
+ > The package is public, but you will not be able to finish setting up feature flags until the main app is available.
45
+ > We will do a proper launch soon.
46
+
47
+ Type-safe, generic feature flag client SDK with ES256 JWT verification, code generation, and React hooks.
48
+
49
+ Additional integrations now ship on dedicated subpaths:
50
+
51
+ - `@mergedapp/feature-flags/react`
52
+ - `@mergedapp/feature-flags/openfeature/web`
53
+ - `@mergedapp/feature-flags/openfeature/server`
54
+ - `@mergedapp/feature-flags/nestjs`
55
+
56
+ The SDK reads already-evaluated flag values for a specific organization and environment. Authoring concepts such as stable `codeKey`s, environment overrides, fallback values, targeting groups, percentage rollouts, and IP, country, region, or attribute targeting are configured in the dashboard and resolved by the API before values reach the client.
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ npm install @mergedapp/feature-flags
62
+ ```
63
+
64
+ If you want OpenFeature interoperability or NestJS helpers, install the matching peer packages too:
65
+
66
+ ```bash
67
+ npm install @mergedapp/feature-flags @openfeature/core @openfeature/web-sdk @openfeature/server-sdk
68
+ ```
69
+
70
+ ```bash
71
+ npm install @mergedapp/feature-flags @nestjs/common @nestjs/core rxjs
72
+ ```
73
+
74
+ ## Quick Start
75
+
76
+ ### 1. Generate Typed Flags
77
+
78
+ Run the CLI to fetch your flag definitions and generate a type-safe TypeScript file:
79
+
80
+ ```bash
81
+ npx merged-ff generate --api-url=<coming_soon> --client-key=lk_pub_your_key_here --organization-id=org_123
82
+ ```
83
+
84
+ This creates `./src/generated/feature-flags.ts` containing a `createClient` factory, typed hooks, and type definitions.
85
+
86
+ ### 2. Create the Client
87
+
88
+ ```typescript
89
+ import { createClient } from "./generated/feature-flags"
90
+
91
+ const client = createClient({
92
+ apiUrl: "<coming_soon>",
93
+ clientKey: "lk_pub_your_key_here",
94
+ organizationId: "org_123",
95
+ environmentId: "env_prod",
96
+ evaluationContext: {
97
+ attributes: {
98
+ user: {
99
+ plan: { code: "pro" },
100
+ role: "admin",
101
+ },
102
+ cart: {
103
+ items: [{ sku: "sku_123" }],
104
+ },
105
+ },
106
+ },
107
+ })
108
+
109
+ await client.initialize()
110
+
111
+ // Fully typed -- flag code keys are autocompleted, return types are inferred
112
+ if (client.isEnabled("enableDarkMode")) {
113
+ enableDarkMode()
114
+ }
115
+
116
+ const retries = client.getValue("maxRetries") // number | undefined
117
+
118
+ // Compile error -- "typo" is not a valid flag code key
119
+ client.isEnabled("typo") // TS Error: Argument of type '"typo"' is not assignable
120
+ ```
121
+
122
+ The generated file uses stable `codeKey` values as the SDK-facing identifiers. You can rename a flag's display name in the dashboard without breaking typed client lookups, but changing the `codeKey` is a breaking change for generated code.
123
+
124
+ ### Using a Config File
125
+
126
+ Create a `featureflags.config.json` in your project root. `featureflags.config.js`, `.mjs`, and `.cjs` are also supported:
127
+
128
+ ```json
129
+ {
130
+ "apiUrl": "<coming_soon>",
131
+ "clientKey": "lk_pub_your_key_here",
132
+ "organizationId": "org_123",
133
+ "outputPath": "./src/generated/feature-flags.ts"
134
+ }
135
+ ```
136
+
137
+ Then run without arguments:
138
+
139
+ ```bash
140
+ npx merged-ff generate
141
+ ```
142
+
143
+ Environment variables `FEATURE_FLAG_API_URL`, `FEATURE_FLAG_CLIENT_KEY`, and `FEATURE_FLAG_ORGANIZATION_ID` are also supported.
144
+
145
+ ## Code Generation
146
+
147
+ The `merged-ff generate` CLI produces a TypeScript file with these exports:
148
+
149
+ ### `FLAGS`
150
+
151
+ A constant object mapping stable code keys to stable UUIDs:
152
+
153
+ ```typescript
154
+ export const FLAGS = {
155
+ enableDarkMode: "550e8400-e29b-41d4-a716-446655440000",
156
+ maxUploadSize: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
157
+ } as const
158
+ ```
159
+
160
+ ### `FlagValues`
161
+
162
+ An interface mapping each flag code key to its TypeScript type:
163
+
164
+ ```typescript
165
+ export interface FlagValues {
166
+ enableDarkMode: boolean
167
+ maxUploadSize: number
168
+ }
169
+ ```
170
+
171
+ ### `createClient(config)`
172
+
173
+ Creates a `MergedFeatureFlags<FlagValues>` instance with `flagIds` pre-configured from the generated `FLAGS` mapping. Provide the API origin plus the organization and environment scope used for evaluation:
174
+
175
+ ```typescript
176
+ import { createClient } from "./generated/feature-flags"
177
+
178
+ const client = createClient({
179
+ apiUrl: "<coming_soon>",
180
+ clientKey: "lk_pub_your_key_here",
181
+ organizationId: "org_123",
182
+ environmentId: "env_prod",
183
+ })
184
+
185
+ await client.initialize()
186
+
187
+ client.isEnabled("enableDarkMode") // boolean -- autocompleted
188
+ client.getValue("maxUploadSize") // number | undefined -- inferred
189
+ client.isEnabled("nonExistent") // Compile error
190
+ ```
191
+
192
+ The client receives the final evaluated value for the configured organization and environment. If your dashboard configuration uses environment overrides, targeting groups, and percentage rollouts, the SDK sees the resolved result rather than the raw authoring definition.
193
+
194
+ If the active targeting rules or rollout bucketing depend on application attributes, pass them through `evaluationContext.attributes`. The server evaluates nested object paths like `user.plan.code` and indexed array paths like `cart.items[0].sku`.
195
+
196
+ ## Evaluation Model
197
+
198
+ Feature flag authoring happens in layers:
199
+
200
+ 1. **Flag default value** -- The base value on the flag definition.
201
+ 2. **Environment override** -- An environment-specific configuration with its own enabled state, fallback value, and optional rollout.
202
+ 3. **Targeting groups** -- Ordered groups inside an environment override. Each group can return a different value when its matchers apply and can also have its own optional rollout.
203
+
204
+ Targeting groups can currently use four matcher types:
205
+
206
+ - **IP range targeting** -- IPv4 or IPv6 CIDR ranges.
207
+ - **Country targeting** -- ISO 3166-1 alpha-2 country codes.
208
+ - **Region targeting** -- ISO 3166-2 subdivision codes.
209
+ - **Attribute targeting** -- Caller-provided paths resolved from `evaluationContext.attributes`.
210
+
211
+ When the server evaluates a request:
212
+
213
+ 1. It picks the current organization and environment scope.
214
+ 2. It applies the enabled environment override, if one exists.
215
+ 3. It evaluates targeting groups for that environment.
216
+ 4. Matchers can combine request-derived data such as IP, country, and region with caller-provided attributes from `evaluationContext`.
217
+ 5. If a matching targeting group has a percentage rollout, the server performs deterministic bucketing before returning that group's value.
218
+ 6. If multiple IP groups match, the most specific CIDR wins.
219
+ 7. If specificity ties, the earlier group wins.
220
+ 8. If no targeting group resolves a value, the environment fallback value is considered.
221
+ 9. If the environment fallback has a percentage rollout, the server performs deterministic bucketing before returning that value.
222
+ 10. If no environment override applies, the base flag default value is returned.
223
+
224
+ For attribute targeting, the SDK does not infer values automatically. Your application is responsible for building the context object and updating it when the request scope changes.
225
+
226
+ The SDK never performs client-side rollout bucketing. Percentage rollout is evaluated server-side so the same logic applies across browser, server, and internal API callers.
227
+
228
+ ```typescript
229
+ client.setEvaluationContext({
230
+ attributes: {
231
+ user: {
232
+ plan: { code: "enterprise" },
233
+ locale: "en-US",
234
+ },
235
+ },
236
+ })
237
+
238
+ await client.refresh()
239
+ ```
240
+
241
+ ## Durable snapshots
242
+
243
+ The SDK now persists the last successful signed evaluation automatically:
244
+
245
+ - browsers use `localStorage`
246
+ - non-browser runtimes use a local file store under the OS temp directory
247
+ - persistence is isolated by a scope key derived from `apiUrl`, org, environment, team, client key fingerprint, and a canonicalized `evaluationContext` hash
248
+
249
+ If the flag service is unavailable later, the SDK restores the last successful snapshot for the exact same scope and keeps serving it until a newer one replaces it.
250
+
251
+ You can disable or override persistence:
252
+
253
+ ```typescript
254
+ import { createClient, createFileFeatureFlagSnapshotStore } from "@mergedapp/feature-flags"
255
+
256
+ const client = createClient({
257
+ apiUrl: "<coming_soon>",
258
+ clientKey: "lk_pub_your_key_here",
259
+ organizationId: "org_123",
260
+ environmentId: "env_prod",
261
+ snapshotPersistence: {
262
+ store: createFileFeatureFlagSnapshotStore(),
263
+ keyPrefix: "my-app-flags",
264
+ },
265
+ })
266
+ ```
267
+
268
+ Set `snapshotPersistence: false` to disable durable snapshots entirely.
269
+
270
+ ## OpenFeature Integration
271
+
272
+ The native lube SDK remains the primary API. OpenFeature support is additive for teams that already standardize on
273
+ OpenFeature.
274
+
275
+ Use separate runtime-specific subpaths because OpenFeature itself publishes different provider contracts for browser and
276
+ server runtimes:
277
+
278
+ - `@mergedapp/feature-flags/openfeature/web`
279
+ - `@mergedapp/feature-flags/openfeature/server`
280
+
281
+ ### Web provider
282
+
283
+ ```typescript
284
+ import { OpenFeature } from "@openfeature/web-sdk"
285
+ import { createOpenFeatureWebProvider } from "@mergedapp/feature-flags/openfeature/web"
286
+
287
+ await OpenFeature.setProviderAndWait(
288
+ createOpenFeatureWebProvider({
289
+ apiUrl: "<coming_soon>",
290
+ clientKey: "lk_pub_your_key_here",
291
+ organizationId: "org_123",
292
+ environmentId: "env_prod",
293
+ }),
294
+ )
295
+ ```
296
+
297
+ ### Server provider
298
+
299
+ ```typescript
300
+ import { OpenFeature } from "@openfeature/server-sdk"
301
+ import { createOpenFeatureServerProvider } from "@mergedapp/feature-flags/openfeature/server"
302
+
303
+ await OpenFeature.setProviderAndWait(
304
+ createOpenFeatureServerProvider({
305
+ apiUrl: "<coming_soon>",
306
+ clientKey: "lk_sec_your_key_here",
307
+ organizationId: "org_123",
308
+ environmentId: "env_prod",
309
+ }),
310
+ )
311
+ ```
312
+
313
+ The default context mapper:
314
+
315
+ - maps OpenFeature `targetingKey` to `evaluationContext.attributes.subject.key`
316
+ - copies other OpenFeature fields into `evaluationContext.attributes`
317
+ - converts `Date` values to ISO strings
318
+ - merges request context over any provider-level base `evaluationContext`
319
+
320
+ The package also exports helper hooks:
321
+
322
+ - `createStaticContextHook()`
323
+ - `createLoggingHook()`
324
+
325
+ Tracking is intentionally not supported yet.
326
+
327
+ ## NestJS Integration
328
+
329
+ Use the `@mergedapp/feature-flags/nestjs` subpath when you want Nest-native decorators, guards, interceptors, or an
330
+ injectable feature-flag service.
331
+
332
+ ```typescript
333
+ import { Module } from "@nestjs/common"
334
+ import { FeatureFlagsModule } from "@mergedapp/feature-flags/nestjs"
335
+
336
+ @Module({
337
+ imports: [
338
+ FeatureFlagsModule.forRoot({
339
+ apiUrl: "<coming_soon>",
340
+ clientKey: "lk_sec_your_key_here",
341
+ organizationId: "org_123",
342
+ environmentId: "env_prod",
343
+ }),
344
+ ],
345
+ })
346
+ export class AppModule {}
347
+ ```
348
+
349
+ The Nest layer is native-only and evaluates directly through the Merged SDK. If you want OpenFeature inside NestJS, use the OpenFeature Nest integration separately and point it at `@mergedapp/feature-flags/openfeature/server`.
350
+
351
+ Exports include:
352
+
353
+ - `FeatureFlagsModule`
354
+ - `FeatureFlagsService`
355
+ - `RequireFeatureFlag()` for controllers/routes
356
+ - `FeatureFlagGate()` for service methods
357
+ - `FeatureFlagContextInterceptor`
358
+ - `FeatureFlagContext`
359
+
360
+ Example controller gate:
361
+
362
+ ```typescript
363
+ import { Controller, Get } from "@nestjs/common"
364
+ import { RequireFeatureFlag } from "@mergedapp/feature-flags/nestjs"
365
+
366
+ @Controller("beta")
367
+ export class BetaController {
368
+ @Get()
369
+ @RequireFeatureFlag({ flagKey: "enableBetaDashboard" })
370
+ list() {
371
+ return { ok: true }
372
+ }
373
+ }
374
+ ```
375
+
376
+ Example service-method gate:
377
+
378
+ ```typescript
379
+ import { Injectable } from "@nestjs/common"
380
+ import { FeatureFlagGate, FeatureFlagsService } from "@mergedapp/feature-flags/nestjs"
381
+
382
+ @Injectable()
383
+ export class BillingService {
384
+ constructor(public readonly featureFlags: FeatureFlagsService) {}
385
+
386
+ @FeatureFlagGate({ flagKey: "enableNewBillingFlow" })
387
+ async charge() {
388
+ return { ok: true }
389
+ }
390
+ }
391
+ ```
392
+
393
+ ### `createTypedHooks<FlagValues>()`
394
+
395
+ Generates fully typed React hooks. The generated file exports pre-built hooks:
396
+
397
+ ```typescript
398
+ export const { FeatureFlagProvider, useFeatureFlag, useFeatureFlags, useFeatureFlagClient, useFeatureFlagStatus } =
399
+ createTypedHooks<FlagValues>()
400
+ ```
401
+
402
+ ## Turbo Pipeline
403
+
404
+ Add the generate command to your `turbo.json` so flags are regenerated before builds:
405
+
406
+ ```json
407
+ {
408
+ "tasks": {
409
+ "generate:flags": {
410
+ "inputs": ["featureflags.config.json"],
411
+ "outputs": ["src/generated/feature-flags.ts"],
412
+ "cache": false
413
+ },
414
+ "build": {
415
+ "dependsOn": ["generate:flags"]
416
+ }
417
+ }
418
+ }
419
+ ```
420
+
421
+ ## React Integration
422
+
423
+ Use the generated typed hooks for full type safety:
424
+
425
+ ```tsx
426
+ import { createClient } from "./generated/feature-flags"
427
+ import { FeatureFlagProvider, useFeatureFlag, useFeatureFlags } from "./generated/feature-flags"
428
+
429
+ const client = createClient({
430
+ apiUrl: "<coming_soon>",
431
+ clientKey: "lk_pub_your_key_here",
432
+ organizationId: "org_123",
433
+ environmentId: "env_prod",
434
+ evaluationContext: {
435
+ attributes: {
436
+ user: {
437
+ plan: { code: "pro" },
438
+ },
439
+ },
440
+ },
441
+ })
442
+
443
+ function App() {
444
+ return (
445
+ <FeatureFlagProvider blockUntilReady={false} client={client}>
446
+ <Dashboard />
447
+ </FeatureFlagProvider>
448
+ )
449
+ }
450
+
451
+ function Dashboard() {
452
+ // Fully typed: "enableDarkMode" is autocompleted, value is boolean | undefined
453
+ const { enabled, value } = useFeatureFlag("enableDarkMode")
454
+ const allFlags = useFeatureFlags()
455
+
456
+ if (enabled) {
457
+ return <DarkDashboard />
458
+ }
459
+
460
+ return <LightDashboard />
461
+ }
462
+ ```
463
+
464
+ The provider calls `client.initialize()` on mount and `client.destroy()` on unmount automatically. Flags update reactively via `useSyncExternalStore`. `blockUntilReady` is required so each integration chooses whether the app should render immediately or wait for the first flag payload.
465
+
466
+ ### React Subpath
467
+
468
+ The `@mergedapp/feature-flags/react` subpath is intentionally low-level. It exports `FeatureFlagProvider` and `createTypedHooks()` so generated bindings can be created, but applications should import `useFeatureFlag`, `useFeatureFlags`, `useFeatureFlagClient`, and `useFeatureFlagStatus` from their generated `./generated/feature-flags` file.
469
+
470
+ ## SSR Hydration
471
+
472
+ Pass pre-evaluated flags to avoid a fetch on the server:
473
+
474
+ ```tsx
475
+ function App({ serverFlags }: { serverFlags: EvaluatedFlag[] }) {
476
+ return (
477
+ <FeatureFlagProvider blockUntilReady={false} client={client} initialFlags={serverFlags}>
478
+ <Dashboard />
479
+ </FeatureFlagProvider>
480
+ )
481
+ }
482
+ ```
483
+
484
+ When `initialFlags` is provided, the provider renders with those values immediately. Once the client initializes on the client side, live flags take over seamlessly.
485
+
486
+ ## Configuration Options
487
+
488
+ | Option | Type | Default | Description |
489
+ | --------------------- | ------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
490
+ | `apiUrl` | `string` | (required) | Base URL for the feature flag API |
491
+ | `clientKey` | `string` | (required) | API key with `lk_pub_*` or `lk_sec_*` prefix |
492
+ | `organizationId` | `string` | (required) | Organization scope sent to the signed evaluation endpoint |
493
+ | `environmentId` | `string` | (required) | Environment scope sent to the signed evaluation endpoint |
494
+ | `teamId` | `string` | - | Optional team scope sent to the signed evaluation endpoint |
495
+ | `publicKey` | `string` | auto-fetch | PEM-format ES256 public key for JWT verification |
496
+ | `refreshInterval` | `number` | `60000` | Polling interval in ms. Set to `0` to disable polling. |
497
+ | `snapshotPersistence` | `false \| { store?: FeatureFlagSnapshotStore; keyPrefix?: string }` | auto | Durable last-known-good snapshot persistence. Browser defaults to `localStorage`; non-browser defaults to a file store in the OS temp dir. |
498
+ | `evaluationContext` | `FeatureFlagEvaluationContext` | - | Optional caller-provided context used for attribute targeting |
499
+ | `flagIds` | `Record<string, string>` | - | Mapping of stable code keys to flag IDs. Auto-configured when using generated `createClient`. |
500
+ | `onError` | `(error: Error) => void` | - | Called when a refresh or verification error occurs |
501
+ | `onFlagsChanged` | `(flags: EvaluatedFlag[]) => void` | - | Called when flag values change after a refresh |
502
+
503
+ `evaluationContext` uses this shape:
504
+
505
+ ```typescript
506
+ type FeatureFlagEvaluationContext = {
507
+ attributes?: Record<string, unknown>
508
+ }
509
+ ```
510
+
511
+ Recommended usage:
512
+
513
+ - Put application-owned targeting inputs in `attributes`.
514
+ - Use nested objects for stable domain structure, for example `user.plan.code`.
515
+ - Use arrays only when position matters or when you plan to target with list membership.
516
+ - Treat client-provided values as targeting inputs, not as authorization guarantees.
517
+
518
+ ## Error Handling
519
+
520
+ The SDK exports three error classes:
521
+
522
+ - **`FeatureFlagError`** -- Base class for all SDK errors.
523
+ - **`FeatureFlagNetworkError`** -- Thrown when the API is unreachable or returns a non-2xx status.
524
+ - **`FeatureFlagVerificationError`** -- Thrown when JWT verification fails (expired, wrong issuer, tampered).
525
+
526
+ On refresh failure, the client applies exponential backoff starting at 5 seconds, capped at 5 minutes. The `onError` callback is invoked on every failure. Existing flags remain available during outages, including restored persisted snapshots when the evaluation scope matches exactly.
527
+
528
+ **Browser tab visibility:** Polling is automatically paused when the tab is hidden and resumed with an immediate refresh when it becomes visible again. This prevents unnecessary server requests from background tabs.
529
+
530
+ ```typescript
531
+ import { FeatureFlagNetworkError, FeatureFlagVerificationError } from "@mergedapp/feature-flags"
532
+ import { createClient } from "./generated/feature-flags"
533
+
534
+ const client = createClient({
535
+ apiUrl: "<coming_soon>",
536
+ clientKey: "lk_pub_your_key_here",
537
+ organizationId: "org_123",
538
+ environmentId: "env_prod",
539
+ onError: (error) => {
540
+ if (error instanceof FeatureFlagNetworkError) {
541
+ console.warn("Flag service unreachable, using cached flags")
542
+ }
543
+ if (error instanceof FeatureFlagVerificationError) {
544
+ console.error("Flag token verification failed", error)
545
+ }
546
+ },
547
+ })
548
+ ```
549
+
550
+ ## Audit & Cleanup
551
+
552
+ ### Audit
553
+
554
+ Scan your codebase for unused or stale flag references:
555
+
556
+ ```bash
557
+ npx merged-ff audit --api-url=<coming_soon> --client-key=lk_pub_xxx --dir=./src
558
+ ```
559
+
560
+ Produces a report showing active, unused, and archived-but-still-referenced flags.
561
+
562
+ ### Cleanup
563
+
564
+ Automatically replace archived boolean flag checks with `false`:
565
+
566
+ ```bash
567
+ # Preview changes without modifying files
568
+ npx merged-ff cleanup --api-url=<coming_soon> --client-key=lk_pub_xxx --dry-run
569
+
570
+ # Apply changes
571
+ npx merged-ff cleanup --api-url=<coming_soon> --client-key=lk_pub_xxx
572
+ ```
573
+
574
+ After cleanup, run `merged-ff generate` to update the generated file.
575
+
576
+ ## API Reference
577
+
578
+ ### `MergedFeatureFlags<TFlags>`
579
+
580
+ The core client class is generic: `MergedFeatureFlags<TFlags extends FlagRegistry = FlagRegistry>`. When using generated `createClient`, the type parameter is pre-filled.
581
+
582
+ | Method | Returns | Description |
583
+ | ----------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
584
+ | `initialize()` | `Promise<void>` | Fetch public key (if needed), fetch and verify flags |
585
+ | `isEnabled<K extends string & keyof TFlags>(name: K)` | `boolean` | Check if a flag is enabled (false for unknown flags) |
586
+ | `getValue<K extends string & keyof TFlags>(name: K)` | `TFlags[K] \| undefined` | Get a flag's value with inferred return type |
587
+ | `getFlag(idOrName: string)` | `EvaluatedFlag \| undef` | Get the full evaluated flag entry |
588
+ | `getAllFlags()` | `EvaluatedFlag[]` | Get all evaluated flags |
589
+ | `refresh()` | `Promise<void>` | Manually trigger a flag refresh from the server |
590
+ | `getStatus()` | `FeatureFlagRuntimeStatus` | Read snapshot source, staleness, and last refresh metadata |
591
+ | `setEvaluationContext(context)` | `void` | Replace the caller-provided attribute context for future refreshes; the current snapshot stays active until the next refresh |
592
+ | `onChange(listener)` | `() => void` | Subscribe to flag changes; returns unsubscribe function |
593
+ | `destroy()` | `void` | Clean up timers, listeners, and cached data |
594
+
595
+ ### React Hooks (Generated)
596
+
597
+ When using hooks from the generated file (via `createTypedHooks<FlagValues>()`), all hooks are fully typed:
598
+
599
+ | Hook | Returns | Description |
600
+ | ------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
601
+ | `useFeatureFlag(name)` | `{ enabled: boolean, value: TFlags[K] \| undefined }` | Get a single flag's typed state |
602
+ | `useFeatureFlags()` | `EvaluatedFlag[]` | Get all flags |
603
+ | `useFeatureFlagClient()` | `MergedFeatureFlags<TFlags>` | Access the underlying typed client |
604
+ | `useFeatureFlagStatus()` | `{ status, isLoading, isReady, error, source, isStale, lastSuccessfulRefreshAt, tokenExpiresAt }` | Read provider initialization and snapshot-source state |
605
+
606
+ There is no public untyped React hook entrypoint. React applications are expected to consume the generated bindings so invalid flag code keys fail at compile time.
607
+
608
+ ### OpenFeature
609
+
610
+ OpenFeature integrations live on dedicated runtime entrypoints:
611
+
612
+ - `@mergedapp/feature-flags/openfeature/web`
613
+ - `@mergedapp/feature-flags/openfeature/server`
614
+
615
+ Use the web entrypoint with `@openfeature/web-sdk` and the server entrypoint with `@openfeature/server-sdk`. The providers support:
616
+
617
+ - Merged evaluation through OpenFeature
618
+ - OpenFeature domains
619
+ - server transaction context
620
+ - hook helpers via `createLoggingHook` and `createStaticContextHook`
621
+
622
+ OpenFeature hooks are evaluation lifecycle middleware. They let you run shared logic before evaluation, after evaluation, on error, and finally. In this SDK, the bundled helpers are meant for two common cases:
623
+
624
+ - `createStaticContextHook` to attach shared context once instead of repeating it at every callsite
625
+ - `createLoggingHook` to observe evaluation outcomes and failures consistently
626
+
627
+ Tracking is not implemented yet.
628
+
629
+ ### NestJS
630
+
631
+ NestJS integration lives at `@mergedapp/feature-flags/nestjs`.
632
+
633
+ It exposes:
634
+
635
+ - `FeatureFlagsModule`
636
+ - `FeatureFlagsService`
637
+ - `RequireFeatureFlag`
638
+ - `FeatureFlagGate`
639
+ - `FeatureFlagGuard`
640
+ - `FeatureFlagContextInterceptor`
641
+ - `FeatureFlagContext`
642
+
643
+ The Nest module is native-only. It evaluates through the Merged SDK and keeps type-safe decorators, guards, interceptors, and generated bindings aligned with the generated registry.
644
+
645
+ ### CLI Commands
646
+
647
+ | Command | Description |
648
+ | ---------- | --------------------------------------------------- |
649
+ | `generate` | Generate typed TypeScript SDK from flag definitions |
650
+ | `audit` | Scan codebase for unused or stale flag references |
651
+ | `cleanup` | Replace archived boolean flag checks with `false` |