Bucket Node.js SDK
Node.js, JavaScript/TypeScript client for Bucket.co.
Bucket supports feature toggling, tracking feature usage, collecting feedback on features, and remotely configuring features.
Installation
Install using your favorite package manager:
{% tabs %}
{% tab title="npm" %}
npm i @bucketco/node-sdk
{% endtab %}
{% tab title="yarn" %}
yarn add @bucketco/node-sdk
{% endtab %}
{% tab title="bun" %}
bun add @bucketco/node-sdk
{% endtab %}
{% tab title="pnpm" %}
pnpm add @bucketco/node-sdk
{% endtab %}
{% tab title="deno" %}
deno add npm:@bucketco/node-sdk
{% endtab %}
{% endtabs %}
Other supported languages/frameworks are in the Supported languages documentation pages.
You can also use the HTTP API directly
Basic usage
To get started you need to obtain your secret key from the environment settings
in Bucket.
[!CAUTION]
Secret keys are meant for use in server side SDKs only. Secret keys offer the users the ability to obtain
information that is often sensitive and thus should not be used in client-side applications.
Bucket will load settings through the various environment variables automatically (see Configuring below).
- Find the Bucket secret key for your development environment under environment settings in Bucket.
- Set
BUCKET_SECRET_KEY
in your .env
file
- Create a
bucket.ts
file containing the following:
import { BucketClient } from "@bucketco/node-sdk";
export const bucketClient = new BucketClient();
bucketClient.initialize().then({
console.log("Bucket initialized!")
})
Once the client is initialized, you can obtain features along with the isEnabled
status to indicate whether the feature is targeted for this user/company:
[!IMPORTANT]
If user.id
or company.id
is not given, the whole user
or company
object is ignored.
const boundClient = bucketClient.bindClient({
user: {
id: "john_doe",
name: "John Doe",
email: "john@acme.com",
avatar: "https://example.com/users/jdoe",
},
company: {
id: "acme_inc",
name: "Acme, Inc.",
avatar: "https://example.com/companies/acme",
},
});
const { isEnabled, track, config } = boundClient.getFeature("huddle");
if (isEnabled) {
track();
if (config?.key === "zoom") {
}
boundClient.flush();
}
You can also use the getFeatures()
method which returns a map of all features:
const features = boundClient.getFeatures();
const bothEnabled =
features.huddle?.isEnabled && features.voiceHuddle?.isEnabled;
High performance feature targeting
The SDK contacts the Bucket servers when you call initialize()
and downloads the features with their targeting rules.
These rules are then matched against the user/company information you provide
to getFeatures()
(or through bindClient(..).getFeatures()
). That means the
getFeatures()
call does not need to contact the Bucket servers once
initialize()
has completed. BucketClient
will continue to periodically
download the targeting rules from the Bucket servers in the background.
Batch Operations
The SDK automatically batches operations like user/company updates and feature tracking events to minimize API calls.
The batch buffer is configurable through the client options:
const client = new BucketClient({
batchOptions: {
maxSize: 100,
intervalMs: 1000,
},
});
You can manually flush the batch buffer at any time:
await client.flush();
[!TIP]
It's recommended to call flush()
before your application shuts down to ensure all events are sent.
Rate Limiting
The SDK includes automatic rate limiting for feature events to prevent overwhelming the API.
Rate limiting is applied per unique combination of feature key and context. The rate limiter window size is configurable:
const client = new BucketClient({
rateLimiterOptions: {
windowSizeMs: 60000,
},
});
Feature definitions
Feature definitions include the rules needed to determine which features should be enabled and which config values should be applied to any given user/company.
Feature definitions are automatically fetched when calling initialize()
.
They are then cached and refreshed in the background.
It's also possible to get the currently in use feature definitions:
import fs from "fs";
const client = new BucketClient();
const featureDefs = await client.getFeatureDefinitions();
Edge-runtimes like Cloudflare Workers
To use the Bucket NodeSDK with Cloudflare workers, set the node_compat
flag in your wrangler file.
Instead of using BucketClient
, use EdgeClient
and make sure you call ctx.waitUntil(bucket.flush());
before returning from your worker function.
import { EdgeClient } from "@bucketco/node-sdk";
const bucket = new EdgeClient();
export default {
async fetch(request, _env, ctx): Promise<Response> {
await bucket.initialize();
const features = bucket.getFeatures({
user: { id: "userId" },
company: { id: "companyId" },
});
ctx.waitUntil(bucket.flush());
return new Response(
`Features for user ${userId} and company ${companyId}: ${JSON.stringify(features, null, 2)}`,
);
},
};
See examples/cloudflare-worker for a deployable example.
Bucket maintains a cached set of feature definitions in the memory of your worker which it uses to decide which features to turn on for which users/companies.
The SDK caches feature definitions in memory for fast performance. The first request to a new worker instance fetches definitions from Bucket's servers, while subsequent requests use the cache. When the cache expires, it's updated in the background. ctx.waitUntil(bucket.flush())
ensures completion of the background work, so response times are not affected. This background work may increase wall-clock time for your worker, but it will not measurably increase billable CPU time on platforms like Cloudflare.
Error Handling
The SDK is designed to fail gracefully and never throw exceptions to the caller. Instead, it logs errors and provides
fallback behavior:
-
Feature Evaluation Failures:
const { isEnabled } = client.getFeature("my-feature");
-
Network Errors:
const { track } = client.getFeature("my-feature");
if (isEnabled) {
try {
await track();
} catch (error) {
}
}
-
Missing Context:
const features = client.getFeatures({
user: { id: "user123" },
});
-
Offline Mode:
const client = new BucketClient({
offline: true,
featureOverrides: () => ({
"my-feature": true,
}),
});
The SDK logs all errors with appropriate severity levels. You can customize logging by providing your own logger:
const client = new BucketClient({
logger: {
debug: (msg) => console.debug(msg),
info: (msg) => console.info(msg),
warn: (msg) => console.warn(msg),
error: (msg, error) => {
console.error(msg, error);
errorTracker.capture(error);
},
},
});
Remote config
Remote config is a dynamic and flexible approach to configuring feature behavior outside of your app – without needing to re-deploy it.
Similar to isEnabled
, each feature has a config
property. This configuration is managed from within Bucket.
It is managed similar to the way access to features is managed, but instead of the binary isEnabled
you can have
multiple configuration values which are given to different user/companies.
const features = bucketClient.getFeatures();
key
is mandatory for a config, but if a feature has no config or no config value was matched against the context, the key
will be undefined
. Make sure to check against this case when trying to use the configuration in your application. payload
is an optional JSON value for arbitrary configuration needs.
Just as isEnabled
, accessing config
on the object returned by getFeatures
does not automatically
generate a check
event, contrary to the config
property on the object returned by getFeature
.
Configuring
The Bucket Node.js
SDK can be configured through environment variables,
a configuration file on disk or by passing options to the BucketClient
constructor. By default, the SDK searches for bucketConfig.json
in the
current working directory.
secretKey | string | The secret key used for authentication with Bucket's servers. | BUCKET_SECRET_KEY |
logLevel | string | The log level for the SDK (e.g., "DEBUG" , "INFO" , "WARN" , "ERROR" ). Default: INFO | BUCKET_LOG_LEVEL |
offline | boolean | Operate in offline mode. Default: false , except in tests it will default to true based off of the TEST env. var. | BUCKET_OFFLINE |
apiBaseUrl | string | The base API URL for the Bucket servers. | BUCKET_API_BASE_URL |
featureOverrides | Record<string, boolean> | An object specifying feature overrides for testing or local development. See examples/express/app.test.ts for how to use featureOverrides in tests. | BUCKET_FEATURES_ENABLED, BUCKET_FEATURES_DISABLED |
configFile | string | Load this config file from disk. Default: bucketConfig.json | BUCKET_CONFIG_FILE |
[!NOTE] > BUCKET_FEATURES_ENABLED
and BUCKET_FEATURES_DISABLED
are comma separated lists of features which will be enabled or disabled respectively.
bucketConfig.json
example:
{
"secretKey": "...",
"logLevel": "warn",
"offline": true,
"apiBaseUrl": "https://proxy.slick-demo.com",
"featureOverrides": {
"huddles": true,
"voiceChat": { "isEnabled": false },
"aiAssist": {
"isEnabled": true,
"config": {
"key": "gpt-4.0",
"payload": {
"maxTokens": 50000
}
}
}
}
}
When using a bucketConfig.json
for local development, make sure you add it to your
.gitignore
file. You can also set these options directly in the BucketClient
constructor. The precedence for configuration options is as follows, listed in the
order of importance:
- Options passed along to the constructor directly,
- Environment variable,
- The config file.
Type safe feature flags
To get type checked feature flags, install the Bucket CLI:
npm i --save-dev @bucketco/cli
then generate the types:
npx bucket features types
This will generate a bucket.d.ts
containing all your features.
Any feature look ups will now be checked against the features that exist in Bucket.
Here's an example of a failed type check:
import { BucketClient } from "@bucketco/node-sdk";
export const bucketClient = new BucketClient();
bucketClient.initialize().then(() => {
console.log("Bucket initialized!");
bucketClient.getFeature("invalid-feature");
const {
isEnabled,
config: { payload },
} = bucketClient.getFeature("create-todos");
});

This is an example of a failed config payload check:
bucketClient.initialize().then(() => {
if (isEnabled && todo.length > config.payload.minLength) {
}
});

Testing
When writing tests that cover code with feature flags, you can toggle features on/off programmatically to test the different behavior.
bucket.ts
:
import { BucketClient } from "@bucketco/node-sdk";
export const bucket = new BucketClient();
app.test.ts
:
import { bucket } from "./bucket.ts";
beforeAll(async () => await bucket.initialize());
afterEach(() => {
bucket.clearFeatureOverrides();
});
describe("API Tests", () => {
it("should return 200 for the root endpoint", async () => {
bucket.featureOverrides = {
"show-todo": true,
};
const response = await request(app).get("/");
expect(response.status).toBe(200);
expect(response.body).toEqual({ message: "Ready to manage some TODOs!" });
});
});
See more on feature overrides in the section below.
Feature Overrides
Feature overrides allow you to override feature flags and their configurations locally. This is particularly useful for development and testing. You can specify overrides in three ways:
- Through environment variables:
BUCKET_FEATURES_ENABLED=feature1,feature2
BUCKET_FEATURES_DISABLED=feature3,feature4
- Through
bucketConfig.json
:
{
"featureOverrides": {
"delete-todos": {
"isEnabled": true,
"config": {
"key": "dev-config",
"payload": {
"requireConfirmation": true,
"maxDeletionsPerDay": 5
}
}
}
}
}
- Programmatically through the client options:
You can use a simple Record<string, boolean>
and pass it either in the constructor or by setting client.featureOverrides
:
const client = new BucketClient({ featureOverrides: { myFeature: true } });
client.featureOverrides = { myFeature: false };
client.clearFeatureOverrides();
To get dynamic overrides, use a function which takes a context and returns a boolean or an object with the shape of {isEnabled, config}
:
import { BucketClient, Context } from "@bucketco/node-sdk";
const featureOverrides = (context: Context) => ({
"delete-todos": {
isEnabled: true,
config: {
key: "dev-config",
payload: {
requireConfirmation: true,
maxDeletionsPerDay: 5,
},
},
},
});
const client = new BucketClient({
featureOverrides,
});
Remote Feature Evaluation
In addition to local feature evaluation, Bucket supports remote evaluation using stored context. This is useful when you want to evaluate features using user/company attributes that were previously sent to Bucket:
await client.updateUser("user123", {
attributes: {
role: "admin",
subscription: "premium",
},
});
await client.updateCompany("company456", {
attributes: {
tier: "enterprise",
employees: 1000,
},
});
const features = await client.getFeaturesRemote("company456", "user123");
const feature = await client.getFeatureRemote(
"create-todos",
"company456",
"user123",
);
const featuresWithContext = await client.getFeaturesRemote(
"company456",
"user123",
{
other: {
location: "US",
platform: "mobile",
},
},
);
Remote evaluation is particularly useful when:
- You want to use the most up-to-date user/company attributes stored in Bucket
- You don't want to pass all context attributes with every evaluation
- You need to ensure consistent feature evaluation across different services
Using with Express
A popular way to integrate the Bucket Node.js SDK is through an express middleware.
import bucket from "./bucket";
import express from "express";
import { BoundBucketClient } from "@bucketco/node-sdk";
declare global {
namespace Express {
interface Locals {
boundBucketClient: BoundBucketClient;
}
}
}
app.use((req, res, next) => {
const user = {
id: req.user?.id,
name: req.user?.name
email: req.user?.email
}
const company = {
id: req.user?.companyId
name: req.user?.companyName
}
const boundBucketClient = bucket.bindClient({ user, company });
res.locals.boundBucketClient = boundBucketClient;
next();
});
app.get("/todos", async (_req, res) => {
const { track, isEnabled } = res.locals.bucketUser.getFeature("show-todos");
if (!isEnabled) {
res.status(403).send({"error": "feature inaccessible"})
return
}
...
}
See examples/express/app.ts for a full example.
Remote flag evaluation with stored context
If you don't want to provide context each time when evaluating feature flags but
rather you would like to utilize the attributes you sent to Bucket previously
(by calling updateCompany
and updateUser
) you can do so by calling getFeaturesRemote
(or getFeatureRemote
for a specific feature) with providing just userId
and companyId
.
These methods will call Bucket's servers and feature flags will be evaluated remotely
using the stored attributes.
client.updateUser("john_doe", {
attributes: {
name: "John O.",
role: "admin",
},
});
client.updateCompany("acme_inc", {
attributes: {
name: "Acme, Inc",
tier: "premium"
},
});
...
const features = await client.getFeaturesRemote("acme_inc", "john_doe");
[!IMPORTANT]
User and company attribute updates are processed asynchronously, so there might
be a small delay between when attributes are updated and when they are available
for evaluation.
Opting out of tracking
There are use cases in which you not want to be sending user
, company
and
track
events to Bucket.co. These are usually cases where you could be impersonating
another user in the system and do not want to interfere with the data being
collected by Bucket.
To disable tracking, bind the client using bindClient()
as follows:
const boundClient = client.bindClient({ user, company, enableTracking: false });
boundClient.track("some event");
const { isEnabled, track } = boundClient.getFeature("user-menu");
if (isEnabled) {
track();
}
Another way way to disable tracking without employing a bound client is to call getFeature()
or getFeatures()
by supplying enableTracking: false
in the arguments passed to
these functions.
[!IMPORTANT]
Note, however, that calling track()
, updateCompany()
or updateUser()
in the BucketClient
will still send tracking data. As such, it is always recommended to use bindClient()
when using this SDK.
Flushing
BucketClient employs a batching technique to minimize the number of calls that are sent to
Bucket's servers.
By default, the SDK automatically subscribes to process exit signals and attempts to flush
any pending events. This behavior is controlled by the flushOnExit
option in the client configuration:
const client = new BucketClient({
batchOptions: {
flushOnExit: false,
},
});
Tracking custom events and setting custom attributes
Tracking allows events and updating user/company attributes in Bucket.
For example, if a customer changes their plan, you'll want Bucket to know about it,
in order to continue to provide up-do-date targeting information in the Bucket interface.
The following example shows how to register a new user, associate it with a company
and finally update the plan they are on.
client.updateUser("user_id", {
attributes: { longTimeUser: true, payingCustomer: false },
});
client.updateCompany("company_id", { userId: "user_id" });
client.track("user_id", "huddle", { attributes: { voice: true } });
It's also possible to achieve the same through a bound client in the following manner:
const boundClient = client.bindClient({
user: { id: "user_id", longTimeUser: true, payingCustomer: false },
company: { id: "company_id" },
});
boundClient.track("huddle", { attributes: { voice: true } });
Some attributes are used by Bucket to improve the UI, and are recommended
to provide for easier navigation:
name
-- display name for user
/company
,
email
-- the email of the user,
avatar
-- the URL for user
/company
avatar image.
Attributes cannot be nested (multiple levels) and must be either strings,
integers or booleans.
Managing Last seen
By default updateUser
/updateCompany
calls automatically update the given
user/company Last seen
property on Bucket servers.
You can control if Last seen
should be updated when the events are sent by setting
meta.active = false
. This is often useful if you
have a background job that goes through a set of companies just to update their
attributes but not their activity.
Example:
client.updateUser("john_doe", {
attributes: { name: "John O." },
meta: { active: true },
});
client.updateCompany("acme_inc", {
attributes: { name: "Acme, Inc" },
meta: { active: false },
});
bindClient()
updates attributes on the Bucket servers but does not automatically
update Last seen
.
Zero PII
The Bucket SDK doesn't collect any metadata and HTTP IP addresses are not being
stored. For tracking individual users, we recommend using something like database
ID as userId, as it's unique and doesn't include any PII (personal identifiable
information). If, however, you're using e.g. email address as userId, but prefer
not to send any PII to Bucket, you can hash the sensitive data before sending
it to Bucket:
import { sha256 } from 'crypto-hash';
client.updateUser({ userId: await sha256("john_doe"), ... });
Typescript
Types are bundled together with the library and exposed automatically when importing
through a package manager.
License
MIT License
Copyright (c) 2025 Bucket ApS