Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
29e0fc1
new: REST API SDK
roncohen Feb 7, 2026
506d07a
Document SDK return shapes
roncohen Feb 9, 2026
3fb71b5
Simplify rest-api-sdk flag update API surface
roncohen Feb 21, 2026
3abf580
Customize generator to flatten SDK request body params
roncohen Feb 21, 2026
7d80b18
Update rest-api-sdk README
roncohen Feb 21, 2026
61e6cbf
Flatten all SDK request body params and regenerate from production spec
roncohen Feb 27, 2026
3debc31
Use extends for flattened body params and update demo app for renamed…
roncohen Mar 2, 2026
6dac9b8
Improve demo app flag toggling UX
roncohen Mar 2, 2026
6400100
Update README for renamed specificTargetValue field and remove basePa…
roncohen Mar 2, 2026
db2dbed
fix rest-api-sdk workflow regressions and app-scoped middleware chaining
roncohen Mar 3, 2026
d85dfd5
docs: mark REST API SDK as beta
roncohen Mar 3, 2026
a62c23d
Merge origin/main into rest-api
roncohen Mar 3, 2026
f8eec77
chore: remove reflag-openapi spec file
roncohen Mar 3, 2026
e21c2bf
show demo image
roncohen Mar 3, 2026
94d32ee
feat(example): auto-apply app/env filters in customer admin panel
roncohen Mar 3, 2026
037ad4f
chore(rest-api-sdk): update README
roncohen Mar 3, 2026
c6bf330
chore: bump version
roncohen Mar 3, 2026
d7a6993
fix(example): avoid build-time API key requirement in CI
roncohen Mar 3, 2026
e1149c8
fix(browser-sdk): avoid localStorage crash during SSR
roncohen Mar 3, 2026
bccd7eb
fix(browser-sdk): use noop storage fallback on server
roncohen Mar 3, 2026
0865e4b
Revert "fix(browser-sdk): use noop storage fallback on server"
roncohen Mar 3, 2026
3ec6cf5
Merge remote-tracking branch 'origin/main' into rest-api
roncohen Mar 3, 2026
e5d84b1
fix(nextjs-bootstrap-demo): default to offline without secret key
roncohen Mar 3, 2026
2e6c93a
docs(rest-api-sdk): update API key wording and flags surface
roncohen Mar 3, 2026
de3e0fa
chore: run prettier fix and ignore .next outputs
roncohen Mar 3, 2026
cd1cf2d
fix(rest-api-sdk): preserve middleware instances in app-scoped client
roncohen Mar 3, 2026
3b4ec7d
style(rest-api-sdk): format api scoping changes
roncohen Mar 3, 2026
75b4b1a
fix(rest-api-sdk): tighten API key handling and add error mapping tests
roncohen Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
**/gen
**/node_modules
**/dist
**/.next
**/.expo
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ Use this for Cloudflare Workers as well.

[Read the docs](packages/node-sdk/README.md)

## REST API SDK (beta)

Typed SDK for Reflag's REST API.

[Read the docs](packages/rest-api-sdk/README.md)

## Reflag CLI

CLI to interact with Reflag and generate types
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"license": "MIT",
"workspaces": [
"packages/*",
"packages/rest-api-sdk/examples/*",
"packages/react-sdk/dev/*",
"packages/react-native-sdk/dev/*",
"packages/openfeature-browser-provider/example"
],
Expand Down
4 changes: 2 additions & 2 deletions packages/react-sdk/dev/nextjs-bootstrap-demo/app/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReflagClient as ReflagNodeClient } from "@reflag/node-sdk";

const secretKey = process.env.REFLAG_SECRET_KEY || "";
const offline = process.env.CI === "true";
const secretKey = process.env.REFLAG_SECRET_KEY;
const offline = process.env.CI === "true" || !secretKey;

declare global {
var serverClient: ReflagNodeClient;
Expand Down
3 changes: 3 additions & 0 deletions packages/rest-api-sdk/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
src/generated/
**/.next/
315 changes: 315 additions & 0 deletions packages/rest-api-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
# @reflag/rest-api-sdk (beta)

Typed SDK for Reflag's REST API.

## Installation

```bash
npm install @reflag/rest-api-sdk
# or
yarn add @reflag/rest-api-sdk
```

## Create a client

Initialize the SDK with a [Reflag REST API Key](https://app.reflag.com/env-current/settings/org-api-access).

```typescript
import { Api } from "@reflag/rest-api-sdk";

const api = new Api({
accessToken: process.env.REFLAG_API_KEY,
});
```

## API surface

Main exports:

- `Api`: base client
- `createAppClient(appId, config)`: app-scoped client
- `ReflagApiError`: normalized API error type
- Generated request/response types and models from `@reflag/rest-api-sdk`

Core method groups:

- Applications: `listApps`, `getApp`
- Environments: `listEnvironments`, `getEnvironment`
- Flags: `listFlags`, `createFlag`, `updateFlag`
- User/company evaluation: `getUserFlags`, `updateUserFlags`, `getCompanyFlags`, `updateCompanyFlags`

## Quick start

```typescript
const apps = await api.listApps();
console.log(apps.data);
// [
// {
// "org": { "id": "org-1", "name": "Acme Org" },
// "id": "app-123",
// "name": "Acme App",
// "demo": false,
// "flagKeyFormat": "kebabCaseLower",
// "environments": [
// { "id": "env-123", "name": "Development", "isProduction": false, "order": 0 },
// { "id": "env-456", "name": "Production", "isProduction": true, "order": 1 }
// ]
// }
// ]

const app = apps.data[0];
const appId = app?.id;

if (appId) {
const environments = await api.listEnvironments({
appId,
sortBy: "order",
sortOrder: "asc",
});

console.log(environments.data);
// [
// { "id": "env-456", "name": "Production", "isProduction": true, "order": 1 }
// ]
}
```

## App-scoped client

If most calls are for one app, use `createAppClient` to avoid repeating `appId`.

```typescript
import { createAppClient } from "@reflag/rest-api-sdk";

const appApi = createAppClient("app-123", {
accessToken: process.env.REFLAG_API_KEY,
});

const environments = await appApi.listEnvironments({
sortBy: "order",
sortOrder: "asc",
});
console.log(environments.data);
// [
// { "id": "env-456", "name": "Production", "isProduction": true, "order": 1 }
// ]

const flags = await appApi.listFlags({});
console.log(flags.data);
// [
// {
// "id": "flag-1",
// "key": "new-checkout",
// "name": "New checkout",
// "description": "Rollout for redesigned checkout flow",
// "stage": { "id": "stage-1", "name": "Beta", "color": "#4f46e5", "order": 2 },
// "owner": {
// "id": "user-99",
// "name": "Jane Doe",
// "email": "jane@acme.com",
// "avatarUrl": "https://example.com/avatar.png"
// },
// "archived": false,
// "stale": false,
// "permanent": false,
// "createdAt": "2026-03-03T09:00:00.000Z",
// "lastCheckAt": "2026-03-03T09:30:00.000Z",
// "lastTrackAt": "2026-03-03T09:31:00.000Z"
// }
// ]
```

## Common workflows

### Create and update a flag

`createFlag` and `updateFlag` return `{ flag }` with the latest flag details.

Use `null` to clear nullable fields like `description` or `ownerUserId` on update.

```typescript
const created = await api.createFlag({
appId: "app-123",
key: "new-checkout",
name: "New checkout",
description: "Rollout for redesigned checkout flow",
secret: false,
});

const updated = await api.updateFlag({
appId: "app-123",
flagId: created.flag.id,
name: "New checkout experience",
ownerUserId: null,
});
console.log(updated.flag);
// {
// "id": "flag-1",
// "key": "new-checkout",
// "name": "New checkout experience",
// "description": "Rollout for redesigned checkout flow",
// "stage": { "id": "stage-1", "name": "Beta", "color": "#4f46e5", "order": 2 },
// "owner": {
// "id": "user-99",
// "name": "Jane Doe",
// "email": "jane@acme.com",
// "avatarUrl": "https://example.com/avatar.png"
// },
// "archived": false,
// "stale": false,
// "permanent": false,
// "createdAt": "2026-03-03T09:00:00.000Z",
// "lastCheckAt": "2026-03-03T09:35:00.000Z",
// "lastTrackAt": "2026-03-03T09:36:00.000Z",
// "rolledOutToEveryoneAt": "2026-03-10T12:00:00.000Z",
// "parentFlagId": "flag-parent-1"
// }
```

### Read user flags for an environment

`getUserFlags` evaluates flag results for one user in one environment and returns
the user’s current values plus exposure/check metadata for each flag.

```typescript
const userFlags = await api.getUserFlags({
appId: "app-123",
envId: "env-456",
userId: "user-1",
});

console.log(userFlags.data);
// [
// {
// "id": "flag-1",
// "key": "new-checkout",
// "name": "New checkout",
// "createdAt": "2026-03-03T09:00:00.000Z",
// "value": true,
// "specificTargetValue": true,
// "firstExposureAt": "2026-03-03T09:05:00.000Z",
// "lastExposureAt": "2026-03-03T09:30:00.000Z",
// "lastCheckAt": "2026-03-03T09:31:00.000Z",
// "exposureCount": 12,
// "firstTrackAt": "2026-03-03T09:06:00.000Z",
// "lastTrackAt": "2026-03-03T09:32:00.000Z",
// "trackCount": 5
// }
// ]
```

### Toggle a user flag

Use `true` to explicitly target on, and `null` to remove specific targeting.

```typescript
const updatedUserFlags = await api.updateUserFlags({
appId: "app-123",
envId: "env-456",
userId: "user-1",
updates: [{ flagKey: "new-checkout", specificTargetValue: true }],
});
console.log(updatedUserFlags.data);
// [
// {
// "id": "flag-1",
// "key": "new-checkout",
// "name": "New checkout",
// "createdAt": "2026-03-03T09:00:00.000Z",
// "value": true,
// "specificTargetValue": true,
// "firstExposureAt": "2026-03-03T09:05:00.000Z",
// "lastExposureAt": "2026-03-03T09:35:00.000Z",
// "lastCheckAt": "2026-03-03T09:36:00.000Z",
// "exposureCount": 13,
// "firstTrackAt": "2026-03-03T09:06:00.000Z",
// "lastTrackAt": "2026-03-03T09:37:00.000Z",
// "trackCount": 6
// }
// ]
```

### Read company flags for an environment

```typescript
const companyFlags = await api.getCompanyFlags({
appId: "app-123",
envId: "env-456",
companyId: "company-1",
});
console.log(companyFlags.data);
// [
// {
// "id": "flag-1",
// "key": "new-checkout",
// "name": "New checkout",
// "createdAt": "2026-03-03T09:00:00.000Z",
// "value": false,
// "specificTargetValue": null,
// "firstExposureAt": null,
// "lastExposureAt": null,
// "lastCheckAt": "2026-03-03T09:31:00.000Z",
// "exposureCount": 0,
// "firstTrackAt": null,
// "lastTrackAt": null,
// "trackCount": 0
// }
// ]
```

### Toggle a company flag

Use `true` to explicitly target on, and `null` to remove specific targeting.

```typescript
const updatedCompanyFlags = await api.updateCompanyFlags({
appId: "app-123",
envId: "env-456",
companyId: "company-1",
// Use `null` to stop targeting the company specifically for that flag.
updates: [{ flagKey: "new-checkout", specificTargetValue: null }],
});
console.log(updatedCompanyFlags.data);
// [
// {
// "id": "flag-1",
// "key": "new-checkout",
// "name": "New checkout",
// "createdAt": "2026-03-03T09:00:00.000Z",
// "value": false,
// "specificTargetValue": null,
// "firstExposureAt": null,
// "lastExposureAt": null,
// "lastCheckAt": "2026-03-03T09:36:00.000Z",
// "exposureCount": 0,
// "firstTrackAt": null,
// "lastTrackAt": null,
// "trackCount": 0
// }
// ]
```

## Error handling

The SDK throws `ReflagApiError` for non-2xx API responses.

```typescript
import { ReflagApiError } from "@reflag/rest-api-sdk";

try {
await api.listApps();
} catch (error) {
if (error instanceof ReflagApiError) {
console.error(error.status, error.code, error.message, error.details);
}
throw error;
}
```

## Example app

See `packages/rest-api-sdk/examples/customer-admin-panel/README.md` for a small Next.js app using this SDK in server actions.

## License

MIT
8 changes: 8 additions & 0 deletions packages/rest-api-sdk/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const base = require("@reflag/eslint-config");

module.exports = [
...base,
{
ignores: ["dist/", "src/generated/", "examples/**", "**/.next/**"],
},
];
25 changes: 25 additions & 0 deletions packages/rest-api-sdk/examples/customer-admin-panel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Customer Admin Panel

Small Next.js (App Router) app that uses `@reflag/rest-api-sdk` with server actions to view and toggle flags for users and companies.

![Company Flags Screenshot](docs/company-flags-screenshot.png)

## Setup

Create a `.env.local` file in this folder with:

```
REFLAG_API_KEY=your-api-key
# Optional
REFLAG_BASE_URL=https://app.reflag.com/api
```

## Run

```bash
yarn dev
```

Visit:

- http://localhost:3000/
Loading
Loading