JavaScript / TypeScript SDK
A tiny, isomorphic client for multi-armed-bandit selection and feedback. Works on Node 18+, edge runtimes (Vercel, Cloudflare Workers, Deno Deploy), Bun, and the browser. Ships dual ESM + CJS with bundled TypeScript types and zero runtime dependencies.
The JS SDK covers select and feedback only. Pool, experiment, and gate management is available via the Python SDK or the REST API.
Installation
npm install qbrixWorks with any package manager:
pnpm add qbrix
yarn add qbrixRequires Node 18+. TypeScript types are included — no @types/ package needed.
Quickstart
import { QbrixClient } from "qbrix";
// reads QBRIX_API_KEY and QBRIX_BASE_URL from the environment automatically
const qbrix = new QbrixClient();
// select an arm for this user/context
const { arm, requestId } = await qbrix.select("homepage-cta", { id: "user-42" });
// render the chosen arm
console.log(`showing: ${arm.name}`);
// report the outcome — pass back the requestId from select
await qbrix.feedback(requestId, 1.0); // 1 = converted, 0 = no actionClient Initialization
import { QbrixClient } from "qbrix";
const qbrix = new QbrixClient({
apiKey: "optiq_...",
baseUrl: "https://cloud.qbrix.io",
timeout: 5000,
maxRetries: 2,
});All options are optional. When omitted, the client reads from environment variables then falls back to built-in defaults.
Configuration
Resolution order per option: explicit argument → environment variable → default.
| Option | Type | Default | Env Var |
|---|---|---|---|
apiKey | string | — | QBRIX_API_KEY |
baseUrl | string | http://localhost:8080 | QBRIX_BASE_URL |
timeout | number (ms) | 30000 | — |
maxRetries | number | 2 | — |
retryOn | number[] | [429, 502, 503, 504] | — |
fetch | typeof fetch | runtime global | — |
headers | Record<string, string> | {} | — |
logger | QbrixLogger | silent | — |
Select
select(experimentId: string, context: Context): Promise<SelectResult>interface Context {
id: string; // required — a stable user/session identifier
vector?: number[]; // optional feature vector for contextual policies
metadata?: Record<string, unknown>; // optional arbitrary attributes
}
interface SelectResult {
arm: { id: string; name: string; index: number };
requestId: string; // pass this back into feedback()
isDefault: boolean; // true when the platform returned the fallback arm
}Example:
const { arm, requestId, isDefault } = await qbrix.select("checkout-banner", {
id: "usr_8a1k2",
metadata: { country: "US", plan: "pro" },
});
showBanner(arm.name);Feedback
feedback(requestId: string, reward: number): Promise<void>Report the outcome for a prior select. requestId is the value returned by that call; reward is the observed signal — for example 1.0 for a conversion and 0.0 for no action, or any numeric reward your experiment defines.
await qbrix.feedback(requestId, 1.0);Recommended Pattern: server-side handler
Your API key (optiq_…) is a secret. Anywhere the client runs, the key goes too — bundling it into browser code exposes it to every visitor. The recommended pattern is a thin server-side or edge handler that keeps the key in an environment variable and returns only what the browser needs:
// edge / route handler — runs on the server
import { QbrixClient, QbrixAPIError } from "qbrix";
const qbrix = new QbrixClient({ apiKey: process.env.QBRIX_API_KEY });
export default async function handler(req: Request): Promise<Response> {
const { userId } = await req.json();
try {
const { arm, requestId } = await qbrix.select("homepage-cta", { id: userId });
// return only what the browser needs — never the api key
return Response.json({ arm, requestId });
} catch (err) {
if (err instanceof QbrixAPIError) {
return Response.json({ error: err.code ?? "qbrix_error" }, { status: err.status });
}
return Response.json({ error: "internal_error" }, { status: 500 });
}
}Browser usage is supported for internal tools and prototypes where the key exposure is an accepted trade-off.
Error Handling
Every failure throws a typed error from the QbrixError hierarchy. Catch the ones you care about with instanceof:
import {
QbrixAPIError,
RateLimitedError,
AuthenticationError,
QbrixTimeoutError,
} from "qbrix";
try {
const { arm, requestId } = await qbrix.select("homepage-cta", { id: "user-42" });
} catch (err) {
if (err instanceof RateLimitedError) {
console.warn(`rate limited; retry after ${err.retryAfter}s`);
} else if (err instanceof AuthenticationError) {
throw new Error("check your QBRIX_API_KEY");
} else if (err instanceof QbrixTimeoutError) {
// request exceeded the configured timeout
} else if (err instanceof QbrixAPIError) {
console.error(`qbrix ${err.status} ${err.code}: ${err.detail}`);
}
throw err;
}Error hierarchy
| Class | When thrown |
|---|---|
QbrixAPIError | Non-2xx response from the proxy. Subclasses: BadRequestError (400), AuthenticationError (401), ForbiddenError (403), NotFoundError (404), ConflictError (409), RateLimitedError (429), InternalServerError (500) |
RateLimitedError | HTTP 429. Adds retryAfter (seconds) |
QbrixConnectionError | Network failure — request never completed |
QbrixTimeoutError | Request exceeded timeout |
All classes extend QbrixError.