Skip to content

Commit 32d0ecf

Browse files
authored
chore: improve node-sdk structure for overrides slightly (#566)
1 parent 8bc9130 commit 32d0ecf

5 files changed

Lines changed: 60 additions & 84 deletions

File tree

.changeset/floppy-webs-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@reflag/node-sdk": patch
3+
---
4+
5+
docs: improve override docs

packages/node-sdk/README.md

Lines changed: 52 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@ You can also [use the HTTP API directly](https://docs.reflag.com/api/http-api)
5959
To get started you need to obtain your secret key from the [environment settings](https://app.reflag.com/env-current/settings/app-environments)
6060
in Reflag.
6161

62-
> [!CAUTION]
63-
> Secret keys are meant for use in server side SDKs only. Secret keys offer the users the ability to obtain
64-
> information that is often sensitive and thus should not be used in client-side applications.
65-
6662
Reflag will load settings through the various environment variables automatically (see [Configuring](#configuring) below).
6763

6864
1. Find the Reflag secret key for your development environment under [environment settings](https://app.reflag.com/env-current/settings/app-environments) in Reflag.
@@ -91,7 +87,7 @@ Once the client is initialized, you can obtain flags along with the `isEnabled`
9187
status to indicate whether the flag is targeted for this user/company:
9288

9389
> [!IMPORTANT]
94-
> If `user.id` or `company.id` is not given, the whole `user` or `company` object is ignored.
90+
> If `user.id` is not given, the whole `user` object is ignore. Similarly, without `company.id` the `company` object is ignored.
9591
9692
```typescript
9793
// configure the client
@@ -203,6 +199,11 @@ const flagDefs = await client.getFlagDefinitions();
203199

204200
`flagsFallbackProvider` is a reliability feature that lets the SDK persist the latest successfully fetched raw flag definitions to fallback storage such as a local file, Redis, S3, GCS, or a custom backend.
205201

202+
> [!NOTE]
203+
>
204+
> `fallbackFlags` is deprecated. Prefer `flagsFallbackProvider` for startup fallback and outage recovery.
205+
> `flagsFallbackProvider` is not used in offline mode.
206+
206207
#### How it works
207208

208209
Reflag servers remain the primary source of truth. On `initialize()`, the SDK always tries to fetch a live copy of the flag definitions first, and it continues refreshing those definitions from the Reflag servers over time.
@@ -240,48 +241,50 @@ You can access the built-in providers through the `fallbackProviders` namespace:
240241
- `fallbackProviders.s3(...)`
241242
- `fallbackProviders.gcs(...)`
242243

243-
##### File provider
244+
##### Static provider
245+
246+
If you just want a fixed fallback copy of simple enabled/disabled flags, you can provide a static map:
244247

245248
```typescript
246249
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
247250

248251
const client = new ReflagClient({
249252
secretKey: process.env.REFLAG_SECRET_KEY,
250-
flagsFallbackProvider: fallbackProviders.file({
251-
directory: ".reflag",
253+
flagsFallbackProvider: fallbackProviders.static({
254+
flags: {
255+
huddle: true,
256+
"smart-summaries": false,
257+
},
252258
}),
253259
});
254260

255261
await client.initialize();
256262
```
257263

258-
The file provider stores one snapshot file per environment in the configured
259-
`directory`.
260-
261-
##### Static provider
262-
263-
If you just want a fixed fallback copy of simple enabled/disabled flags, you can provide a static map:
264+
##### File provider
264265

265266
```typescript
266267
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
267268

268269
const client = new ReflagClient({
269270
secretKey: process.env.REFLAG_SECRET_KEY,
270-
flagsFallbackProvider: fallbackProviders.static({
271-
flags: {
272-
huddle: true,
273-
"smart-summaries": false,
274-
},
271+
flagsFallbackProvider: fallbackProviders.file({
272+
directory: ".reflag",
275273
}),
276274
});
277275

278276
await client.initialize();
279277
```
280278

279+
The file provider stores one snapshot file per environment in the configured
280+
`directory`.
281+
281282
##### Redis provider
282283

283284
The built-in Redis provider creates a Redis client automatically when omitted and uses `REDIS_URL` from the environment. It stores snapshots under the configured `keyPrefix` and uses the first 16 characters of the secret key hash in the Redis key.
284285

286+
Without a `keyPrefix` set, it will default to to the key `reflag:flags-fallback:${secretKeyHash}`.
287+
285288
```typescript
286289
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
287290

@@ -297,13 +300,15 @@ await client.initialize();
297300

298301
The built-in S3 provider works out of the box using the AWS SDK's default credential chain and region resolution. It stores the snapshot object under the configured `keyPrefix` and uses a hash of the secret key in the object name.
299302

303+
Without a `keyPrefix` set, it will default to path `reflag/flags-fallback/${secretKeyHash}`.
304+
300305
```typescript
301306
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
302307

303308
const client = new ReflagClient({
304309
secretKey: process.env.REFLAG_SECRET_KEY,
305310
flagsFallbackProvider: fallbackProviders.s3({
306-
bucket: process.env.REFLAG_SNAPSHOT_BUCKET!,
311+
bucket: "reflag-fallback-bucket",
307312
}),
308313
});
309314

@@ -314,13 +319,15 @@ await client.initialize();
314319

315320
The built-in GCS provider works out of the box using Google Cloud's default application credentials. It stores the snapshot object under the configured `keyPrefix` and uses a hash of the secret key in the object name.
316321

322+
Without a `keyPrefix` set, it will default to path `reflag/flags-fallback/${secretKeyHash}`.
323+
317324
```typescript
318325
import { ReflagClient, fallbackProviders } from "@reflag/node-sdk";
319326

320327
const client = new ReflagClient({
321328
secretKey: process.env.REFLAG_SECRET_KEY,
322329
flagsFallbackProvider: fallbackProviders.gcs({
323-
bucket: process.env.REFLAG_SNAPSHOT_BUCKET!,
330+
bucket: "reflag-fallback-bucket",
324331
}),
325332
});
326333

@@ -329,49 +336,32 @@ await client.initialize();
329336

330337
#### Testing fallback startup locally
331338

332-
To test fallback startup in your own app, first run it once with a working Reflag connection so a snapshot is saved. Then restart it with the same secret key and fallback provider configuration, but set `apiBaseUrl` to `http://127.0.0.1:65535`. That forces the live fetch to fail and lets you verify that the SDK initializes from the saved snapshot instead.
339+
To test fallback startup in your own app, first run it once with a working Reflag connection so a snapshot is saved. Then restart it with the same secret key and fallback provider configuration, but set `apiBaseUrl` (or set the `REFLAG_API_BASE_URL` environment variable) to `http://127.0.0.1:65535`. That forces the live fetch to fail and lets you verify that the SDK initializes from the saved snapshot instead.
333340

334341
#### Writing a custom provider
335342

336-
If you just want a fixed fallback copy of the flag definitions, a custom provider can be very small:
343+
If you just store definitions in your database or similar, a custom provider can be very small:
337344

338345
```typescript
339346
import type {
340347
FlagsFallbackProvider,
341348
FlagsFallbackSnapshot,
342349
} from "@reflag/node-sdk";
343350

344-
const fallbackSnapshot: FlagsFallbackSnapshot = {
345-
version: 1,
346-
savedAt: "2026-03-10T00:00:00.000Z",
347-
flags: [
348-
{
349-
key: "huddle",
350-
description: "Fallback example",
351-
targeting: {
352-
version: 1,
353-
rules: [],
354-
},
355-
},
356-
],
357-
};
358-
359-
export const staticFallbackProvider: FlagsFallbackProvider = {
360-
async load() {
361-
return fallbackSnapshot;
351+
export const customFallbackProvider: FlagsFallbackProvider = {
352+
async load(context) {
353+
// load snapshot from database
354+
// optionally, look up the snapshot using the context.secretKeyHash as a key
355+
return snapshot;
362356
},
363357

364-
async save() {
365-
// no-op
358+
async save(context, snapshot) {
359+
const serialized = JSON.stringify(snapshot);
360+
// write serialized snapshot to database, optionally using context.secretKeyHash as a key
366361
},
367362
};
368363
```
369364

370-
> [!NOTE]
371-
>
372-
> `fallbackFlags` is deprecated. Prefer `flagsFallbackProvider` for startup fallback and outage recovery.
373-
> `flagsFallbackProvider` is not used in offline mode.
374-
375365
## Bootstrapping client-side applications
376366

377367
The `getFlagsForBootstrap()` method is useful whenever you need to pass flag data to another runtime or serialize it without wrapper functions. Server-side rendering (SSR) is a common example, but it is also useful for other bootstrapping and hydration flows.
@@ -666,9 +656,9 @@ reflagClient.initialize().then(() => {
666656

667657
![Config type check failed](docs/type-check-payload-failed.png "Remote config type check failed")
668658

669-
## Testing
659+
## Testing with flag overrides
670660

671-
When writing tests that cover code with flags, you can toggle flags on/off programmatically to test different behavior. For tests, you will often want to run the client in offline mode and provide flag overrides directly through the client options.
661+
When writing tests that cover code with flags, you can toggle flags on/off programmatically to test different behavior. For tests, you will often want to run the client in offline mode:
672662

673663
`reflag.ts`:
674664

@@ -680,7 +670,11 @@ export const reflag = new ReflagClient({
680670
});
681671
```
682672

683-
You can then set base overrides for a test run by passing `flagOverrides` in the constructor, replacing them later with `setFlagOverrides()`, or clearing them with `clearFlagOverrides()`:
673+
There are a few ways to programmatically manipulate the overrides which are appropriate when testing:
674+
675+
### Base overrides
676+
677+
You can set base overrides for a test run by passing `flagOverrides` in the constructor, replacing them later with `setFlagOverrides()` and clearing them with `clearFlagOverrides()`:
684678

685679
```typescript
686680
// pass directly in the constructor
@@ -719,6 +713,8 @@ describe("API Tests", () => {
719713
});
720714
```
721715

716+
### Layering overrides
717+
722718
`pushFlagOverrides()` serves a different purpose: it adds a temporary layer on top of the base overrides and returns a remove function that removes only that layer. This is useful for nested tests:
723719

724720
```typescript
@@ -755,7 +751,9 @@ The precedence is:
755751

756752
If the same flag is set in both places, the pushed override wins until its remove function is called.
757753

758-
`pushFlagOverrides()` also accepts a function if the temporary override depends on the evaluation context:
754+
### Context dependent overrides
755+
756+
`setFlagOverrides()` and `pushFlagOverrides()` also accept a function if the override depends on the evaluation context:
759757

760758
```typescript
761759
const remove = client.pushFlagOverrides((context) => ({
@@ -767,13 +765,9 @@ const remove = client.pushFlagOverrides((context) => ({
767765
remove();
768766
```
769767

770-
## Flag Overrides
771-
772-
Flag overrides allow you to override flags and their configurations locally. This is particularly useful when testing changes locally, for example when running your app and clicking around to verify behavior before deploying your changes.
768+
### Additional ways to provide flag overrides
773769

774-
For automated tests, see the [Testing](#testing) section above.
775-
776-
When testing locally during development, you also have these additional ways to provide overrides:
770+
You also have these additional ways to provide overrides, which can be helpful when testing out locally:
777771

778772
1. Through environment variables:
779773

@@ -801,29 +795,6 @@ REFLAG_FLAGS_DISABLED=flag3,flag4
801795
}
802796
```
803797

804-
To get dynamic overrides, use a function which takes a context and returns a boolean or an object with the shape of `{isEnabled, config}`:
805-
806-
```typescript
807-
import { ReflagClient, Context } from "@reflag/node-sdk";
808-
809-
const flagOverrides = (context: Context) => ({
810-
"delete-todos": {
811-
isEnabled: true,
812-
config: {
813-
key: "dev-config",
814-
payload: {
815-
requireConfirmation: true,
816-
maxDeletionsPerDay: 5,
817-
},
818-
},
819-
},
820-
});
821-
822-
const client = new ReflagClient({
823-
flagOverrides,
824-
});
825-
```
826-
827798
## Remote Flag Evaluation
828799

829800
In addition to local flag evaluation, Reflag supports remote evaluation using stored context. This is useful when you want to evaluate flags using user/company attributes that were previously sent to Reflag:

packages/node-sdk/src/flagsFallbackProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ export function createStaticFallbackProvider({
206206
return {
207207
async load() {
208208
return {
209-
version: 1,
209+
version: 0,
210210
savedAt: new Date().toISOString(),
211211
flags: Object.entries(flags).map(([key, isEnabled]) =>
212212
staticFlagApiResponse(key, isEnabled),

packages/node-sdk/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ export type FlagsFallbackSnapshot = {
385385
/**
386386
* Snapshot schema version.
387387
*/
388-
version: 1;
388+
version: number;
389389

390390
/**
391391
* ISO timestamp indicating when the snapshot was saved.

packages/node-sdk/test/flagsFallbackProvider.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe("flagsFallbackProvider", () => {
4545
});
4646

4747
await expect(provider.load(context)).resolves.toEqual({
48-
version: 1,
48+
version: 0,
4949
savedAt: expect.any(String),
5050
flags: [
5151
{

0 commit comments

Comments
 (0)