Quoin docs

SDK

Install @jingsu96/quoin-flags, define a flag in code, read it with a key, and understand what "unconfirmed" means. The connect screen's snippets are copied from this page.

This is the reference that the connect screen (S2) copies from. The two snippets that the screen shows are the two fenced blocks below. A test in apps/web asserts that they are equal, so the app and the docs can never drift apart. If you already read the Quickstart, the only new item here is the key.

On a private registry

Everything on this page is real, tested code in the repo as of QN-02b (docs/reports/qn-02b.md). This includes defineFlags, createClient, the read over GET /api/flags, the first-read beacon, and the exposure queue behind decide() (see Exposures). The SDK is published as @jingsu96/quoin-flags on a private registry, not on npm. To install it, the .npmrc of your project must route the @jingsu96 scope to that registry, and your ~/.npmrc must have a read token for it. The quoin CLI is not published. As of QN-03a-ii, it runs from source inside the repository (see CLI).

1. Install and sign in

pnpm add @jingsu96/quoin-flags
quoin login

quoin login --url <where Quoin runs> signs in the CLI, so that you can change flags from a terminal (see CLI). Your app authenticates with the key below. The CLI is never required to read a flag.

2. Define your first flag in code

Give the flag a key, a type, a default, an owner and a date by which the flag must be gone. A typo in a key is a compile error, not a silent false. description is required. defineFlags throws without it, so the snippet that you paste must have a description.

import { defineFlags, flag } from "@jingsu96/quoin-flags";

export const flags = defineFlags({
  "checkout.new_summary": flag.boolean({
    default: false,
    description: "New order summary on checkout",
    owner: "mia",
    expires: "2026-12-01",
  }),
});

Every temporary flag must have an expires date. A temporary flag has kind: 'release', the default, or kind: 'experiment'. If a flag must live forever, give it kind: 'ops'. An ops flag has no expiry.

3. Create the client with your key

import { createClient } from '@jingsu96/quoin-flags';
import { flags as registry } from './flags';

export const quoin = createClient(registry, {
	apiKey: process.env.QUOIN_API_KEY,
	subject: (input) => ({ id: getUnitId(input as Request) }),
});

QUOIN_API_KEY is the key that the connect screen shows one time. Use qn_dev_… for development and qn_live_… for production. For other environments, use the own key of the environment. The key names its environment, so no other value must name it. Store the key as a secret. Quoin keeps only a SHA-256 hash and the prefix. The prefix is the word plus the first 8 characters, for example qn_dev_a1b2c3d4. A lost key is a new key. Quoin never recovers a lost key.

subject turns the value that you pass to flags() into the Subject of the engine. The value can be a Request, a session, or a plain object. The id of the Subject is the unit id. Every exposure uses the unit id as its key. Quoin stores the unit id exactly as sent and never hashes it. Use an opaque identifier, for example a user id or a device id. Do not use an email address or a name. See Exposures. If you do not give a mapper, the subject is {}. If a mapper throws, the SDK does the same. It never gives an error in your request.

4. Read it where the difference is

const on = (await quoin.flags(request)).isOn("checkout.new_summary")

flags() fetches the rules for the environment of the key. It returns three reads:

CallRecords an exposure?Use it for
isOn(key) / value(key)NoTo decide what to render, to log, and for all work before the unit has experienced the difference
decide(key)YesThe one call at the time when your app serves the variant to the unit

Exposure happens at decide(), never anywhere else. Call isOn() and value() as often as you want. Call decide() exactly one time, where the unit sees the result. decide() gives the exposure that it recorded to an in-memory queue. The queue sends the exposures in batches to POST /api/exposures every 1 second or 50 items. The queue retries on a network failure, a 5xx or a 429. It never retries on a 4xx. The queue never blocks your request. See Exposures for the full contract of the queue. The contract includes waitUntil for Workers and other short-lived runtimes. In these runtimes, build the client inside the request handler, for two reasons. The ctx.waitUntil of a request is valid only for that request. The shared queue gives its flush to the latest createClient call for a key. This example shows the pattern:

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext) {
		const quoin = createClient(registry, {
			apiKey: env.QUOIN_API_KEY,
			subject: (input) => ({ id: getUnitId(input as Request) }),
			waitUntil: (work) => ctx.waitUntil(work),
		});
		const flags = await quoin.flags(request);
		return renderOneClickCheckout(flags.decide('checkout.one_click'));
	},
};

What happens on the first read

The first time that a process calls flags(), the client posts the definitions of the registry to POST /api/flags/first-read. A definition has these fields: key, type, kind, owner, description, expiry, and whether the flag is client-safe. The client never sends the code default. At this time, a code-defined flag stops being unconfirmed in Quoin. The workspace gets a definition that it did not have. A server-scope key can create up to 20 definitions for each call. A client-scope key can only confirm flags that the workspace already declared. Quoin marks the row for the environment of the key as confirmed, one time. After that beacon arrives, the connect screen redirects to the page of the flag on the next load. There is no live update. Only reload shows the change.

The beacon is best effort. The SDK sends it one time for each process for each key. The SDK counts it as sent only on a 2xx. After a failure, the SDK retries not sooner than the 60 seconds of the row cache. The beacon never blocks a read and never makes a read fail. If a definition differs from the definition that Quoin already holds, Quoin logs the difference. Quoin does not rewrite the definition.

On a Starter workspace, the beacon creates new definitions only up to the 5 flags of the plan. It still answers 200. It names the keys that it skipped in skipped. A skipped flag evaluates to its code default. This continues until the plan changes and the first read of the next process creates the flag. See Plans and limits.

How reads behave when Quoin is unreachable

createClient never throws. It caches rows in-process for 60 seconds (cacheTtlMs). A kill switch can take that long to reach your app. It can also take the 30-second edge cache of the server and one fetch. If a network error or a 5xx occurs, the last good rows answer. If there is no cache yet, the code defaults answer. If the client does not understand the schemaVersion of a payload, the client discards the payload with one quoin.sdk_outdated warning. Then the code defaults answer. baseUrl defaults to the hosted service, https://app.usequoin.app. To point at a local Worker, give your own baseUrl.

The client stops a read that hangs after readTimeoutMs. The default is 2,000 ms, unless you pass another value. The client answers in the same way as for other failures. Two callbacks let you count the failures. The client never throws. These are the callbacks:

  • onReadFailed(problem) runs one time for each failed read. A read fails if Quoin is unreachable, the read times out, the status is an error, the body is malformed, or the schemaVersion is unknown.
  • onInvalid(key, problem) runs when a stored row for a flag does not validate. Then the code default answers.

The row cache, the first-read beacon and the exposure queue of the client are shared for each (baseUrl, apiKey) for the life of the process. The client never evicts them. The intended shape is one server key for each process. TBD: whether the client drains and evicts the queue of a rotated key, QN-11b's owner.

The words

  • Key scope: client keys are safe to ship in a browser. They see only flags that are declared client: true. server keys see every flag and can create definitions through the beacon. operator keys belong to a person, not to an environment. They cannot read flags.
  • Unconfirmed: a flag that Quoin knows about, which the app declared or a beacon carried, and for which no SDK has read the row of an environment there. Confirmation is for each environment, never one time for each flag.
  • Definition: what the code says (key, type, default, owner, expiry). Rule: what Quoin says (force, rollout, experiment). The first match wins. See Flags and rules.

Next

On this page