# Introduction > ConfigDirector is a cloud service to manage remote configuration and feature flags. ConfigDirector is designed to provide your applications with **safety** and **longevity**. Safety by having enforced types, validations, and integrity alerts. Longevity by allowing you to safely rename obsolete names, tracking the lifecycle of feature flags and configs, and alerting on deprecated values in need of maintenance. ## Get started ## Add ConfigDirector to your app Each SDK page covers installing, configuring, and reading values.
## Manage configs outside the dashboard ## Moving from another service ## Resources for Agents - [Sitemap (XML)](https://docs.configdirector.com/sitemap.xml) - [Sitemap (Markdown): every page on the site](https://docs.configdirector.com/sitemap.md) - [llms.txt: index of the documentation for LLMs](https://docs.configdirector.com/llms.txt) - [llms-full.txt: the full documentation as a single file](https://docs.configdirector.com/llms-full.txt) - [OpenAPI document: every route this site serves to agents](https://docs.configdirector.com/openapi.json) # Architecture > How ConfigDirector delivers config values and updates ## ConfigDirector Architecture This section explains the high level architecture on how ConfigDirector evaluates config values and how it delivers updates to client and server SDKs. For both, client and server SDKs, there are three connection mode options: *streaming*, *polling* and *offline*. The *streaming* connection is the default behavior. In this connection mode, the client or server SDK opens a persistent HTTP connection to the ConfigDirector services. Upon connecting, it receives an initial payload with all the config values. From that point on, the server pushes any updates via Server Sent Events (SSE) whenever configs are updated. If the connection is dropped, the SDKs will automatically attempt to reconnect after 2 seconds, and apply an exponential backoff if failures continue. In the case of the *polling* connection mode, the SDKs retrieve all the config values with an initial request. From that point on, they periodically poll the ConfigDirector services for additional updates. The polling interval is configurable, and every SDK enforces the same minimum, described in [Polling intervals](#polling-intervals) below. The use of *polling* is discouraged and should only be used in circumstances where a persistent streaming connection does not work within your system's constraints. In the *offline* connection mode, an SDK never connects to the ConfigDirector services and sends nothing. It serves values your application gives it, inline or, on a server SDK, from a JSON file, to every context, so the application is invisible to the dashboard. It is meant for test runs that pin every flag to a known value. Every SDK can run this way; the Node.js SDK's [Run without a connection](https://docs.configdirector.com/sdks/server/node-js#run-without-a-connection) section shows an example. ### Polling intervals Every SDK of the same kind shares one default polling interval and one minimum polling interval:
Kind of SDK Default polling interval Minimum polling interval
Client SDKs (JavaScript, React, Vue, Next.js client side, Nuxt client side, React Native, Flutter, Swift, Android, and their OpenFeature providers) 60 seconds 30 seconds
Server SDKs (Node.js, Next.js server side, Nuxt server side, Python, Java, .NET, Go, and their OpenFeature providers) 300 seconds (5 minutes) 60 seconds
The rule is enforced identically in every SDK: when the connection mode is *polling* and the configured interval is below the minimum, the SDK polls at the minimum instead and logs a single warning when the client is created. No SDK rejects a low interval and there is no maximum. In *streaming* mode the polling interval has no effect. Each SDK page documents the name and unit of its polling interval option. ### Architecture of client SDKs Your client or mobile application makes use of one of ConfigDirector's client SDKs to communicate with ConfigDirector's services that evaluate configs. The high level view is as follows: 1. The ConfigDirector client SDK opens a connection to the ConfigDirector SDK service. It provides the context, if one was given. 2. The ConfigDirector SDK service evaluates all of the configs' targeting rules using the context provided, and returns the configs with their evaluated values 3. The client SDK stores the payload it received from the SDK service and returns the appropriate values whenever a config value is requested by the application 4. The client SDK and SDK service keep the connection open. Whenever configs or segments are updated (via the dashboard or Admin API) the SDK service pushes events to the client SDK with the updated config values. If the application updates the context via the client SDK, the process restarts from step (1) above. A new connection is initiated with the updated context. If a config value is requested from the client SDK *before* it receives the initial payload from the SDK service, the client SDK returns the values the SDK service last sent it, which it keeps on the device, as long as they were evaluated for the same context. An application opened without a network, after at least one successful connection, therefore serves those values instead of the in-code default values, with the evaluation reason `cached`. On the first launch, or when the application starts with another context, the client SDK returns the in-code default value until the initial payload arrives. From then on it returns the values of the latest payload it received, and keeps them on the device in place of the earlier ones. The *Values kept on the device* section of each browser and mobile SDK page shows how to turn this off or use your own store. ### Architecture of server SDKs Your server application makes use of on of ConfigDirector's server SDKs to communicate with ConfigDirector's services. In the case of server SDKs, evaluation is done by the SDK rather than the remote service. The high level view is as follows: 1. The ConfigDirector server SDK opens a connection to the ConfigDirector SDK service 2. The SDK service returns all the configs, their targeting rules, and the segments those rules use, without evaluating them 3. The server SDK stores the payload in memory 4. When your application requests a config to be evaluated for a given context, the server SDK uses the in memory config definitions and targeting rules to evaluate the given config value 5. The server SDK and the SDK service keep the connection open. Whenever configs or segments are updated (via the dashboard or Admin API) the SDK service pushes events with the updated configs, targeting rules, and segments. In the event that a config value is requested from the server SDK *before* it receives the initial payload from the SDK service, the server SDK will return the default value provided in code. Once it receives an initial payload, from that point on it will evaluate configs with the definitions and targeting rules it has cached in memory until new updates are received from the SDK service. # Configs Anatomy > Detailed description of configs and their attributes and properties ## Config attributes Configs have the following attributes: ### Key - An identifier for the config. This is how the config will be referenced in code via the SDKs, and how it will be presented in the dashboard. - Must be unique across the entire project - Can only contain letters, numbers, periods, dashes, and underscores - It is recommended to follow a consistent naming convention that is descriptive - ConfigDirector provides the ability to safely rename the key, even for configs that are actively being used. Each old key keeps working alongside the new one for at least 35 days and until it is no longer used, and then it is removed. [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#old-keys) explains how ConfigDirector tells that an old key is no longer used. - A config can keep up to two old keys at a time, so you can rename it again before an earlier old key is removed. Once it has two, you can rename it after one of them is removed. - A config renamed within two hours of its creation, before any application has evaluated it and while no code scan references it, has its key replaced outright, with no old key kept. ### Description - An optional description of the config. This can be a useful place to provide more context on how this config is used by applications, what its intention is, and any other useful context. ### Role - The *role* of this config which can be one of `Config`, `Feature flag`, `Experiment`, or `Kill switch` - Each *role* offers different defaults and properties, more on this further below - The *role* is chosen when the config is created. A `Feature flag` can later become a `Kill switch` and a `Kill switch` a `Feature flag`, from the config's **Settings** tab; the role is never updated otherwise ### Lifetime - It can be `Permanent` or `Temporary` - *Permanent* configs are meant for values that are planned to remain in place for the foreseeable future. ConfigDirector will not generate any alerts whenever it detects that a *Permanent* config may no longer be in use. - On the other hand, *Temporary* configs are meant to be used for short term values (for example a feature flag) that should be removed once it is no longer needed. ConfigDirector will track and notify when it detects a *Temporary* config may no longer be needed. See [Config Lifecycle](https://docs.configdirector.com/monitoring/lifecycle) for how that is decided. - The *role* decides the lifetime of every role but `Config`: a `Feature flag` and an `Experiment` are always *Temporary*, a `Kill switch` is always *Permanent*. Only a `Config` chooses its lifetime, and can change it later from the **Settings** tab. ### SDK Availability - Determines if the given config should be available to server SDKs, client SDKs, or both - Some config values may only be relevant to clients, in which case selecting only `Client SDK` in the availability will exclude them from server SDKs. This can help reduce the payload size which may be relevant for projects with a large number of configs and/or configs with large values (such as JSON and JSON schemas). - Conversely, some config values may only be meant for server side applications and including them in the client payload may be undesirable for a variety of reasons (payload size, publicly exposing server config data, etc) ### Type - Feature flags and kill switches are always boolean - Configs can be of a variety of types: `Boolean`, `String`, `Integer`, `Float`, `Enum`, `URL`, `Timespan`, `JSON` - `URL` configs hold an HTTP or HTTPS URL. The SDKs return it as a string. - `Timespan` configs hold a duration in milliseconds, seconds, or minutes. The SDKs return the number exactly as it was entered: the unit labels the value in the dashboard, and no unit conversion takes place. - Experiments can be based on: `Boolean`, `String`, `Integer`, `Float`. Experiments are based on pre-defined variations, so the `Enum` type is redundant and it's excluded from the available types. ### Constraints - Some types, like `Integer`, `Float`, `URL`, and `Timespan`, allow for constraints to be defined - `Integer` and `Float` configs can have a minimum and a maximum value. - `URL` configs can be restricted to HTTPS only. New `URL` configs start with this restriction on. - `Timespan` configs always have a minimum and a maximum value, and only accept whole numbers unless fractional values are allowed. - The constraints can be used to restrict the values that the config is allowed to return. The constraints are enforced in the dashboard and the admin API whenever targeting rules are updated. - Constraints can be an additional layer of safety to prevent unwanted values from being configured by mistake. For example, if a config is meant to represent a timeout value, constraints can be used to enforce the value is within an acceptable range. - `JSON` configs can be validated against a JSON Schema. See [JSON Schema Validation](https://docs.configdirector.com/getting-started/json-schema-validation) for how it works and the limits that apply. ## Roles ### Configs `Config` is the default role and what provides the most flexibility. This role is meant for any arbitrary configuration value (excluding secrets) needed by applications. ### Feature flags & Kill switches *Feature Flags* and *Kill Switches* are boolean configs that behave very similarly but have slightly different defaults and meanings. A `Feature flag` is always *temporary*: it is meant for short-lived flags that will be removed once the feature is fully released. On the other hand, a `Kill switch` is always *permanent*: it is meant to remain in place for the foreseeable future as a means to enable/disable a given feature when needed. A feature flag that has become a long-term toggle can be updated to a kill switch from the config's **Settings** tab, and a kill switch that turns out to be short-lived to a feature flag. Both roles are boolean, so the config keeps its key, its targeting rules in every environment, its tags and its SDK availability. ### Experiments An `Experiment` is intended to provide values for testing the performance of different variations of a feature. It is always *temporary*. *Experiments* are defined in terms of *variations* which represent each value that could be returned. Targeting rules can be defined to return different variations to different users and segments, or randomize them in a percentage rollout. ## Copying a config A config can be copied from its **Settings** tab: **Copy config…** creates a new config from it, with the same definition, tags and every environment's targeting rules, under a key you choose, in the same project or in another project of your organization. The copy starts its own life as a new config and shares nothing with the original afterwards; its old keys, lifecycle state and activity are not carried over, and an archived config has to be restored before it can be copied. A percentage rollout in the copy assigns contexts to its shares on its own, so a user may land in a different share than in the original. When the copy goes to another project, environments are matched by slug, tags by name and segments by key. Each environment of the destination project copies the rules of the source environment with the same slug; an environment that exists only in the source is left out, and for one that exists only in the destination you pick the source environment it starts with. A tag the destination does not have yet is created there, or left out when you cannot create tags in that project. A rule that targets a segment keeps working when the destination has a segment with the same key; otherwise the rule is dropped, and the preview names every dropped rule before you confirm. Nothing else is created in the destination: create the segment there first and copy again to bring the rule along. The [Admin API](https://docs.configdirector.com/integrations/admin-api#copying-a-config) offers the same operation with a dry run. # Core Concepts > ConfigDirector's core concepts and data model ## Data Model ### Organization - All entities exist and are managed within an organization - Users can belong to multiple organizations. If you belong to multiple organizations, when you visit your user profile you will see an option to switch your active organization. ### Projects - Projects are a way to organize related groups of configs together. They can be analogous to a product or application, to a product team, or however you'd like to use them. - Configs belong to projects - Each project defines an arbitrary number of environments. By default, the `Test` and `Production` environments are created automatically. - An organization can have many projects. On the Free option there is a limit on how many projects you can create, see [Pricing and billing](https://docs.configdirector.com/getting-started/pricing-and-billing). - A project can be protected from being deleted, see [Deletion protection](#deletion-protection). Projects created in the dashboard start protected. ### Environments - Each project can have an arbitrary number of environments. However, we recommend to match the environments in your ConfigDirector project to the deployable environments for your application. The `Test` and `Production` environments are created automatically for each project. - Targeting rules for configs exist within an environment - Client and server SDK keys belong to an environment. This means your application will need to configure the corresponding SDK key for each environment it is deployed to. Usually, this means providing the SDK keys via environment variables in your application. - When you provide a context via an SDK, that context will exist within that environment (the environment the SDK key belongs to). - An environment with **Live environment** turned on in its settings is one that serves real traffic, such as `Production`. ConfigDirector uses it to judge a config's [lifecycle](https://docs.configdirector.com/monitoring/lifecycle) and to decide which environments start protected from deletion. - An environment can be protected from being deleted, see [Deletion protection](#deletion-protection). Live environments created in the dashboard start protected. ### Deletion protection - Deletion protection is designed to prevent accidental deletion of a project or environment. While it is on, nobody can delete that project or environment, whether from the dashboard, the Admin API, Terraform, agent skills, etc. - Deletion protection can only be turned on or off from the dashboard. - A project created in the dashboard starts with deletion protection on, and so does every live environment created in the dashboard, including the `Production` environment a new project starts with. Your organization's first project and its `Production` environment start protected too. - Projects and environments created via the Admin API (including via Terraform, CLI, MCP, and agent skills), has deletion protection off. - A protected environment also protects its project: a project cannot be deleted while any of its environments has deletion protection on. - To update deletion protection for a project, open the project's **Project details** tab and use the switch in the **Deletion protection** section. It saves on its own. To update it for an environment, open the project's **Environments** tab, pick the environment, flip **Protect this environment from deletion**, and click **Save**. Anyone who can edit the project or environment can update it. The Admin API, the MCP server, and Terraform report the setting but cannot update it. - While deletion protection is on, the dashboard replaces the **Delete** button with a note saying where to turn it off, and a delete request through the Admin API, the MCP server, or Terraform is refused with a message naming where to turn it off. ### Configs - Configs are an individual instance of a configuration value, feature flag, kill switch, or experiment. - Configs belong to projects, and each project can have an arbitrary number of configs. On the Free option there is a limit on the total number of configs across your organization, see [Pricing and billing](https://docs.configdirector.com/getting-started/pricing-and-billing). - The value returned by a config for any given environment is defined via targeting rules. ### Targeting Rules - Each config, has a set of targeting rules for each environment defined in that project - In each environment, the targeting rules end with a default targeting rule (the catch-all or fallback rule) whose value is returned when no other rule matches - Targeting rules can have many rules with one or multiple conditions in each rule, as well as many percentage based rollouts. - A condition tests either one attribute of the context (its identifier, its context kind, its name, a trait, the app name, or the app version), or whether the context is in a [segment](https://docs.configdirector.com/getting-started/segments). Every condition of a rule must match for the rule to apply. - A context kind condition uses `equals`, `does NOT equal`, `is one of`, or `is NOT one of`. For example, `context.kind equals device` matches only device contexts, and a context sent without a context kind is tested as `user`. - A trait that holds `true` or `false` can be tested with "is true" or "is false". - A percentage rollout assigns each context by its context kind and identifier, so user `42` and device `42` can receive different values. Users keep the values they already receive, whether the application leaves the context kind out or sets it to `user`, so starting to use context kinds does not reshuffle a rollout. - Text and array conditions are case-sensitive. For example, a condition `context.traits /country is one of US` does not match a context whose `country` trait is `us`. If your application sends values in more than one casing, either normalize them before passing them to the SDK or list every casing as a target value. - While editing the rules you can check which value a given context would receive, and why, with the [targeting rules tester](https://docs.configdirector.com/getting-started/testing-targeting-rules) - To use the same rules in another environment, open that environment's rules, click **Copy rules from…** and pick the environment to copy from. Its saved rules and default targeting rule value replace the ones in the editor. Nothing changes until you click **Save**, so you can review or adjust the copied rules first. ### Segments - A segment is a named set of users, such as beta testers, defined once per project and used by the targeting rules of any config in that project. See [Segments](https://docs.configdirector.com/getting-started/segments). - A segment's definition applies in every environment, unless an environment overrides it with its own - Editing a segment applies right away to every config whose targeting rules use it ### Contexts - A context is the user or other entity a config is evaluated for, such as a device, an organization, or a tenant - Contexts are provided via the SDKs and belong to the environment that matches the SDK key being used - Contexts may contain `id`, `kind`, `name`, and `traits`. The `traits` is an arbitrary JSON object - The context kind says what the context represents, such as a user or a device. Leaving it out means `user`. The dashboard suggests `user`, `device`, `organization`, `tenant`, and `listing`, and you can use any other kind that fits your application - A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores - A context is identified by its context kind and identifier together, so user `42` and device `42` are separate contexts, each with its own evaluation history - A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given - All of the attributes of a context may be used in targeting rules ### SDK Keys - SDK keys belong to and identify an environment within a project when using any of our SDKs - There are client and server SDK keys. The documentation for each SDK will note which key should be used. - Each environment can have up to five client SDK keys and five server SDK keys - Server SDK keys are considered a secret. Server SDKs download the targeting rules in order to be able to evaluate targeting rules for any context in memory without needing to reach ConfigDirector servers over the network for each evaluation. - Since targeting rules *may* contain private or proprietary information, the server SDK key should be treated as a sensitive password - Client SDK keys are not considered sensitive. This is because client SDKs only receive the final evaluated values. Do NOT store secret or sensitive values, such as passwords, API keys, private keys, etc., in `config` values. ConfigDirector is not designed to be a secrets vault and does not provide the level of security controls required to guard secret or sensitive information. # JSON Schema Validation > Validate JSON config values against a schema, and the limits that apply `JSON` configs can be configured with a JSON Schema. When a schema is set, every value the config can return is validated against the schema before it is saved, so a value that does not pass validation never reaches your applications. This applies to targeting rules values (including the default rule), and experiment variations, whether they are changed from the dashboard, the Admin API, or Terraform. JSON Schema Validation is available on Free and Pay as you go. ## When values are validated - When you add or update the schema, every existing value in every environment is validated against the new schema. The update is rejected if any environment holds a value that no longer validates. - When you save targeting rules for an environment, every rule value is validated against the schema. - When you update the variations of a JSON experiment, every variation is validated against the schema. ## Schema support Schemas are validated as JSON Schema draft-07. If your schema has a `$schema` keyword, it must be `http://json-schema.org/draft-07/schema#`. A schema that declares another version, such as 2020-12, is rejected while JSON Schema validation is turned on. To fix it, remove `$schema` or change it to `http://json-schema.org/draft-07/schema#`. Regular expressions in `pattern` and `patternProperties` are analyzed before the schema is accepted, and a pattern that can take exponential time to match, known as catastrophic backtracking, is rejected. If you received that error when saving a JSON Schema, rewrite the regular expression patterns with bounded repetition instead of nested quantifiers. ## Limits These limits apply to both pricing options.
Limit Value
Schema size 32 KB
JSON value size 16 KB
Nesting depth of a schema 50 levels
Items in an enum 1,000
Regular expression patterns per schema 32
Length of any given regex pattern 512 characters
Branches across anyOf , oneOf , and allOf 100
## Hourly budgets on Free Validating the provided schema itself, and validating JSON against a schema are two of the most resource intensive operations we run. In order to protect our systems from abuse, the Free option has a fair-use budget per organization, which resets every hour:
Operation Free Pay as you go
Schema updates per hour 20 No limit
JSON value validations per hour 300 No limit
A schema updates is any save that adds, edits, or removes a schema. A value validation is any save of targeting rules or experiment variations for a config that has a JSON schema. When the budget is used up, the update is rejected with an error that says when the budget resets. The Admin API responds with HTTP status `429`, and av error message in the response body. # Pricing and Billing > The Free and Pay as you go options, the free allowance, the usage rates, the billing cycle, the spending limit, billing alert thresholds, and every limit that applies to an organization, its configs, and the Admin API ConfigDirector has two pricing options, and an organization is on exactly one of them: - **Free**: no payment method on file. Usage is capped at the free allowance, and the organization can have up to 5 projects and 100 configs. - **Pay as you go**: a payment method is on file. The same free allowance applies every month, and usage beyond it is billed at the published rates. There is no monthly fee, and projects, configs, seats, and environments are unlimited. Prices are in USD, plus applicable tax. The [pricing page](https://www.configdirector.com/pricing) has a cost estimator; this page explains how the billing works. ## Billing metrics Three usage quantities are metered. Everything else, including seats, environments, evaluations, and alerts, is free.
Metric What is counted
Monthly active users A distinct user, device, or other context that your client apps identify to ConfigDirector in a calendar month, or a distinct app install or browser that evaluates configs without identifying anyone. Each one counts once a month across all your projects and environments: the same identifier signed in on two devices counts once, and a person who uses your app anonymously and then signs in counts twice. Only client SDKs count: the browser and mobile SDKs, and the browser side of the Next.js and Nuxt SDKs.
Service connection hours The hours server SDKs spend connected or persistently polling. A server SDK in polling mode counts 5 minutes per request, which at the default 5-minute interval matches a streaming connection. 750 hours covers one server connected all month, with a buffer for deploys and traffic spikes.
Admin API requests Calls to the Admin API, including every tool call made through the MCP server and every change applied by the Terraform provider . Dashboard use and SDK evaluations are not Admin API requests.
An app install or a browser that evaluates configs without identifying anyone is recognized by the instance ID the client SDK keeps in the app's or the browser's own storage. When the SDK cannot store it, for example in a React Native app without AsyncStorage, every app start counts as one more monthly active user. For example, a web app used every day by 2,000 people who sign in counts 2,000 monthly active users, however often each of them opens it. Browsers that evaluate configs before their person signs in count as well. One server connected all month uses about 730 service connection hours, and a nightly Terraform plan that reads 40 resources uses about 1,200 Admin API requests a month. ## The free allowance Every month, each organization gets this allowance at no cost, on Free and on Pay as you go alike:
Metric Free allowance per month
Monthly active users 1,000
Service connection hours 2,250, about 3 servers connected continuously all month
Admin API requests 10,000
On Free, the allowance is a cap. When a Free organization goes past the allowance of a metric, a [free allowance exceeded](https://docs.configdirector.com/monitoring/alerts#free-allowance-exceeded) alert is raised and a **billing grace period** of 7 days starts, in which the organization can start pay as you go. The alert names the date and time the grace period ends. If the grace period ends with usage still over the allowance, service is suspended: the service stops accepting SDK connections and closes the open ones, so your applications fall back to the in-code default value they pass to the SDK, and the Admin API and the MCP server refuse requests, all with `429 Too Many Requests` and a `Retry-After` header, as described under [Spending limit](#spending-limit). The dashboard keeps working and shows a banner. A [service suspended: free allowance exceeded](https://docs.configdirector.com/monitoring/alerts#service-suspended-free-allowance-exceeded) alert is raised and emailed. Service is reinstated as soon as pay as you go starts, or shortly after the next billing cycle begins. On Pay as you go, the allowance is the first price tier, priced at zero. Nothing is charged until usage goes past it. ## Pay as you go rates Usage beyond the free allowance is priced in tiers. A tier's rate applies only to the usage that falls inside it, so the free allowance stays free and the rate drops as usage grows.
Monthly active users Per 1,000 users
First 1,000 Free
1,000 to 500,000 $5.00
500,000 to 1,000,000 $3.75
Over 1,000,000 $2.50
Service connection hours Per 750 hours
First 2,250 Free
2,250 to 7,500 $6.00
7,500 to 22,500 $5.00
Over 22,500 $3.50
Admin API requests Per 10K requests
First 10,000 Free
10,000 to 250,000 $1.00
250,000 to 1,000,000 $0.75
Over 1,000,000 $0.50
For example, 600,000 monthly active users in a month cost nothing for the first 1,000, $2,495.00 for the next 499,000 at $5.00 per 1,000, and $375.00 for the last 100,000 at $3.75 per 1,000: $2,870.00 in all. Organizations with usage, capacity, or support needs beyond these rates can ask for [custom pricing](https://www.configdirector.com/custom-pricing). ## The billing cycle The billing cycle is the calendar month in UTC. Usage counts reset at the start of each month, and the free allowance applies again. On Pay as you go, the month's usage above the free allowance is invoiced on the first day of the following month and charged to the payment method on file. The **Billing & usage** page in the dashboard shows the month so far: each metric against the free allowance on the **Usage** tab, with the month-to-date spend in total and per metric, and the invoices and receipts on the **Invoices & Receipts** tab. If a payment fails, service keeps running. The **Billing** tab shows the failed payment, and updating the payment method retries it. ## Starting pay as you go On the **Billing** tab of the **Billing & usage** page, **Start pay as you go** shows the rates and asks for a payment method. Pay as you go is active as soon as the payment method is saved, with no interruption in service. Owners and billing managers can do this, and every other billing change described on this page. An organization that chose Pay as you go at sign-up stays on Free until a payment method is added; the dashboard reminds you until then. ## Switching to Free **Switch to Free** on the **Billing** tab schedules the switch for the end of the current billing cycle. Usage until then is invoiced at the published rates, and the organization returns to Free when the cycle ends. Until then, the switch can be undone with **Resume**. An organization must be within the Free limits of 5 projects and 100 configs to switch; the dashboard says what to remove if it is not. The spending limit and the billing alert thresholds are removed when the organization returns to Free, and the free allowance applies as a cap again. ## Spending limit A Pay as you go organization can set a **spending limit**: a dollar amount per billing cycle, of at least $1.00, that the invoice never exceeds. It is set and removed on the **Billing** tab. The spend alerts track the limit. When the month's spend is on track to reach it, a [spend projected to reach the spending limit](https://docs.configdirector.com/monitoring/alerts#spend-projected-to-reach-the-spending-limit) alert is raised at 70% and again at 90% of the limit. When spend reaches the limit, a [spending limit reached](https://docs.configdirector.com/monitoring/alerts#spending-limit-reached) alert is raised and service is suspended for the rest of the billing cycle: the service stops accepting SDK connections and closes the open ones, so your applications fall back to the in-code default value they pass to the SDK, and the Admin API and the MCP server refuse requests. The dashboard keeps working. Nothing above the limit is charged. While service is suspended, the SDK endpoints, the Admin API, and the MCP server answer every request with `429 Too Many Requests` and a `Retry-After` header. The SDKs keep retrying in the background with their usual backoff, so they pick up where they left off once service resumes, without a restart. The dashboard shows a banner with a link to the spending limit. Service resumes as soon as the limit is raised or removed, or shortly after the next billing cycle starts. Raising or removing the limit takes effect right away and resolves the open spending limit alerts. Setting the limit below the month's spend so far is allowed; the next hourly check raises the reached alert and suspends service. Without a spending limit, the service keeps running whatever the usage, and all of it is billed at the published rates. ## Billing alert thresholds A Pay as you go organization can add up to 5 **billing alert thresholds**, each a distinct dollar amount of monthly spend with a priority of Medium, High, or Critical. When the month's spend reaches a threshold, a [billing alert threshold reached](https://docs.configdirector.com/monitoring/alerts#billing-alert-threshold-reached) alert is raised at that priority. Thresholds never affect the service; they are a heads-up about the invoice to come. Threshold alerts are emailed to owners, admins, and billing managers at the priority chosen for the threshold: Critical right away and Medium or High in the daily digest. A threshold above the spending limit never fires, since the invoice never exceeds the limit. Thresholds are managed on the **Billing** tab. Editing or deleting a threshold resolves its open alert right away, and raising a threshold during the month arms it again. ## Limits Every limit below applies on Free and on Pay as you go alike, unless the table says otherwise. A save that goes over a limit is refused with a message naming it, in the dashboard and the Admin API alike. ### Organization
Limit Value
Projects 5 on Free, unlimited on Pay as you go
Configs, across the organization 100 on Free, unlimited on Pay as you go
Members Unlimited
Member invitations 50 per organization and 6 per email address, in any 24 hours
Organization name 300 characters
### Projects and environments
Limit Value
Environments per project Unlimited. Test and Production are created with the project.
Project name and slug 255 characters each
Environment name 200 characters
Environment slug 150 characters
Client SDK keys per environment 5
Server SDK keys per environment 5
Tags per project Unlimited. A tag name is 1 to 50 characters, unique in the project ignoring case.
### Configs and values
Limit Value
Config key 4 to 150 characters: letters, numbers, periods, dashes, and underscores, starting with a letter or number. Unique in the project.
Old keys kept after a rename 2 at a time. See Configs Anatomy .
Description 1,000 characters
Variation name 150 characters
String, enum, and URL values 1,000 characters
Integer and float values 60 digits
Timespan values and constraints Up to 9,007,199,254,740,991, the largest exact integer in JavaScript
JSON values 16 KB
JSON Schema 32 KB, with the schema limits on JSON Schema validation
Tags per config 20
Values across a project 1 MB
### Targeting rules and segments
Limit Value
Values in one percentage rollout 20
Segment conditions in one targeting rule 10
Trait path in a condition 500 characters
Context kind 1 to 64 lowercase letters, digits, dashes, or underscores
Segments per project 200
Condition groups per segment, and per environment override 10
Conditions per condition group 20
Segment name 150 characters
There is no limit on the number of rules in a config's targeting rules, or on the conditions in a rule. [Segments](https://docs.configdirector.com/getting-started/segments#limits) describes the segment limits in context. ### Admin API and MCP server
Limit Value
Admin API requests 10,000 a month in the free allowance, then per request on Pay as you go
Request body 4 MB. A larger body is refused with 413 .
MCP tool calls 60 per minute per organization. Calls over the rate are refused with 429 until the minute passes.
Schema updates on Free 20 per hour, see hourly budgets
JSON value validations on Free 300 per hour, see hourly budgets
## How long things are kept
What Kept for
Evaluation activity charts The Activity tab shows up to the last 30 days. The configs list's weekly evaluation counts cover the last 7 days.
Activity data behind lifecycle and alerts The daily summaries that size quiet windows cover 60 days. Individual evaluation and connection records are archived after 60 days and deleted after 180.
Archived configs Restorable for 30 days, then purged. See Config Lifecycle .
Old config keys after a rename At least 35 days, and until the old key is no longer used. See Dynamic Detection .
Alerts A resolved alert is removed 30 days after it resolves, a dismissed one 30 days after the dismissal, and an alert you have read but not dismissed leaves your inbox after 90 days. Dismissals themselves expire per alert kind, see Alerts .
Alert sightings The detection history behind an alert's grace period is kept for 30 days.
# Quickstart > Welcome to ConfigDirector docs! This is a quickstart guide to get you up and running. Welcome to **ConfigDirector**, a cloud service to manage remote configuration and feature flags. In this section we'll walk you through: 1. Creating your first config 2. Installing and configuring an SDK 3. Retrieving the config in your app via the SDK 4. Configuring contexts and targeting rules Feature flags and remote configuration values allow you to change the behavior of your application without releasing new code. Your application can utilize one of the many ConfigDirector SDKs to retrieve the configuration values or feature flag states to decide if and how a certain feature should be presented to a given user. ## 1. Create your first config 1. [Sign in](https://app.configdirector.com/) to the ConfigDirector dashboard. You'll be prompted to create a free account if you don't have one. Don't worry, it's free to start and does not require a payment method. You can start pay as you go later if you need more than the free allowance. 2. Click on the `Create` button next to the `Configs` navigation to create a new config. 3. Enter a key (the key is both the name and how you'll reference it in code) for the config, adjust other selections, and save it. ## 2. Install and configure an SDK for your language and framework Client and server SDK keys belong to an environment within your project. Use environment variables to provide them at runtime to match the given environment. 1. Retrieve your client and server SDK keys by going to `SDK keys` in the dashboard's navigation panel. 2. Select each environment to access its client and server SDK keys. 3. Install and configure an SDK for your application stack: Server SDK keys are secret values. Do not commit them to your source code repository. Provide them at runtime via environment variables instead. ## 3. Retrieve config values ## 4. Configure contexts and targeting rules 1. With any of our SDKs you can provide an optional context with attributes that can be leveraged in targeting rules. A context describes a user unless you give it another [context kind](https://docs.configdirector.com/getting-started/core-concepts#contexts), such as a device or a tenant. Choose your SDK for instructions on how to provide a context: 1. You can now configure targeting rules by navigating to your config in the dashboard. Targeting rules are the very first tab on the config view: ![Targeting rules example](https://docs.configdirector.com/img/screenshots/targeting-rules-sample.webp) ## Next steps Prefer to manage projects, configs, targeting rules, and segments as code, or from an AI coding assistant? See the [Terraform provider](https://docs.configdirector.com/integrations/terraform) and the [MCP server](https://docs.configdirector.com/integrations/mcp). # Segments > Define a set of users once and use it in the targeting rules of every config in the project A segment is a named set of users, such as "Beta testers" or "Enterprise customers", defined once in a project and used by the targeting rules of any config in that project. Instead of copying the same conditions into every flag, a rule says "the context is in Beta testers". When the definition of a beta tester changes, you edit the segment, and every config that uses it follows. Segments are available on Free and Pay as you go, with the same [limits](#limits). ## Who is in a segment A segment holds one or more **condition groups**. A context is in the segment when any one of its groups matches, and a group matches when every condition in it matches. Inside a group conditions combine with AND, and across groups with OR. For example, a "Beta testers" segment with two groups: - Group 1: the `/email` trait ends with `@acme.com` - Group 2: the `/betaOptIn` trait equals `true`, AND the app version is at least `2.0.0` Everyone at Acme is a beta tester, and so is anyone outside Acme who opted in on version 2 or later of the app. The conditions in a group are the same attribute conditions that targeting rules use: the identifier, the context kind, the name, a trait, the app name, or the app version, with the same operators. See [Targeting Rules](https://docs.configdirector.com/getting-started/core-concepts#targeting-rules). A group with a context kind condition, such as `context.kind equals device`, matches only contexts of the kinds it allows. A segment cannot use another segment, and it holds no percentages. To roll something out to a percentage of a segment, use a percentage rollout in the config's targeting rules. ## Name and key - The **name** is how the segment shows in the dashboard and in audit logs. It can be up to 150 characters long. - The **key** identifies the segment in the Admin API, Terraform, and the MCP server. It must be unique within the project and follows the same rules as a [config key](https://docs.configdirector.com/getting-started/configs#key). While you create a segment, the key follows the name until you edit it. Both can be changed at any time. Targeting rules refer to a segment by its id, not its key, so renaming a segment does not affect what your applications receive. ## Environment overrides The groups you define for a segment are its **project definition**, and every environment uses it by default. An environment can have an **environment override** instead: its own complete set of condition groups, used in that environment in place of the project definition. An override replaces the project definition as a whole; nothing is merged. For example, Production can use the project definition of "Beta testers", while Staging overrides it with two named QA accounts. Removing an override returns the environment to the project definition. A new environment starts on the project definition, and deleting an environment deletes its overrides. ## Creating and editing a segment Open a project and select **Segments** in the sidebar. The list shows each segment's name and key, how many configs use it in their targeting rules (**Used by**, with the configs and their environments when you hover the count), the environments that override it, and when it was last updated. **Create segment** opens the form for a new segment. On a segment's page, the scope selector chooses what you are editing: - **Project definition**: the name, the key, and the groups every environment without an override uses. - An environment that uses the project definition: its groups are shown read-only. **Override in** the environment copies them into an override that you can edit and save. - An environment with an override: the override's groups. **Reset to project definition** removes the override. Each scope is saved on its own. In a group, **AND** adds a condition; below the groups, **OR add a condition group** adds a group. Viewers, operators, developers, admins, and owners can see segments. Operators, developers, admins, and owners can edit them and their environment overrides. Developers, admins, and owners can also create and delete them. The **Audit log** tab lists every change to the segment, with who made it, from where (the dashboard, the Admin API, Terraform, OpenTofu, or MCP), and before and after values. A change to an environment override names the environment. ### Saving changes that reach live environments A segment edit takes effect right away in every config that uses the segment, without saving those configs. So when a save changes the segment in a [live environment](https://docs.configdirector.com/getting-started/core-concepts#environments) where configs use it, ConfigDirector asks you to confirm, and lists those environments and configs. Saving the project definition changes every environment without an override, and saving or resetting an override changes its own environment. A save that leaves the segment as it was in every live environment where configs use it, such as a rename, does not ask for confirmation. ## Testing a segment **Test segment** on the segment's page checks whether a context is in the segment. Pick an existing context or describe a custom one, the same way as in the [targeting rules tester](https://docs.configdirector.com/getting-started/testing-targeting-rules#choosing-a-context). The result says whether the context is in the segment and which group matched, each group is marked as matched or not, and each condition shows the value the context had. The tester evaluates the groups as they are on the page, saved or not, in the scope you selected. With an environment selected, it picks contexts from that environment. With the project definition selected, choose the environment to pick contexts from. ## Using a segment in targeting rules On a config's **Targeting rules** tab: - **AND** on a conditional rule opens a menu with **Attribute condition** and **Segment condition**. - The add rule menu has **Add segment rule**, a conditional rule that starts with a segment condition. - A segment condition reads **is in** or **is not in**, followed by the segment, which you can search by name or key. A segment condition is combined with the rule's other conditions by AND, like any other condition. For example, "context is in Beta testers AND the `/country` trait is one of `US`" matches beta testers in the US. A rule can hold up to 10 segment conditions. In each environment, a segment condition uses the segment's definition for that environment: its override if it has one, otherwise the project definition. The [targeting rules tester](https://docs.configdirector.com/getting-started/testing-targeting-rules#reading-the-markers) shows, for each segment condition, whether the context is in the segment and which group matched. The config's audit log shows a segment condition by the segment's name and key, for example `IF context is in segment Beta testers (beta-testers)`. Editing a segment writes an entry to the segment's audit log only. The configs that use it keep their rules as they are, and their audit logs do not change. ## Segment membership of a context The page of a context (**Contexts**, then a context) lists every segment in the project and whether the context is in it, in the context's environment. Membership depends on the context kind as well as the identifier, so user `42` and device `42` can be in different segments. For a context in a segment, it shows the group that matched, and a segment overridden in that environment is marked **Override**. A segment's name opens it on that environment's scope, so the group numbers match. Contexts do not record the app name or the app version, so a segment that tests either is evaluated without them and marked **App not known**. ## Deleting a segment **Delete segment** at the bottom of a segment's page deletes its project definition and every environment override. A segment cannot be deleted while the targeting rules of any config use it, in any environment. The dialog lists those configs and their environments, so you can remove the segment conditions first. Archived configs are not counted. An archived config whose targeting rules use a deleted segment can no longer be restored. ## Server SDK versions Client SDKs receive values that ConfigDirector has already evaluated, segments included, so every client SDK version works with segments. Server SDKs evaluate targeting rules themselves, so they need a version that understands segments:
SDK Package Segments supported since
Node.js @configdirector/server-sdk 1.9.1
Next.js (server side) @configdirector/nextjs-sdk 1.9.2
Nuxt (server side) @configdirector/nuxt-sdk 1.9.2
Python configdirector-server-sdk 1.6.1
Java com.configdirector:server-sdk 1.8.1
.NET ConfigDirector.ServerSdk 1.7.1
Go github.com/ConfigDirector/go-sdks/configdirector 0.1.0
OpenFeature Node.js @configdirector/openfeature-server-provider 1.9.1
OpenFeature Python configdirector-openfeature-server-provider 1.4.1
OpenFeature Java com.configdirector:openfeature-server-provider 1.5.1
OpenFeature .NET ConfigDirector.OpenFeature.ServerProvider 1.4.1
OpenFeature Go github.com/ConfigDirector/go-sdks/configdirector-openfeature 0.1.0
An older server SDK is never sent a rule that contains a segment condition. For that SDK, the rule is skipped and the next rule applies, so the context gets the value of a later rule or the default targeting rule value. Keep this in mind for a rule that excludes a segment, such as "context is in Blocked users, serve `false`" above a rule that serves `true` to everyone: an older SDK skips the exclusion and serves `true` to blocked users too. ConfigDirector tells you when an older SDK is affected: - The [SDK cannot evaluate segments](https://docs.configdirector.com/monitoring/alerts#sdk-cannot-evaluate-segments) alert names the environment, the SDK version, and the SDK keys it connects with. - Saving targeting rules that use a segment lists the server SDK versions that cannot evaluate segments and connected to that environment in the last 7 days, before the rules are saved. When saving a segment asks for confirmation, the confirmation lists them too. ## Limits
Limit Value
Segments per project 200
Condition groups per segment, and per environment override 10
Conditions per condition group 20
Segment conditions per targeting rule 10
The limits apply to the dashboard, the Admin API, Terraform, and the MCP server alike. ## Managing segments as code Segments can be managed through the Admin API, the [Terraform provider](https://docs.configdirector.com/integrations/terraform#segments), the [MCP server](https://docs.configdirector.com/integrations/mcp), and the [CLI](https://docs.configdirector.com/integrations/cli#segments). Each needs an API token with the `Segments: Read` permission to read segments, `Segments: Create` to create them, `Segments: Update` to change them, and `Segments: Delete` to delete them. # Testing Targeting Rules > See which value a context would receive from the targeting rules you are editing, and why, before you save The targeting rules tester answers "what would this user get?" while you are editing the rules of a config. It runs the same evaluator our server SDKs ship, over the rules as they are in the editor, saved or not, and shows the value along with the reason every rule did or did not apply. The tester is available on Free and Pay as you go and in every environment. ## Opening the tester Open a config, go to the **Targeting rules** tab, pick an environment, and click **Test rules** next to the environment selector. Two nodes join the rule flow: a **Test context** node above the first rule and a **Result** node below the default targeting rule. Every rule gains a marker in a column to its right. Click the button again to close the tester. Nothing about the tester is saved with the config. ![Test rules button](https://docs.configdirector.com/img/screenshots/rules-tester-test-rules-button.webp)![Testing rules button](https://docs.configdirector.com/img/screenshots/rules-tester-testing-rules-button.webp) ## Choosing a context The test context node has two modes. - **Existing** searches the contexts of the selected environment by identifier, name or label. Each result shows the context kind and the traits. Pick one and it appears as a card; **Change** goes back to the search, and **Edit as custom** copies it into the custom form so you can tweak a trait without touching the stored context. Contexts you have tested recently are listed under the search box while it is empty. - **Custom** lets you describe a context that may not exist yet, either as a form (kind, identifier, name, an anonymous checkbox, and trait rows) or as JSON in the shape the SDKs send: `id`, `kind`, `name`, `anonymous`, and `traits`. In a custom context, the context kind starts as `user`. Leaving the **Kind** field empty, or leaving `kind` out of the JSON, also means `user`. The field suggests `user`, `device`, `organization`, `tenant`, and `listing`, followed by the context kinds already seen in the environment, and you can type any other kind. If the context kind is not valid, the tester keeps what you entered and says that the context was ignored. The markers and the result then show what the rules serve when no context is given. That is what your applications get when an SDK ignores a context with an invalid context kind. Stored contexts do not carry the application name and version, because those arrive with each evaluation rather than with the context. Both modes have **App name** and **App version** fields under the context for rules that target application metadata. Switching environment keeps the tester open. A picked existing context is cleared, since contexts belong to a given environment, and a custom context is kept. ![Search for a context](https://docs.configdirector.com/img/screenshots/rules-tester-search-context.webp)![Enter a custom context](https://docs.configdirector.com/img/screenshots/rules-tester-custom-context.webp) ## Reading the markers Each rule is marked with one of these outcomes.
Marker Meaning
Matched Every condition held, or the context fell into one of the rule's percentage shares.
Not matched At least one condition did not hold, or the context fell outside every share.
Not evaluated An earlier rule already served the value, so this rule was not reached.
Error The rule could not be evaluated.
Under the marker, each condition is written out with the value the context actually had, for example `/plan "pro" is not one of enterprise`. A trait the context does not have shows as `(absent)`, which is the most common reason a rule you expected to match did not. A condition that is still being filled in never matches and says so. A segment condition says whether the context is in the segment and which of its condition groups matched, for example `context is in segment Beta testers, group 2 matched`. The tester uses each segment as it is saved, with its definition for the selected environment: the environment's override if it has one, otherwise the project definition. To try out changes to a segment itself, use the [segment tester](https://docs.configdirector.com/getting-started/segments#testing-a-segment). For a percentage rule, the marker shows the bucket that the context kind and identifier hashed to and which share it fell into, for example `Bucket 12.3 falls inside the 20% share serving true (0 to 20)`. Bucketing is deterministic for a given context kind and identifier, so the same context always lands in the same bucket, in the tester and in your applications, while user `42` and device `42` can land in different buckets. The default targeting rule is marked **Matched** when no rule above it matched, and **Not evaluated** when a rule above it served the value. ![Marker and result](https://docs.configdirector.com/img/screenshots/rules-tester-marker-and-result.webp) ## Reading the result The result node states the outcome: which context receives which value, naming the context by context kind and identifier, such as `device 42`, and the conditions that were met as part of the match. For an experiment, the value is the served variation. **Copy trace** puts the full trace on your clipboard, with the context that was evaluated and every marker, ready to paste into a ticket or a chat. ## Evaluating unsaved edits The tester evaluates the rules as they are in the editor. Change a condition, a value, a share, or the default targeting rule value, and the markers and the result update after a short pause, without saving. While the editor holds unsaved changes the test context node shows an **Evaluating unsaved draft** badge, so you always know whether you are looking at what production serves or at what it would serve after you save. ## Anonymous contexts A context without an identifier cannot be bucketed by identifier. For that reason, ConfigDirector derives one from the instance ID a client SDK keeps for each app install or browser, so an anonymous user keeps the same percentage assignment until the app is uninstalled or the site's data is cleared, and the derived identifier is never stored. The tester mirrors this by holding one identifier for as long as it is open and using it for any context you test without an identifier. The bucket line says so when that identifier was used. ## What the tester runs The tester uses the same evaluator as our server SDKs, and the trace is produced by the same walk that produces the value, so the two cannot disagree. Evaluation runs on ConfigDirector's servers, not in your browser, and the context you type is used for that evaluation only. It is not stored and does not appear on the Contexts page or in the activity graphs. # Admin API > Manage projects, environments, configs, targeting rules, segments, tags, and webhooks over HTTPS with an API token. The base URL, authentication, permissions, error responses, usage counting, the endpoints, and the OpenAPI document. ## Introduction The Admin API is the HTTPS interface to everything you manage in the dashboard for a project: projects, environments, configs and their targeting rules, segments, and tags. It also manages your organization's [webhooks](https://docs.configdirector.com/integrations/webhooks). The [Terraform provider](https://docs.configdirector.com/integrations/terraform) and the [MCP server](https://docs.configdirector.com/integrations/mcp) are built on it, and anything they can do, a script of yours can do too. Every request goes through the same validations as the dashboard, so a value that the dashboard would refuse is refused here with the same message. Every change shows in the audit log with a source of **API** and the name of the token that made it. The API describes itself with an OpenAPI 3.1 document, which needs no authentication: ```text https://api.configdirector.com/v1/openapi.json ``` It holds the exact request and response schema of every endpoint on this page, including the targeting rule and segment condition shapes, and the base URL below as its server, so it is the file to hand to an OpenAPI client generator. ## Base URL ```text https://api.configdirector.com/v1 ``` Requests and responses are JSON. Send `Content-Type: application/json` with every request that has a body. Projects, environments, tags, and webhooks are addressed by their id, a UUID; configs and segments are addressed by their key, so a URL reads the way the dashboard does: ```text GET https://api.configdirector.com/v1/projects/{projectId}/configs/new-checkout ``` ## Authentication Create an API token in the dashboard under **API tokens** and send it as a bearer token: ```text Authorization: Bearer YOUR-API-TOKEN ``` A token belongs to the member who created it, and only that member can see, rename, or revoke it. When it is created, it is given a subset of the permissions below, and it never has more than its creator's role grants, even if the role changes later. A token can have an expiration date, which is recommended for tokens stored on a workstation. Expired tokens are deleted 7 days after they expire. Keep tokens out of source control, and provide them through an environment variable or a secret store. A request that is not authenticated, or that the token's permissions do not allow, is refused with `403` and a body that says why: ```json { "error": "The Authorization header is missing. Send `Authorization: Bearer `.", "code": "AUTHORIZATION_HEADER_MISSING" } ```
code Meaning
AUTHORIZATION_HEADER_MISSING The request has no Authorization header.
AUTHORIZATION_HEADER_INVALID The header is not of the form Bearer .
API_TOKEN_NOT_FOUND No token matches. It may have been revoked, deleted 7 days after it expired, or its organization disabled.
API_TOKEN_EXPIRED The token's expiration date has passed. The message says when. You get this code for 7 days after the token expires. Create a new token.
API_TOKEN_OWNER_REMOVED The member who created the token no longer belongs to the organization. Create a new token.
INSUFFICIENT_PERMISSIONS The token is valid but lacks the permission the endpoint needs. The endpoint table below names each one.
## Permissions Each endpoint needs one permission, named in the [endpoints](#endpoints) table, except the [targeting rules reference](#targeting-rules-reference) and the [API token](#api-token) endpoint, which any valid API token can read. The permissions the Admin API uses are:
Resource Permissions
Projects projects:read , projects:create , projects:update , projects:delete
Environments environments:read , environments:create , environments:update , environments:delete
Configs configs:read , configs:create , config-settings:update , config-targeting-rules:update , configs:archive
Segments segments:read , segments:create , segments:update , segments:delete
Tags tags:read , tags:create , tags:update , tags:delete
Code references code-references:read , code-references:update
Contexts contexts:read , to evaluate a config for a saved context
Webhooks webhooks:manage , for every webhook endpoint. Only owners and admins can give it to a token.
Creating a config needs only `configs:create`, with or without tags: the tag ids you send need no other permission. Finding an existing tag by name needs `tags:read`, and making a new tag before you create the config needs `tags:create`, so a config with some existing tags found by name and some new ones needs all three. Evaluating a config for a saved context needs `contexts:read` as well as `configs:read`; that is the only thing the Admin API does with contexts, which are otherwise browsed in the dashboard. Members, API tokens, SDK keys, and the audit log are managed in the dashboard only. ## Terraform, OpenTofu, the CLI, and the audit log Every change made through the API is recorded in the audit log with the source **API** and the token's name, except uploading or removing a repository scan, whose record is the repository's scan time. A request from Terraform, OpenTofu, or the ConfigDirector CLI names its client in the `X-SourceClient` header, so the audit log shows which one made the change. The accepted values are `terraform`, `opentofu`, and `cli`; any other value is recorded as **API**. The Terraform provider and the [CLI](https://docs.configdirector.com/integrations/cli) send the header for you. ## Responses and errors
Status When
200 The request succeeded and the body holds the resource.
204 The request succeeded and there is nothing to return: updating a project or targeting rules, archiving a config, and deletions.
400 The body failed validation. The response lists each problem, with the field it concerns when there is one. A config key that is already in use in the project is reported this way too. A repository scan over one of its bounds is reported this way, naming the bound; a scan that would give a project its 51st scanned repository is refused with { "error": "...", "code": "REPOSITORY_LIMIT_REACHED" } instead. A webhook URL that is not https:// or whose host is private or cannot be found, an organization's 11th webhook, and a test or resend on a disabled webhook are reported this way too. Deleting a project or environment whose deletion protection is on, or a project with a protected environment, is reported this way too, naming where to turn it off.
403 Not authenticated, or the token lacks the permission. See Authentication .
404 No resource with that id or key exists in the project or organization. The body says which, { "error": "...", "code": "CONFIG_NOT_FOUND" } , with code one of PROJECT_NOT_FOUND , ENVIRONMENT_NOT_FOUND , CONFIG_NOT_FOUND , SEGMENT_NOT_FOUND , TAG_NOT_FOUND , SCANNED_REPOSITORY_NOT_FOUND , CONTEXT_NOT_FOUND , WEBHOOK_NOT_FOUND , WEBHOOK_DELIVERY_NOT_FOUND , API_TOKEN_NOT_FOUND or EXPERIMENT_RUN_NOT_FOUND . A project id that no project of the organization has gets PROJECT_NOT_FOUND on every endpoint under /projects/{projectId} . An id in the path that is not a UUID gets an empty body.
409 Deleting a segment that the targeting rules of a config still use. The body's usages lists each config and the environments whose rules use it.
413 The request body is larger than 4 MB, or larger than 32 MB when uploading a repository scan.
429 Service is suspended, because a Free organization has stayed over its free allowance past the billing grace period or a Pay as you go organization has reached its spending limit. The body is { "error": "Service suspended", "code": "SERVICE_SUSPENDED" } with a Retry-After header. A Free organization that has used up an hourly JSON Schema budget gets 429 too, with a message that says when the budget resets.
A `400` body: ```json { "errors": [ { "name": "key", "message": "The key must be at least 4 characters long" } ] } ``` ## Usage Each request counts as one Admin API request against your organization's monthly [free allowance](https://docs.configdirector.com/getting-started/pricing-and-billing#the-free-allowance) of 10,000, and beyond it at the [Pay as you go rates](https://docs.configdirector.com/getting-started/pricing-and-billing#pay-as-you-go-rates). Terraform applies and MCP tool calls count the same way, since both go through the API. Reading the OpenAPI document is free. ## Endpoints ### API token
Method Path What it does Permission
GET /api-token Describe the API token that sends the request: its name, its organization's id and name, its permissions, and when it expires. Use it to tell which organization a token belongs to and what it can do. None, only a valid API token
`permissions` lists what the token can do now, in alphabetical order. A permission chosen for the token that its creator's role no longer grants is left out, and a token without permissions gets an empty list. `expiresAt` is `null` for a token that never expires. ### Projects
Method Path What it does Permission
GET /projects List the organization's projects, without their environments. Each carries storeCodeSnippets , whether the project stores code snippets, and deletionProtection , whether the project is protected from deletion. projects:read
POST /projects Create a project from a name and a slug . Returns it with its environments, Test and Production . projects:create
GET /projects/{projectId} Get a project with its environments. projects:read
PUT /projects/{projectId} Rename a project or change its slug. The project's Code snippets setting, storeCodeSnippets , and its Deletion protection setting, deletionProtection , are read-only here and change in the dashboard. projects:update
DELETE /projects/{projectId} Delete a project with everything in it. projects:delete
### Environments
Method Path What it does Permission
GET /projects/{projectId}/environments List the project's environments. Each carries deletionProtection , whether the environment is protected from deletion. environments:read
POST /projects/{projectId}/environments Create an environment from a name , a slug , a color , and whether it is live . environments:create
GET /projects/{projectId}/environments/{environmentId} Get an environment. environments:read
PUT /projects/{projectId}/environments/{environmentId} Update an environment's name, slug, color, or live flag. The environment's Deletion protection setting, deletionProtection , is read-only here and changes in the dashboard. environments:update
DELETE /projects/{projectId}/environments/{environmentId} Delete an environment and everything in it. environments:delete
### Configs
Method Path What it does Permission
GET /projects/{projectId}/configs List the project's configs in key order, without targeting rules. search lists the configs whose key or description contains the text, ignoring case. role , type , status (the lifecycle state, such as settled or stale ) and lifetime take one value each. Repeat tagIds to list the configs that have any of those tags, or send untagged=true for the configs without tags. A config is listed only when it matches every filter sent. configs:read
POST /projects/{projectId}/configs Create a config from its key , role , type , and defaultValue , which becomes the default targeting rule value in every environment. lifetime is required for the config role and optional for the others, whose role decides it: flag and experiment are always temporary , kill-switch always permanent , and a value sent for those must match. Optional: description , typeOptions for the type's constraints, client and server availability, variations , and tagIds . configs:create
GET /projects/{projectId}/configs/{configKey} Get a config with its targeting rules in every environment. configs:read
PATCH /projects/{projectId}/configs/{configKey} Update the config's settings: key , description , role , lifetime , typeOptions , variations , availability , or tagIds . Only the fields sent change. role can only be updated from flag to kill-switch and back; an update to kill-switch makes the config permanent and resolves its open lifecycle and in-code default value mismatch alerts, an update to flag makes it temporary. A lifetime sent for a flag , kill-switch or experiment must match the role after the update. The type is never updated. config-settings:update
PUT /projects/{projectId}/configs/{configKey}/targets Replace the targeting rules of one environment. See Updating targeting rules . config-targeting-rules:update
GET /projects/{projectId}/configs/{configKey}/activity The time of the most recent evaluation per environment, SDK key kind, SDK name, and SDK version. configs:read
POST /projects/{projectId}/configs/{configKey}/evaluate Evaluate the config for a context in one environment and explain the result: the value served, the rule that served it, and what happened to each rule. See Evaluating a config . configs:read
POST /projects/{projectId}/configs/{configKey}/copy Copy the config into a new one under the key the body names, in this project or in the project projectId names, with its definition, tags and every environment's targeting rules. dryRun: true returns the plan without creating anything. See Copying a config . configs:create
GET /projects/{projectId}/configs/{configKey}/environments/{environmentId}/runs List an experiment's runs in one environment, newest first. See Experiment runs, shipping and stopping . configs:read
GET /projects/{projectId}/configs/{configKey}/runs/{runId} Get one run of the config. configs:read
POST /projects/{projectId}/configs/{configKey}/environments/{environmentId}/ship Ship a variation in one environment: serve it to everyone and end the run. The body names the variationId . See Experiment runs, shipping and stopping . config-targeting-rules:update
POST /projects/{projectId}/configs/{configKey}/environments/{environmentId}/stop Stop the run in one environment: remove its percentage rollouts and keep the default targeting rule. config-targeting-rules:update
DELETE /projects/{projectId}/configs/{configKey} Archive the config. It can be restored from the dashboard for 30 days. configs:archive
A config's `type` is one of `boolean`, `string`, `integer`, `float`, `enum`, `url`, `json`, or `timespan`, and its `role` is `config`, `flag`, `kill-switch`, or `experiment`. `typeOptions` carries the constraints of the type: `isInteger`, `min`, and `max` for numbers, `values` and `valueType` for an enum, `useSchema` and `schema` for JSON, `unit`, `min`, `max`, and `allowFractions` for a timespan, and `secureOnly` for a URL. [Configs Anatomy](https://docs.configdirector.com/getting-started/configs) explains each. An experiment's `variations` are a list of `{ id, name, value }`. ConfigDirector gives each variation an `id` and keeps it for as long as the variation keeps its value, so renaming a variation keeps its `id` and changing its value gives it a new one. Create an experiment with its variations as `{ name, value }`. To update them, send the complete list to `PATCH`, with or without the `id` that `GET` returns for each variation. A `PATCH` is refused with `400` when it sends an `id` that is not one of the config's variations, the same `id` twice, or the same value twice. A copy gives its variations new ids, and a config restored from the archive keeps the ones it had. In the `targets` that `GET` returns, every value that is one of the config's variations comes with that variation's `id`: `variationId` beside a rule's or a percentage's `value`, and `defaultVariationId` beside `defaultValue`. Look it up in `variations` to show the variation's name. A config without variations has neither. ### Targeting rules reference
Method Path What it does Permission
GET /targeting-rules-reference The format PUT /projects/{projectId}/configs/{configKey}/targets accepts: the JSON Schema of the request body, the condition kinds, the operators for each target type, the attributes a condition can test, the operators of context kind and segment conditions, and notes on how rules are evaluated. It is the same document the MCP server's get_targeting_rules_reference tool returns. None, only a valid API token
### Segments
Method Path What it does Permission
GET /projects/{projectId}/segments List the project's segments, each with the configs and environments whose rules use it. segments:read
POST /projects/{projectId}/segments Create a segment from a key , a name , its condition groups , and optional environment overrides , a map from environment id to that environment's own groups. segments:create
GET /projects/{projectId}/segments/{segmentKey} Get a segment. segments:read
PUT /projects/{projectId}/segments/{segmentKey} Replace the segment's key, name, and groups. When overrides is sent, the environment overrides become exactly that map, so {} removes them all; when it is left out, they stay. segments:update
DELETE /projects/{projectId}/segments/{segmentKey} Delete the segment and its overrides. Refused with 409 while any config's rules use it. segments:delete
### Tags
Method Path What it does Permission
GET /projects/{projectId}/tags List the project's tags by name. tags:read
POST /projects/{projectId}/tags Create a tag. Names are trimmed, 1 to 50 characters, and unique in the project ignoring case. tags:create
GET /projects/{projectId}/tags/by-name Find a tag by name , ignoring surrounding whitespace and case. tags:read
GET /projects/{projectId}/tags/{tagId} Get a tag. tags:read
PUT /projects/{projectId}/tags/{tagId} Rename a tag. Its id, and every config assignment, stay the same. tags:update
DELETE /projects/{projectId}/tags/{tagId} Delete a tag. Configs that had it lose the assignment. tags:delete
### Scanned repositories The [CLI](https://docs.configdirector.com/integrations/cli) uploads the code references it finds in a repository with `configdirector scan --upload`. A project keeps one set of references per scanned repository, replaced whole by each upload, and the dashboard uses them to show where a config is read in code and to mark the configs no scanned repository reads. A repository name is its git remote path without the host and `.git`, such as `acme/shop`, with the slash URL-encoded as `%2F` in the path.
Method Path What it does Permission
GET /projects/{projectId}/repositories List the project's scanned repositories by name, each with its latest scan's remoteUrl , commit , scannedAt and filesScanned , the referenceCount of direct and indirect references, the dynamicReferenceCount the scan could not resolve, and snippetsStored , whether its latest upload's snippets were kept. code-references:read
PUT /projects/{projectId}/repositories/{repositoryName} Upload a scan: remoteUrl , commit , filesScanned , and the references and dynamicReferences exactly as configdirector scan --json prints them. Replaces every reference the repository uploaded before; the first upload creates the repository. A project holds at most 50 scanned repositories, and a scan uploads at most 10,000 references, dynamic references included. The body may be up to 32 MB. code-references:update
DELETE /projects/{projectId}/repositories/{repositoryName} Remove a scanned repository and its references, so a renamed or retired repository no longer keeps configs in code. code-references:update
#### Snippets A direct or indirect reference can carry a `snippet`, the lines of code around it, so the dashboard shows the code without opening the repository. A snippet has `firstLine`, the line number of its first line, `lines`, 1 to 11 strings of at most 250 characters each, `language`, one of `typescript`, `tsx`, `javascript`, `jsx`, `vue`, `svelte`, `dart`, `swift`, `kotlin`, `java`, `python`, `csharp`, `go`, `ruby`, `php`, `rust`, `scala`, `groovy`, `objective-c`, `c`, `cpp` and `elixir`, and `truncatedLines`, the numbers of the lines that were cut at 250 characters. The reference's `line` must fall within the snippet's lines. A snippet over one of these bounds is refused with `400`, naming the problem. Dynamic references take no snippet. A project's Code snippets setting, `storeCodeSnippets`, says whether the project stores snippets. When it is off, an upload that carries snippets still succeeds and is stored without them, and the repository's `snippetsStored` reads `false`. #### Deletion protection A project's or environment's Deletion protection setting, `deletionProtection`, says whether it is protected from deletion. A project or environment created through the Admin API or the MCP server starts with it off, as does a non-live environment created in the dashboard. A project created in the dashboard, and a live environment created in the dashboard, start with it on; an organization's first project and its Production environment start with it on too. It is turned on and off only in the dashboard, under the project's or environment's settings; the Admin API returns it and ignores it in `POST` and `PUT` bodies. While it is on, `DELETE` of the project or environment, and `DELETE` of a project that holds a protected environment, is refused with `400`, naming where to turn it off. ### Webhooks Webhooks belong to the organization, so their paths have no project. [Webhooks](https://docs.configdirector.com/integrations/webhooks) explains the webhook events they receive and how to verify them.
Method Path What it does Permission
GET /webhooks List the organization's webhooks in name order, without their secrets. Each carries lastDelivery , the newest delivery's status , attempts , nextAttemptAt and createdAt , or null before the first delivery. webhooks:manage
POST /webhooks Create a webhook from a name and a url . Optional: up to 5 custom headers as { name, value } , enabled ( true when left out), the projectIds , environmentIds and tagIds filters (empty matches everything), eventTypes (every change type when left out), and alertKinds (none when left out). Returns the webhook with its secret . webhooks:manage
GET /webhooks/{webhookId} Get a webhook with its secret . While a rotated secret still signs requests, previousSecretExpiresAt says until when. webhooks:manage
PUT /webhooks/{webhookId} Replace the webhook's name, URL, custom headers, filters, subscriptions, and whether it is enabled. A header sent without a value keeps its saved value, and a header left out is removed. Leaving out enabled enables the webhook. The secret never changes here. webhooks:manage
DELETE /webhooks/{webhookId} Delete the webhook and its deliveries. webhooks:manage
POST /webhooks/{webhookId}/rotate-secret Give the webhook a new secret . The previous one keeps signing every request too for 24 hours. webhooks:manage
POST /webhooks/{webhookId}/test Send a webhook.test webhook event. Returns its delivery, pending its first attempt. webhooks:manage
GET /webhooks/{webhookId}/deliveries List the webhook's deliveries from the last 30 days, newest first, each with its attempts, the latest response and the webhook event sent. To list only the deliveries in one status, send status as pending , succeeded , failed or abandoned . limit is at most 50, which is also the default; when there are more, send nextCursor as before for the next page. webhooks:manage
POST /webhooks/{webhookId}/deliveries/{deliveryId}/resend Send the delivery's webhook event again under the same webhook-id . Returns the delivery, pending its next attempt. webhooks:manage
A webhook's `headers` hold only the header names: values are never returned. `disabledReason` is `member` when someone disabled the webhook, `failures` when ConfigDirector disabled it after repeated failures, and `null` while it is enabled. Disabling a webhook abandons its pending deliveries. ## Examples Check which organization a token belongs to and what it can do: ```bash curl https://api.configdirector.com/v1/api-token \ -H "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" ``` ```json { "name": "Laptop", "organization": { "id": "0199a3c2-5b1e-7d40-9f6a-2c8e4b7d1a30", "name": "Acme" }, "permissions": ["configs:read", "projects:read"], "expiresAt": "2027-01-15T00:00:00.000Z" } ``` List the projects of the organization: ```bash curl https://api.configdirector.com/v1/projects \ -H "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" ``` Create a feature flag that starts off in every environment. A feature flag is always temporary, so no `lifetime` is sent: ```bash curl -X POST https://api.configdirector.com/v1/projects/${PROJECT_ID}/configs \ -H "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "key": "new-checkout", "description": "The redesigned checkout flow", "role": "flag", "type": "boolean", "defaultValue": false }' ``` ### Updating targeting rules `PUT /projects/{projectId}/configs/{configKey}/targets` replaces the rules of one environment. The body names the environment, the default targeting rule value, and the rules in order. Each rule, condition, and percentage carries an `id`, a UUID you generate. A conditional rule serves `true` to two named users, and the default targeting rule serves `false` to everyone else: ```bash curl -X PUT https://api.configdirector.com/v1/projects/${PROJECT_ID}/configs/new-checkout/targets \ -H "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "environmentId": "'"${ENVIRONMENT_ID}"'", "defaultValue": false, "rules": [ { "id": "7a6b0d1e-3b4e-4a8e-9c3b-1f2e3d4c5b6a", "type": "conditional", "order": 0, "target": "value", "value": true, "conditions": [ { "id": "0c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f", "kind": "attribute", "attribute": "identifier", "operator": "is one of", "targetType": "text", "targetValues": ["user-101", "user-202"] } ] } ] }' ``` A rule has a `type` of `conditional` or `percentage`. A conditional rule holds `conditions`, every one of which must match, and either a `target` of `value` with the `value` to serve or a `target` of `percentage` with `percentages`, a list of `{ id, percentage, value }` that adds up to 100. A percentage rule holds only `percentages`. An attribute condition tests `identifier`, `kind`, `name`, `appName`, `appVersion`, or `traits` with a `trait` JSON pointer, read as the `targetType` `text`, `number`, `semver`, `datetime`, `array`, or `boolean`, each with its own operators. A segment condition is `{ id, kind: "segment", operator: "in" | "not in", segmentId }`. The [targeting rules reference](#targeting-rules-reference) and the OpenAPI document list every operator per target type, and the [Terraform provider](https://docs.configdirector.com/integrations/terraform) page shows the same rules in HCL. Rules are validated against the config's type and constraints before they are saved, exactly as in the dashboard: a value outside an integer's range, a URL that is not HTTPS on a secure-only config, or a JSON value that fails the config's schema is refused with `400`. ### Experiment runs, shipping and stopping An experiment has a **run** in an environment for as long as a percentage rollout is saved in that environment's targeting rules. The targeting rules save that adds the first rollout starts the run, and the save that removes the last one ends it; changing a rollout's shares, or adding or removing a rollout, ends the run as `replaced` and starts a new one. Only experiments have runs: a percentage rollout on a feature flag starts none. `GET /projects/{projectId}/configs/{configKey}/environments/{environmentId}/runs` lists the environment's runs, newest first, and `GET /projects/{projectId}/configs/{configKey}/runs/{runId}` returns one. A run carries who started it and, once it has ended, who ended it (`startedBy` and `endedBy`: the member, the `sourceClient` and the API token when one was used), the `startedAt` and `endedAt` times, the `outcome` (`shipped`, `stopped` or `replaced`), the `shippedVariationId` when a variation was shipped, and `rollouts`, the rollouts as they were when the run started: for each rollout rule its `ruleId`, its `conditions` in one line when it is a conditional rule, and its `shares` with their `variationId` and `percentage`. ```json { "id": "0193a9c2-6f4e-7d2a-9b1c-3e5f7a9b1d2e", "configId": "0193a9c2-0000-7000-8000-000000000001", "environmentId": "0193a9c2-0000-7000-8000-000000000002", "startedAt": "2026-11-03T12:00:00.000Z", "startedBy": { "member": { "id": "…", "name": "Dana", "email": "dana@example.com" }, "sourceClient": "dashboard" }, "endedAt": "2026-11-17T10:00:00.000Z", "endedBy": { "member": { "id": "…", "name": "Deploy bot", "email": "ops@example.com" }, "sourceClient": "api", "apiToken": { "id": "…", "name": "release-pipeline" } }, "outcome": "shipped", "shippedVariationId": "0193a9c2-0000-7000-8000-000000000011", "rollouts": [ { "ruleId": "7d3e9b2f-0000-4000-8000-00000000bb00", "shares": [ { "id": "7d3e9b2f-0000-4000-8000-00000000bb01", "variationId": "0193a9c2-0000-7000-8000-000000000010", "percentage": 50 }, { "id": "7d3e9b2f-0000-4000-8000-00000000bb02", "variationId": "0193a9c2-0000-7000-8000-000000000011", "percentage": 50 } ] } ] } ``` `POST /projects/{projectId}/configs/{configKey}/environments/{environmentId}/ship` ships a variation in one targeting rules save: the default targeting rule value becomes the variation's value, every percentage rollout is removed, a conditional rule left with nothing to serve is removed, and a conditional rule that serves a single value is kept. The run ends with the outcome `shipped` and the variation. The body names the variation: ```bash curl -X POST https://api.configdirector.com/v1/projects/${PROJECT_ID}/configs/checkout-cta-copy/environments/${ENVIRONMENT_ID}/ship \ -H "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "variationId": "0193a9c2-0000-7000-8000-000000000011" }' ``` `POST /projects/{projectId}/configs/{configKey}/environments/{environmentId}/stop` ends the run without choosing a variation: every percentage rollout is removed and the default targeting rule keeps serving. It takes no body and is refused with `400` when no rollout is saved in the environment. Both actions answer with the config and its targeting rules after the save, as `GET /projects/{projectId}/configs/{configKey}` returns them, and both are recorded like any targeting rules update: one audit log entry and one `targeting_rules.updated` webhook event. A `variationId` that is not one of the config's variations is refused with `400`. ### Copying a config `POST /projects/{projectId}/configs/{configKey}/copy` creates a new config from an existing one: the same role, lifetime, type, constraints, variations, SDK availability, description and tags, and every environment's targeting rules and default targeting rule value. The copy starts as a new config with no old keys and shares nothing with the source afterwards. Every rule gets its own ids, and a percentage rollout in the copy assigns contexts to its shares on its own. The config limit and the validations of a create and of a targeting rules update apply, and an archived config cannot be copied until it is restored. The copy stays in the same project unless the body names another project of the organization in `projectId`. Across projects, environments are matched by slug, tags by name and segments by key: - Each destination environment copies the rules and default targeting rule value of the source environment with the same slug. A source environment with no destination of its slug is skipped. A destination environment with no source of its slug has to be mapped in `environments`, destination slug to source slug, and the request is refused with `400` naming each unmapped environment until it is. A mapping can also replace a slug match. - A tag is matched to the destination tag with the same name. A missing tag is created in the destination project when the token has `tags:create`, and skipped otherwise. - A rule that tests a segment is rewritten to the destination segment with the same key. When no segment of that key exists in the destination, the rule is dropped and the plan lists it with its position and segment key. No segment is ever created, and no copy is refused over a segment. Preview the copy first with `dryRun: true`. The response is the plan: for each destination environment, its slug, whether it is live, the environment the rules come from, how many rules are copied and the rules dropped; the source environments skipped; and the tags matched, created and skipped. Within a project every environment copies from itself and nothing is dropped or skipped. ```bash curl -X POST https://api.configdirector.com/v1/projects/${PROJECT_ID}/configs/search-ranking/copy \ -H "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "projectId": "'"${MOBILE_PROJECT_ID}"'", "key": "search-ranking", "environments": { "qa": "staging" }, "dryRun": true }' ``` ```json { "plan": { "environments": [ { "slug": "test", "live": false, "sourceSlug": "test", "rulesCopied": 2, "droppedRules": [] }, { "slug": "qa", "live": false, "sourceSlug": "staging", "rulesCopied": 1, "droppedRules": [] }, { "slug": "production", "live": true, "sourceSlug": "production", "rulesCopied": 2, "droppedRules": [{ "position": 2, "segmentKey": "beta-testers" }] } ], "skippedSourceEnvironments": [], "tags": { "matched": ["growth"], "created": ["search"], "skipped": [] } } } ``` Here rule 2 of `production` tests the segment `beta-testers`, which the destination project does not have, so the copy carries the other two rules of that environment and leaves rule 2 out. Create the segment in the destination first and copy again to bring the rule along. Send the same body without `dryRun` to create the copy. The response is `201` with the same `plan` and the created `config`, as `GET /projects/{projectId}/configs/{configKey}` returns it. A key already in use in the destination project, a reached config limit, or an unmapped destination environment is refused with `400` in the same shape as a create; a `projectId` of a project outside the organization is a `404` with the code `PROJECT_NOT_FOUND`. ### Evaluating a config `POST /projects/{projectId}/configs/{configKey}/evaluate` answers what a context gets for a config in one environment, and why. It evaluates the saved targeting rules of that environment with the same evaluator the SDKs use, so the answer is the one an application would get. Nothing is recorded: the request is not SDK activity, so it does not keep a config in use or count toward your SDK usage. The body names the environment by id and describes the context one of three ways. An inline context carries what an application would send to the SDK; `kind` defaults to `user`, and `identifier`, `name` and `traits` are optional: ```bash curl -X POST https://api.configdirector.com/v1/projects/${PROJECT_ID}/configs/new-checkout/evaluate \ -H "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "environmentId": "'"${ENVIRONMENT_ID}"'", "context": { "kind": "user", "identifier": "123", "name": "Ada", "traits": { "plan": "pro" } } }' ``` A saved context, one ConfigDirector has stored from an SDK's evaluations in that environment, is named by its kind and identifier and evaluated with its stored name and traits. This form needs `contexts:read` as well as `configs:read`: ```bash curl -X POST https://api.configdirector.com/v1/projects/${PROJECT_ID}/configs/new-checkout/evaluate \ -H "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "environmentId": "'"${ENVIRONMENT_ID}"'", "savedContext": { "kind": "user", "identifier": "123" } }' ``` A body with only `environmentId` evaluates with an empty context, as an SDK given none would. The response holds `key`, the config's current key even when the request used an old one; `environment` as `{ id, slug, name }`; `context`, the context evaluated, or `null` for an empty one; `value`, the value served, as a string; `variationId`, the id of its variation when the config has one with that value; `servedBy`, either `{ "kind": "rule", "ruleId": "..." }` or `{ "kind": "default" }` for the default targeting rule; and `rules`, one entry per rule in evaluation order with its `outcome` (`matched`, `not-matched`, `not-evaluated` once an earlier rule served the value, or `errored`), its `conditions` with what each resolved to from the context, or which segment group matched, and the `bucket` a percentage rollout assigned the context to. `targetingRules` carries the environment's rules as `GET /configs/{configKey}` returns them in `targets`, and `segments` the definition of every segment those rules reference, keyed by segment id, with the groups in effect in that environment, so the response alone says what each rule tested. `variations` lists the config's variations as `GET` returns them, so an experiment's values can be shown by name. A config, environment or saved context that does not exist is a `404` whose body says which: `{ "error": "...", "code": "CONFIG_NOT_FOUND" }`, `ENVIRONMENT_NOT_FOUND` or `CONTEXT_NOT_FOUND`. # Agent Skills > Install the ConfigDirector agent skills, so your coding agent adds an SDK to your project, wraps a change in a feature flag, reads and tests configs the way these docs describe, checks config keys in CI, removes a settled flag from your code, moves your flags from LaunchDarkly, creates configs from your description, changes their targeting rules, and creates and changes segments ## Introduction The agent skills are a pre-release. What a skill does may change before 1.0, and the [changelog](https://github.com/ConfigDirector/agent-skills/blob/main/CHANGELOG.md) lists every change. An agent skill is a set of instructions your coding agent loads to carry out one ConfigDirector task, such as adding an SDK to your project or checking config keys in CI. Once the skills are installed, ask your agent to add feature flags to your app, and it picks the matching skill on its own and does the work the way someone who has read these docs would. The skills use the shared [Agent Skills](https://agentskills.io) format, so they work with any coding agent that reads it. Two of them give your agent rules for writing and testing code that reads configs. They need no API token of their own, and run the [CLI](https://docs.configdirector.com/integrations/cli)'s checks only when your project already has the CLI set up. ## Install Run this in your project: ```bash npx skills add ConfigDirector/agent-skills ``` It asks which skills to install, which of your coding agents to install them into, and whether to install them in the current project, where you can commit them for your team, or for every project on your machine. Add `-g` to install for every project without being asked, or `--skill` with a skill's name to install only that one: ```bash npx skills add ConfigDirector/agent-skills -g npx skills add ConfigDirector/agent-skills --skill configdirector-setup ``` `npx skills add` works with Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI, Windsurf and [the other agents it supports](https://github.com/vercel-labs/skills#supported-agents). The skills live in the [ConfigDirector/agent-skills](https://github.com/ConfigDirector/agent-skills) repository on GitHub. ## The skills The skills come in two groups. Developer skills are for working in a repository, such as adding the SDK or wrapping a change in a flag. Operator skills change your ConfigDirector account from what you describe, such as creating a config or changing its targeting rules. ### Developer skills
Skill What it does API token permissions
configdirector-setup Adds ConfigDirector to your project: picks the SDK for your framework, installs it, reads the SDK key from an environment variable, creates a first feature flag once you confirm it, or reads the flag you name, and evaluates the flag in your code. Projects: Read , Configs: Read , Configs: Create , for the first flag
configdirector-connect Sets up your agent's access to your ConfigDirector account, through the MCP server or the CLI, for each organization you work for, and checks it. Projects: Read , plus the permissions you choose for the agent
configdirector-flag-a-change Wraps a code change in a new temporary feature flag: proposes a key, creates a boolean flag that is off in every environment once you confirm it, and reads it in your code so the change runs where the flag is on and your app behaves as before where it is off, with a test for each branch. Projects: Read , Configs: Read , Configs: Create
configdirector-use-configs-in-code Gives your agent the rules for code that reads a config: what the in-code default value is for, the type to read each config as, which SDKs receive it, the evaluation context, keys the CLI can check, and one client per app. Applies when your agent writes or reviews such code. When your agent can reach your account, checks that your project has a config before writing a read, and offers to create one it lacks. None of its own; with your agent's access, Projects: Read and Configs: Read to check a config
configdirector-test-with-flags Gives your agent the rules for testing code that reads a config: the SDK's test client or the OpenFeature in-memory provider instead of a mock, no real SDK key, a test for each branch of a flag, and test values of the config's type. Applies when your agent writes tests around flagged code. None of its own
configdirector-validate-in-ci Adds configdirector validate to your repository's CI, so the build fails when the code reads a config key that does not exist, was renamed, or has the wrong type. On GitHub, it adds the ConfigDirector Code Scan action , which can also upload where each config is read on pushes to your default branch, so each config's Code references tab shows the code that reads it. Projects: Read , Configs: Read , plus Code references: Update for the upload, for a token used only by CI
configdirector-clean-up-flag Removes a settled temporary feature flag from your code, keeping the branch it serves: reads the flag's lifecycle state , its recent evaluations and every reference configdirector scan finds, proposes the branch to keep, and shows the exact change before editing. Stops with the facts when the flag is still in use. Archives the config only once you confirm the removal is deployed everywhere that read the flag. Projects: Read , Environments: Read , Configs: Read , Configs: Archive
configdirector-migrate-from-launchdarkly Moves your flags from LaunchDarkly, following the migration guide . Recreates a LaunchDarkly project's segments and flags in your project, with the targeting rules of each environment, from flag data you export in your own terminal, so your agent never handles a LaunchDarkly token: proposes which flags to move and which to hardcode, shows each batch of configs next to LaunchDarkly's settings and rules before creating it, and checks the rules with an evaluation. Then switches one application's SDK calls over, runs configdirector validate and your checks, and removes the LaunchDarkly package once no read remains. Skips every key your project already has, and changes nothing in LaunchDarkly. Projects: Read , Environments: Read , Configs: Read , Configs: Create , Configs: Update targeting rules , Segments: Read , Segments: Create , plus Tags: Read for flags with tags
### Operator skills
Skill What it does API token permissions
configdirector-create-config Creates a config of any role and type from your description, such as a kill switch, a timespan with a range, an enum, or an experiment with variations, with the SDKs that receive it and its tags. Marks the tags your project does not have yet as new, shows every setting and the default targeting rule value every environment starts with, asks for the ones it cannot work out from your description, and creates the config, with any new tags, once you confirm. Asks for the lifetime only for a general config, since a feature flag, kill switch or experiment always has the same one. Also updates a feature flag's role to kill switch, which makes it permanent, or a kill switch's to feature flag, after showing what the update does. Projects: Read , Configs: Read , Configs: Create , plus Tags: Read to find the tags your project already has, Tags: Create to make new ones and Configs: Update settings to update a role
configdirector-roll-out Changes a config's targeting rules in one environment from your description: turns a flag on or off, rolls a change out to a percentage, targets specific users, contexts with certain attributes, or a segment, sets an experiment split, or changes the value it serves. Shows the environment's current rules next to the resulting rules and changes them once you confirm, and in a live environment asks for an explicit yes. On request, shows what a context you describe now gets. Projects: Read , Environments: Read , Configs: Read , Configs: Update targeting rules , plus Segments: Read for segments and Contexts: Read to evaluate a saved context
configdirector-segments Creates a segment or changes one from your description: who is in it, as condition groups, its name or key, and an environment override added, changed or removed. Shows the current and resulting definition side by side with the configs that use the segment in the environments the change reaches, and writes it once you confirm; when one of those environments is live and a config uses the segment there, asks for an explicit yes. Never deletes a segment: it shows what uses it and gives you the command or the dashboard page to delete it yourself. Projects: Read , Environments: Read , Segments: Read , plus Segments: Create to create a segment and Segments: Update to change one
## Use with your other skills The ConfigDirector skills work alongside the other skills you use. Name several skills in one prompt, and your agent follows each one for its part of the work. For example, say you implement changes with a skill named `implement`. Name it together with `configdirector-flag-a-change`, and the change your `implement` skill writes will end up behind a new feature flag that is off in every environment. In Claude Code, type a slash before each skill's name: ```text [Claude Code] Use /implement to add CSV export to the reports page, and /configdirector-flag-a-change to ship it behind a new flag. ``` Other agents call a skill by name in their own way. ## How the skills reach your account Besides `configdirector-connect`, eight skills work with your ConfigDirector account. `configdirector-setup` and `configdirector-flag-a-change` create a flag, `configdirector-create-config` creates a config and updates a feature flag's role to kill switch or back, `configdirector-roll-out` changes a config's targeting rules, `configdirector-segments` creates and changes segments, `configdirector-migrate-from-launchdarkly` recreates your LaunchDarkly segments and flags, and `configdirector-clean-up-flag` archives a flag once you confirm its removal is deployed, through the [MCP server](https://docs.configdirector.com/integrations/mcp) when your agent has it set up, and through the [CLI](https://docs.configdirector.com/integrations/cli) otherwise. The CLI has no command that updates a role, so through the CLI, `configdirector-create-config` shows you the role update to make on the config's **Settings** tab instead. Three skills also run the CLI on their own: `configdirector-validate-in-ci` checks your code with `validate`, the same command your CI runs, `configdirector-clean-up-flag` finds every reference to a flag with [`scan`](https://docs.configdirector.com/integrations/cli#scan), and `configdirector-migrate-from-launchdarkly` checks the application it switched with `validate`. Each needs access set up with an API token you create. Ask your agent to connect ConfigDirector, and `configdirector-connect` sets it up with you: 1. It asks what you want the agent to do, and names the permissions for it. 2. In the ConfigDirector dashboard, you open **API tokens** and create a token with those permissions and an expiration. 3. If your agent supports MCP servers, it adds the MCP server with the configuration the [MCP page](https://docs.configdirector.com/integrations/mcp) gives for your agent, and when you work for more than one organization, with a variable named for the repository's organization, as [More than one organization](https://docs.configdirector.com/integrations/mcp#more-than-one-organization) describes. Otherwise, it has you install the CLI and sign in with [`configdirector login`](https://docs.configdirector.com/integrations/cli#sign-in-on-your-machine) in your own terminal, once for each organization you work for. 4. It checks the access with one read through the MCP server, or two through the CLI, and tells you what the token can do. Through the CLI, it also tells you which organization the token belongs to. When the CLI is signed in to several organizations and a repository's `configdirector.json` names none, a skill asks which one you mean, adds it to the file with [`configdirector init`](https://docs.configdirector.com/integrations/cli#more-than-one-organization), and tells you the file changed, so it goes in your next commit. When the token belongs to another organization than the one the repository names, such as a `CONFIGDIRECTOR_TOKEN` set for another organization, a skill tells you so and sends you to `configdirector-connect` instead of using it. A skill that changes your account and finds neither set up stops and asks how to go on: you set up access with `configdirector-connect` and the skill tries again, or you make the change in the dashboard. When your request also asks for code, such as the SDK `configdirector-setup` adds, the skill can go on with the code without the change, and your code reads its in-code default value until the flag or config exists. You can also set up either one by hand from its page. The token's permissions decide what your agent can do. The MCP server offers only the tools they allow, so a read-only token makes a read-only agent, and the CLI refuses a command that needs a permission the token lacks. A skill that changes your account reads the token's permissions before proposing anything, so a missing one shows up before you confirm a change: the skill names it as the **API tokens** page does, and asks how to go on, as above. Changes your agent makes through the MCP server show in the [audit log](https://docs.configdirector.com/integrations/admin-api#terraform-opentofu-the-cli-and-the-audit-log) with the source **MCP** and the name of the member who created the token, and changes made through the CLI with the source **CLI**. ## Safety The skills follow these rules: - Your agent never sees your API token. The MCP configuration reads the token from an environment variable or your editor's secret prompt, and you sign in to the CLI with `configdirector login` in your own terminal. Never paste a token into the chat, where it would stay in the transcript. If you do, the skill asks you to revoke it and create another. - Every change to your account is confirmed: a skill shows what it will create or change, and waits for your yes. - A skill that cannot make a change tells you at once, before any other work, and asks how to go on. It checks the token's permissions first, and every hand-over starts with anything you asked for that was not created or changed. - Before a skill changes the targeting rules of an environment, it shows you the current and the resulting rules side by side. In a live environment, it also tells you the environment serves real traffic and waits for an explicit yes. - Before a skill changes a [segment](https://docs.configdirector.com/getting-started/segments), it shows you the current and the resulting definition side by side, which environments the change reaches, and the configs that use the segment there. When one of those environments is live and a config uses the segment there, it also tells you so and waits for an explicit yes. - Before a skill removes code, it shows you every reference it found and what happens to each. - A flag's config is archived only after you confirm that the code without it is deployed everywhere that read the flag, and the skill shows the archive first. An archived config can be restored from the dashboard for 30 days. - No skill deletes anything in your account. ## Versions The skills are released together under one version number, and the [changelog](https://github.com/ConfigDirector/agent-skills/blob/main/CHANGELOG.md) lists what each release changed. To update the skills you installed, run `npx skills update`. # CLI > Find where your code reads each config, fail the build when it reads a key that does not exist, was renamed, or has the wrong type, see the value a context gets for a config and why, read a project's configs and targeting rules, create configs, replace a config's targeting rules, archive configs, and create, update and delete segments, with the configdirector command line tool ## Introduction Commands and their output may change before 1.0, and the [changelog](https://github.com/ConfigDirector/cli/blob/main/CHANGELOG.md) lists every change. `configdirector` checks your code against your ConfigDirector project, on your machine or as a CI step, and answers what a context gets for a config. Besides `login`, `logout` and `init`, which set it up, it has three commands: - `configdirector scan` lists every place your code reads a config key through a ConfigDirector SDK or OpenFeature provider, with the file and line. - `configdirector validate` checks those keys against the project's configs, and fails the build when the code reads a key no config has, the old key of a renamed config, a config as the wrong type, a config that is not served to that kind of SDK, or an enum value the config does not allow. - `configdirector evaluate` shows the value a context gets for a config in an environment, the rule that served it, and why each rule matched or not. It also lists a project's environments, configs and segments, and shows a config's targeting rules and when SDKs last evaluated it, as [Browse the project](#browse-the-project) describes, and it creates and copies configs, replaces their targeting rules and archives them, as [Create a config](#create-a-config), [Copy a config](#copy-a-config), [Targeting rules](#targeting-rules) and [Archive a config](#archive-a-config) describe, and creates, updates and deletes segments, as [Segments](#segments) describes. `scan --upload` sends the scan to the project, as [Upload the scan to ConfigDirector](#upload-the-scan-to-configdirector) describes, and `repositories list` and `repositories remove` list and remove the repositories uploaded to it, as [Scanned repositories](#scanned-repositories) describes. `configs create`, `configs copy`, `targeting-rules set`, `configs archive`, `segments create`, `segments update`, `segments delete`, `scan --upload` and `repositories remove` are the only commands that change anything in ConfigDirector. If you have used LaunchDarkly's `ld-find-code-refs` or ConfigCat's `configcat scan`: those tools search your code for the keys your flags already have. `scan` also lists the SDK calls whose key no config has, which is how `validate` catches a misspelled key before it ships. A key held in a constant is found too, with nothing to configure. ## Install The CLI needs Node.js 20 or later. Install it globally: ```bash npm install -g @configdirector/cli configdirector --version ``` Or run it without installing by writing `npx @configdirector/cli` in place of `configdirector`, as the [CI examples](#run-it-in-ci) do: ```bash npx @configdirector/cli --version ``` Both get the newest release. To upgrade on your own schedule, add a version number from the changelog, as in `@configdirector/cli@0.11.0`. If you installed `@configdirector/cli@beta` before 0.11.0, install it again without `@beta`, which stays on the last beta. Or install it with Homebrew, which takes care of Node.js: ```bash brew install ConfigDirector/tap/configdirector configdirector --version ``` The Homebrew formula is updated with every npm release, so `brew upgrade configdirector` gets the newest one. ## Create an API token The CLI reads your projects and configs through the [Admin API](https://docs.configdirector.com/integrations/admin-api), with an API token. 1. In the ConfigDirector dashboard, open **API tokens** and create a token. 2. Choose `Projects: Read` and `Configs: Read`, which `scan` and `validate` need. For `evaluate`, add `Environments: Read`, and `Contexts: Read` to evaluate a config for a [saved context](#the-context). To [browse the project](#browse-the-project), add `Environments: Read` for `environments list`, `Segments: Read` for `segments list` and `segments get`, `Tags: Read` for `configs list --tag`, and `Code references: Read` (`code-references:read`) for [`repositories list`](#scanned-repositories). A token with only these permissions cannot change anything. To [create configs](#create-a-config), add `Configs: Create`, with `Tags: Read` for `configs create --tag` and `Tags: Create` for `configs create --new-tag`. To [copy configs](#copy-a-config), add `Configs: Create`, with `Tags: Create` to create the tags another project doesn't have. To [replace targeting rules](#targeting-rules), add `Environments: Read` and `Configs: Update targeting rules` (`config-targeting-rules:update`). To [archive configs](#archive-a-config), add `Configs: Archive` (`configs:archive`). To [create, update and delete segments](#segments), add `Segments: Create`, `Segments: Update` and `Segments: Delete` (`segments:create`, `segments:update` and `segments:delete`), with `Segments: Read` for `segments update`. To [upload the scan](#upload-the-scan-to-configdirector) or [remove a scanned repository](#scanned-repositories), add `Code references: Update` (`code-references:update`). 3. Set an expiration on a token you keep on your workstation, and do not commit a token into source control. A token belongs to the member who created it, and it stops working when that member leaves the organization. For CI, create a token used only by CI rather than reusing the one on your workstation, so you can replace or revoke it without affecting anything else. If its creator leaves, CI fails with `API_TOKEN_OWNER_REMOVED` until another member creates a new one. ## Configure ### Sign in on your machine ```bash configdirector login ``` `login` asks you to paste the token and hides it while you do. It asks ConfigDirector which organization the token belongs to and what it can do, refuses a token without `Projects: Read`, and saves it as your sign-in for that organization, with the organization's name, where only your user can read it:
System Saved in
macOS ~/Library/Application Support/configdirector/sign-ins.json
Linux ~/.config/configdirector/sign-ins.json
Windows %APPDATA%\configdirector\sign-ins.json
When `XDG_CONFIG_HOME` is set, the sign-ins are saved in `$XDG_CONFIG_HOME/configdirector/sign-ins.json` instead. `configdirector login --token ` saves a sign-in without asking. You keep one sign-in per organization. Running `login` again with a token of an organization you are signed in to replaces that sign-in, and with a token of another organization adds one, so you can work for two organizations from one machine, as [More than one organization](#more-than-one-organization) describes. `configdirector logout` removes the sign-in the commands in the current directory would use; the token itself keeps working until you revoke it on the **API tokens** page. Versions before `0.10.0-beta.1` saved one token in `config.json` in the same directory. The first command you run after upgrading moves that token into a sign-in, with one request to learn its organization, and deletes `config.json`. A token ConfigDirector no longer accepts is deleted without becoming a sign-in, and the command says so; when the Admin API cannot be reached, `config.json` stays for the next run. In CI, skip `login` and set `CONFIGDIRECTOR_TOKEN`. The CLI uses `--token` first, then `CONFIGDIRECTOR_TOKEN`, then the sign-in for the organization the repository names, then your only sign-in. `configdirector api-token get` shows which token a command in the current directory would use, with its organization, name, permissions and expiration, as [Browse the project](#browse-the-project) describes. ### Link the repository to a project From the repository root, with your project's slug, shown as **Project slug** on the project's **Project details** page: ```bash configdirector init --project web-app ``` `init` looks for the project through each of your sign-ins, or through the token you give it, and writes `configdirector.json` in the current directory with the project's slug and the id of its organization. It never overwrites an existing file, except to add `organization` to a file from an earlier version that has none, keeping its other fields. Commit the file, so every run and every CI job uses the same project, and run the CLI from the directory that holds it. ```json [configdirector.json] { "project": "web-app", "organization": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "include": ["src/**"], "exclude": ["src/generated/**"], "ignoreKeys": ["billing-flag-from-other-project"] } ```
Field Meaning
project The project's slug. --project and CONFIGDIRECTOR_PROJECT take precedence over it.
organization The id of the project's organization. A command in the directory uses your sign-in for it, and refuses a token of another organization, such as a CONFIGDIRECTOR_TOKEN set for an MCP server, when that organization has a project with the same slug. --organization and CONFIGDIRECTOR_ORGANIZATION take precedence over it.
include Globs of the files to scan, relative to configdirector.json . init writes ["**"] , every file.
exclude Globs of the files to skip, on top of the files the scan always skips .
ignoreKeys Keys that validate never reports, such as the key of a config another project serves.
### More than one organization A project's slug is unique within its organization, so two organizations can each have a `web-app`. With one sign-in per organization and the organization's id in each repository's `configdirector.json`, every command uses the right token without a variable per directory. Sign in once per organization, with a token of each: ```bash configdirector login # paste a token of Acme configdirector login # paste a token of Globex ``` Then run `init` in each repository. It looks for the project through both sign-ins and writes the organization that has it: ```bash cd ~/code/acme-storefront configdirector init --project web-app # Linked this directory to the project Web App (web-app) of the organization Acme in .../configdirector.json cd ~/code/globex-portal configdirector init --project web-app # Linked this directory to the project Web App (web-app) of the organization Globex in .../configdirector.json ``` From then on, a command in either repository uses that organization's sign-in. When both organizations have a project with the slug, `init` stops and asks you to name one with `--organization`, by the organization's name as `login` saved it, whatever its case, or by its id. `--organization` and `CONFIGDIRECTOR_ORGANIZATION` also pick the sign-in for a command run outside a linked repository, such as `projects list`; a command run with two sign-ins and no organization named stops and lists them. A token you pass with `--token` or `CONFIGDIRECTOR_TOKEN` is always used as it is, so a `CONFIGDIRECTOR_TOKEN` set for every terminal, for example for an [MCP server](https://docs.configdirector.com/integrations/mcp#more-than-one-organization), takes precedence over your sign-ins. In a repository that names another organization, the command then stops before changing anything and names the variable, as long as that organization has a project with the same slug. Give the MCP server a variable named for its organization instead, and leave `CONFIGDIRECTOR_TOKEN` unset. ### Options and environment variables Every command takes these options, as well as `--help` and `--version`:
Option Environment variable Meaning
--token CONFIGDIRECTOR_TOKEN The API token. Without either, the sign-in saved by login for the organization named below, or your only sign-in.
--organization CONFIGDIRECTOR_ORGANIZATION The organization whose sign-in to use, by the name login saved or by its id. Without either, the organization in configdirector.json . Giving one together with --token or CONFIGDIRECTOR_TOKEN is a mistake, and the command exits with 2.
--project CONFIGDIRECTOR_PROJECT The project's slug. Without either, every command that reads a project uses the project in configdirector.json . projects list and targeting-rules reference ignore all three.
--base-url CONFIGDIRECTOR_BASE_URL The Admin API host. Defaults to https://api.configdirector.com .
--json Print JSON instead of text. See Output .
An option takes precedence over its environment variable. An empty variable counts as unset, so a CI job whose token secret is missing stops with `No API token found` rather than sending an empty token. ## Scan ```bash configdirector scan ``` `scan` lists every place your code reads a config key, grouped by key, with the file, the line, the SDK call and the SDK: ```txt max-cart-items src/admin/feature-list.ts:1 (indirect) src/checkout.ts:6 getValue (js-server) new-checkout src/admin/feature-list.ts:1 (indirect) src/checkout.ts:5 getValue(NEW_CHECKOUT) (js-server) test/checkout.test.ts:3 createTestClient (js-server, test) Dynamic references (calls whose key is not a string literal) src/settings.ts:3 getValue(setting.key) (js-server) 3 direct, 2 indirect and 1 dynamic references in 5 scanned files. ``` Each place is one of three kinds of code reference: - **Direct**: an SDK call whose key is known, either a string such as `getValue("max-cart-items", 20)` or a constant that holds one, shown after the call as `getValue(NEW_CHECKOUT)`. These are the references `validate` checks. - **Indirect**: one of the project's keys written as a quoted string anywhere else, such as a list of keys, the argument of your own wrapper function, or a test fixture. The key is used there, but not through a call `validate` can check, so look at these before you rename or archive the config. - **Dynamic**: an SDK call whose key cannot be known from the code, such as a variable, a key built at runtime, or a constant assigned a different string in two places. These are listed so you know they exist. They are never checked, so they never fail the build; to have one checked, pass the key as a string or a constant. A reference is marked `test` when it is a call to an SDK test client, such as `createTestClient` or `setValue`, or an indirect reference in a test directory or a test file such as `checkout.test.ts`. The scan exits with 0 whenever it ran, whether it found references or not. `configdirector scan src` scans only `src`. ### What the scan reads A call is found when its file imports a ConfigDirector SDK or the OpenFeature SDK. In JavaScript, TypeScript, Python, Dart and Go, it is also found when the file imports one of your own files that does, such as the setup module the SDK pages create. In Kotlin, Java and C#, it is also found when the file uses a class of your own that holds the client, such as `object Flags`, in a file named after the class (`Flags.kt`, `Flags.java` or `Flags.cs`) that imports the SDK. In Swift and Go, it is enough for another file of the same target or package to import the SDK, and Nuxt's auto-imported composables need no import. A `getValue` from another library in a file that imports none of these is not reported. The scan skips the files `.gitignore` ignores, the `node_modules`, `vendor`, `build`, `dist` and `target` directories, binary files, and files over 1 MB. `include` and `exclude` in `configdirector.json` narrow it further. If a call you expected is missing, the file most likely reaches the SDK client in one of these ways: - Through two of your own files, such as a module that imports the setup module and passes the client on. Import the setup module directly, or have the module in between re-export the client with `export { client } from "./setup"`. - Through a path alias that only your bundler knows, such as `resolve.alias` in `vite.config.ts`. Add the alias to `compilerOptions.paths` in `tsconfig.json`. - Through a CommonJS re-export, such as `module.exports = require("./setup")`. - In Kotlin, Java or C#, through a class of your own declared in a file with another name, such as an `object Flags` inside `ConfigDirectorSetup.kt`, or through a class that gets the client from another class of yours. Move the class that holds the client into a file named after it, or declare the client with its SDK type, such as a `ConfigDirectorClient` parameter or property, so the file names the SDK. - In Kotlin, through a client declared at the top level of a file, outside any object or class. Declare it in an object, such as `object Flags`. - In Kotlin or Java, through a class of another source directory or module, such as a test in `src/test/java` using a class of `src/main/java`. Declare the client with its SDK type in the file that calls it. - In Kotlin or Java, through an import in a file whose directory does not match its `package`, such as `package com.example.web` in `src/main/kotlin/web/`. Classes in the file's own directory are still found. - In C#, in a file with no `.csproj` in its directory or above it, up to the directory the scan runs in. ### Snippets With `--json` or `--upload`, each direct and indirect reference comes with a snippet: its line and the 2 lines before and after it, so ConfigDirector can show the code around the reference without opening the repository. The text output shows no snippets, and dynamic references have none. ```bash configdirector scan --json --context-lines 4 ``` `--context-lines ` sets how many lines before and after the reference to include, from 0 to 5, and `--context-lines 0` includes the reference line alone. A snippet stops at the start and end of the file, and a line longer than 250 characters is cut to its first 250. `--no-snippets` leaves every snippet out. Giving both, or a `--context-lines` that is not a whole number from 0 to 5, exits with 2. ### Upload the scan to ConfigDirector ```bash configdirector scan --upload ``` `--upload` sends the scan to your project, so the dashboard can show where each config is read and which configs no repository reads. ConfigDirector keeps the latest scan of each repository: every upload replaces all the references the project held for that repository, so a reference you remove from the code is gone after the next upload. Upload from your default branch, after each push to it. `scan --upload` does not check which branch is checked out, so an upload from a feature branch replaces the default branch's references with that branch's. Keep `validate` for pull requests. On GitHub, the [ConfigDirector Code Scan action](#github-actions) does both: it runs `validate` on pull requests and uploads only on pushes to the default branch. On GitLab, add a job that runs only on the default branch: ```yaml [.gitlab-ci.yml] configdirector-upload: image: node:24 rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - npx --yes @configdirector/cli scan --upload ``` The repository is named after the path of its `origin` remote, without the host and `.git`: `https://github.com/acme/shop.git`, `git@github.com:acme/shop.git` and `ssh://git@github.com/acme/shop.git` all upload to `acme/shop`, and a GitLab project in a subgroup keeps its whole path, such as `group/subgroup/app`. The upload also sends the remote's URL and the SHA of the commit that is checked out, which the CLI reads by running `git`. A user name and password in an `https` remote, such as the job token GitLab CI puts there, are removed before the URL is sent. `--repository ` names the repository yourself, for a clone whose `origin` points somewhere else or a directory that is not a git repository. Without it, `scan --upload` exits with 2 when `git` is not installed, when the directory is not in a git repository, or when that repository has no `origin` remote. With it, each of those cases uploads with no remote URL and no commit. A repository with no commit yet uploads with no commit. The scan's output ends with what was uploaded: ```txt new-checkout src/checkout.ts:5 getValue(NEW_CHECKOUT) (js-server) 1 direct, 0 indirect and 0 dynamic references in 3 scanned files. Uploaded the scan of acme/shop at 4d40f50 to web-app: 1 reference and 0 dynamic references, with 0 left out for a key that cannot be a config key. ``` The upload holds the direct, indirect and dynamic references that `scan --json` prints; `ignoreKeys` changes nothing in it. A reference to a key that cannot be a config key, such as one shorter than 4 characters, is left out and counted on that last line. When the scan finds no source file, `scan --upload` exits with 2 and uploads nothing, so a wrong path or `include` never empties the repository's references. A scan that reads files and finds no reference is uploaded, and the repository then has no references. With `--json`, `scan --upload` prints the Admin API's response as received, the [scanned repository](https://docs.configdirector.com/integrations/admin-api#scanned-repositories) with its reference counts, in place of the scan. A project holds at most 50 scanned repositories, and one upload holds at most 10,000 references, dynamic references included. Uploading another repository to a project that holds 50, or a larger scan, exits with 2. `scan --upload` needs `Projects: Read`, `Configs: Read` and `Code references: Update` (`code-references:update`), and makes three Admin API requests. Uploads do not appear in the audit log. #### What leaves your repository An upload sends ConfigDirector: - the path of each file with a reference, from the directory the scan ran in, and the line of each reference; - the config keys the code reads; - for each SDK call, the SDK, the call's name, the key argument as written when it is not a plain string, and the in-code default value when it is a literal; - whether each reference is in a test; - the [snippet](#snippets) of each direct and indirect reference: at most 11 lines of code, the reference line and up to 5 lines on each side, each cut to 250 characters; - how many files the scan read; - the URL of the `origin` remote, without the user name and password of an `https` remote; - the SHA of the commit that is checked out, without its changes, author or message. Nothing else from the repository is sent. To send no code, add `--no-snippets`: the upload then sends everything above but the snippets. To keep a project from storing any code, an owner or an admin can turn off **Store code snippets** in the **Code snippets** card of the project's **Project details** page. The project then removes the snippets it holds, and `scan --upload` sends none and says so after what it uploaded: ```txt Uploaded the scan of acme/shop at 4d40f50 to web-app: 1 reference and 0 dynamic references, with 0 left out for a key that cannot be a config key. Code snippets were not sent: this project does not store them. ``` If the setting is turned off while `scan --upload` runs, the upload still succeeds, and the project stores it without its snippets. ## Scanned repositories ConfigDirector keeps the latest upload of every repository that ran [`scan --upload`](#upload-the-scan-to-configdirector), and nothing removes one on its own. A repository that is renamed or retired keeps its last upload, keeps counting toward the 50 a project can hold, and its references still count when the dashboard shows where each config is read and which configs no repository reads. These commands list a project's scanned repositories and remove one. ```bash configdirector repositories list ``` `repositories list` prints a header row, then one line per repository, with the commit of its latest upload, when it was scanned in UTC, and how many references and dynamic references it holds, and ends with how many scanned repositories the project has out of the 50 it can hold: ```txt REPOSITORY COMMIT SCANNED REFERENCES DYNAMIC REFERENCES ConfigDirector/cli 4d40f50 2026-10-17 10:00 UTC 12 references 1 dynamic reference acme/storefront 9f3e2a1 2026-10-16 09:12 UTC 1 reference 0 dynamic references web-app has 2 of the 50 scanned repositories a project can hold. ``` A repository uploaded without a commit shows `no commit`, and `--no-headers` leaves out the header row. With `--json`, it prints the Admin API's response as received: the [scanned repositories](https://docs.configdirector.com/integrations/admin-api#scanned-repositories), which also give each one's remote URL and how many files its scan read. ```bash configdirector repositories remove acme/storefront ``` `repositories remove` removes the repository and all its references from the project, with no confirmation: ```txt Removed acme/storefront and its code references from web-app. ``` A removed repository that uploads again is added back, so remove the upload step from its CI first. To rename a repository, remove the old name and upload under the new one. `repositories remove` exits with 0 when it removed the repository, and with 2 when no scanned repository in the project has the name, listing the names the project has. With `--json`, it prints `{ "removedRepository": "acme/storefront" }`, with the name as you gave it, since the Admin API answers with no body. `repositories list` needs `Projects: Read` and `Code references: Read` (`code-references:read`). `repositories remove` needs `Projects: Read` and `Code references: Update` (`code-references:update`), and `Code references: Read` to list the names when none matches. Each makes two Admin API requests, and `repositories remove` one more when no repository has the name. Removals do not appear in the audit log. ## Validate ```bash configdirector validate ``` `validate` runs the same scan, then checks every direct reference against the project's configs. It lists the problems it found by kind, and exits with 1 when there is at least one: ```txt Unknown keys src/checkout.ts:9 free-shiping: no config in web-app has this key. Deprecated keys src/checkout.ts:6 checkout-v2: the config was renamed to new-checkout. Type mismatches src/checkout.ts:8 checkout-timeout: read as string, but the config's type is timespan, served as a float. Availability mismatches src/theme.tsx:4 fraud-threshold: read from a client SDK (react), but the config is server-only. Enum values not allowed src/theme.tsx:3 theme: "system" is not one of the allowed values: light, dark. Checked 7 direct references in 4 scanned files and found 5 problems. 1 dynamic reference was not checked. ```
Problem What it means for your app What to do
Unknown keys No config in the project has this key, as its current key or an old one, so the SDK returns the in-code default value every time. The key of an archived config is unknown too. Correct the key, or create the config. If another project serves the key, add it to ignoreKeys .
Deprecated keys The config was renamed, and the code still reads an old key. The old key works until ConfigDirector removes it once it is no longer used, as Key explains. Replace it with the current key, which the output names.
Type mismatches The in-code default value, or a getter whose name sets the type such as getBoolean , reads the config as a type the SDKs do not serve it as, so the SDK can return the in-code default value instead of the config's value. Read the config as the type the output names.
Availability mismatches A client SDK reads a server-only config, or a server SDK reads a client-only one. That SDK never receives the config, so it returns the in-code default value every time. Change the config's SDK availability , or read the config where it is served.
Enum values not allowed The in-code default value is not one of the enum's allowed values. Use an allowed value as the in-code default value, or add the value to the enum.
The SDKs serve a timespan as a number in its unit and a URL as a string. So an in-code default value, or a getter whose name sets the type, reads a config of these types: a boolean reads a boolean config; an integer reads an integer, float or timespan config; a decimal number reads a float or timespan config; a string reads a string, enum or URL config; and an object or array reads a JSON config. Read a timespan with a number, not a string such as `"30s"`. An enum whose values are numbers can also be read with a number. A test client call is checked for unknown and deprecated keys only, since ConfigDirector never serves a test client. Indirect and dynamic references are never checked, and a config's lifecycle state is never a problem. ### Exit codes
Code Meaning
0 The command succeeded. For validate , it found no problem.
1 validate found at least one problem, or configs create found a config that already has the key.
2 The command could not run: a mistake in the command, no token or a rejected one, a token of another organization than the repository's, a missing permission, a suspended service, an unreachable API, or something the command names that does not exist, such as a config key, an environment, a saved context, a segment key, a tag or a scanned repository. segments delete also exits with 2 when the targeting rules of a config use the segment. The message says which.
A CI job can therefore tell a wrong config key from an outage. ### Run it in CI Add the token as a secret named `CONFIGDIRECTOR_TOKEN`. On GitHub, the ConfigDirector Code Scan action runs `validate` and uploads the scan. On GitLab and other CI systems, run the CLI with `npx`. #### GitHub Actions Add the secret under the repository's **Settings → Secrets and variables → Actions**, and add this workflow, or its last step to an existing job that checks out the code: ```yaml [.github/workflows/configdirector.yml] name: ConfigDirector on: pull_request: push: branches: [main] jobs: configdirector: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: ConfigDirector/scan-code@v1 with: token: ${{ secrets.CONFIGDIRECTOR_TOKEN }} ``` On a pull request, the action runs `validate`. On a push to the repository's default branch, it runs `validate` and then [`scan --upload`](#upload-the-scan-to-configdirector), so the dashboard shows where each config is read. It uploads even when `validate` found a problem, so the dashboard shows the code as it is, and the job still fails. `validate` needs `Projects: Read` and `Configs: Read`, and the upload also needs `Code references: Update`. With `upload: false`, the action never uploads, and a token with only the two read permissions is enough.
Input Default What it does
token Required. The API token, from a secret.
version latest The version of @configdirector/cli to run, or an npm dist-tag. A version number from the changelog , 0.9.0-beta.1 or later, upgrades on your own schedule.
working-directory . The directory that holds configdirector.json . The scan reads the files under it.
upload auto When the scan is uploaded: auto on pushes to the default branch only, true on every run, false never.
context-lines 2 How many lines before and after each reference its snippet holds, from 0 to 5.
snippets true false uploads no code, as --no-snippets does.
The action sets up Node.js 24, and the steps after it in the same job run with that Node.js too, so in a job that sets up another version, put the action after the steps that need it. The [action's README](https://github.com/ConfigDirector/scan-code) also covers pull requests from forks, which GitHub runs without your secrets. #### GitLab Add the token as a masked variable under the project's **Settings → CI/CD → Variables**, which GitLab passes to every job as an environment variable, and add this job to `.gitlab-ci.yml`. A variable marked **Protect variable** reaches only pipelines of protected branches and tags, so leave it unmarked for the check to run on merge requests. ```yaml [.gitlab-ci.yml] configdirector-validate: image: node:24 script: - npx --yes @configdirector/cli validate ``` To upload the scan from GitLab too, add the job in [Upload the scan to ConfigDirector](#upload-the-scan-to-configdirector). #### Other CI systems Run `npx --yes @configdirector/cli validate` with Node.js 20 or later and `CONFIGDIRECTOR_TOKEN` set, for every change, and `npx --yes @configdirector/cli scan --upload` after each push to the default branch. Each `scan` or `validate` run makes two Admin API requests, `scan --upload` makes three, `login` makes one, and `init` makes one per sign-in it searches; the first command run after upgrading to `0.10.0-beta.1` makes one more, to move the token an earlier version saved. The GitHub action makes two on a pull request and five on a push to the default branch. They count toward your organization's monthly [free allowance](https://docs.configdirector.com/getting-started/pricing-and-billing#the-free-allowance) like any other Admin API request. ## Evaluate ```bash configdirector evaluate new-checkout --environment production --context-id 123 --context-trait plan=pro ``` `evaluate` shows the value a context gets for a config in one environment, and why. It evaluates the environment's saved targeting rules with the same evaluator the SDKs use, so the value is the one your application would get: ```txt new-checkout in Production for user 123 true, served by rule 2 1. 50% get true not matched: bucket 71.3 is outside 0 to 50 2. plan is one of pro, team: true matched 3. in segment Beta testers: true not evaluated Default targeting rule: false ``` The first line names the config, the environment and the context. The next gives the value and the rule that served it, or the default targeting rule when no rule matched. Then comes each targeting rule in order, with what it tests, the value it serves, and its outcome:
Outcome What it means
matched The context meets every condition of the rule, and for a percentage rollout, its bucket falls in a share. The value comes from this rule.
not matched The rule does not apply to the context. The output names the first condition that failed and the context's value for it, such as plan is one of pro, team, but it is "free" , or the bucket the context fell in outside the rule's shares.
not evaluated An earlier rule served the value, so this rule was not checked.
errored The rule could not be evaluated, so it was skipped, as an SDK skips it.
A percentage rollout gives each context a bucket from 0 to 100, worked out from the config, the context kind and the identifier, so a context keeps its bucket from one evaluation to the next. A context without an identifier always gets bucket 0. ### The context Give the context one of three ways: - **Options**: `--context-kind` (`user` when left out), `--context-id`, `--context-name`, and `--context-trait name=value` for each trait. A trait's value is a string; write `name:=` followed by JSON for a number, a boolean, an array or an object, as in `--context-trait beta:=true` or `--context-trait age:=30`. `--context-trait` sets a top-level trait; for a nested one, use `--context-json`. - **JSON**: `--context-json '{"identifier": "123", "traits": {"address": {"country": "NZ"}}}'`, or `--context-json @context.json` to read the JSON from a file. The object has the fields `kind`, `identifier`, `name` and `traits`, with `kind` defaulting to `user`. - **A saved context**: `--saved-context user:123` evaluates a context ConfigDirector has stored from SDK evaluations in that environment, named by its context kind and identifier, with its stored name and traits. It needs the `Contexts: Read` permission. Using two of these at once is a mistake in the command, and `evaluate` exits with 2. With none of them, the config is evaluated for an empty context, as an SDK given no context would, and the first line says `for an empty context`. ### Environment, project and exit codes `--environment` takes the environment's slug, shown as **Environment slug** on the project's **Environments** tab, and is required: there is no default environment. The project comes from `--project`, `CONFIGDIRECTOR_PROJECT` or `configdirector.json`, as for `scan`. A renamed config can be evaluated by an old key, and the output then starts with a line naming its current key. `evaluate` exits with 0 whenever it evaluated the config, whichever value it got and whichever rule served it. It exits with 2 when it could not, for instance when no config in the project has the key, when no environment has the slug, in which case the message lists the project's environments, or when no saved context in the environment has that context kind and identifier. `evaluate` needs a token with `Projects: Read`, `Configs: Read` and `Environments: Read`, and `Contexts: Read` as well for `--saved-context`. Nothing is recorded: an evaluation from the CLI is not SDK activity, so it does not keep a config in use or count toward your SDK usage. Each run makes three Admin API requests. With `--json`, `evaluate` prints the Admin API's response as it was received, which [Evaluating a config](https://docs.configdirector.com/integrations/admin-api#evaluating-a-config) describes. ## Browse the project These commands read what a project holds, so you, or a coding agent working in your terminal, can look a config up without opening the dashboard:
Command What it shows Permission
configdirector projects list Your organization's projects, with their slugs and names Projects: Read
configdirector environments list The project's environments, with their slugs and names, marking the live ones Environments: Read
configdirector configs list The project's configs, with their type, role, lifetime and lifecycle state Configs: Read
configdirector configs get A config's settings, then its targeting rules in every environment Configs: Read
configdirector configs activity When SDKs last evaluated the config, per environment, SDK key kind, SDK name and SDK version Configs: Read
configdirector segments list The project's segments, with how many configs use each Segments: Read
configdirector segments get A segment's condition groups and its environment overrides Segments: Read
configdirector api-token get The token a command here would use: its organization, name, permissions and expiration, and whether it came from --token , CONFIGDIRECTOR_TOKEN or a sign-in. Never the token itself Any valid token
`projects list` lists every project of the token's organization, whichever one is selected, and `api-token get` reads no project. The other commands take the project from `--project`, `CONFIGDIRECTOR_PROJECT` or `configdirector.json`, as `scan` does, and also need `Projects: Read` to find it. `configs get` shows the config's settings, then each environment's targeting rules in order, written as `evaluate` writes them, with the default targeting rule value last: ```txt new-checkout Description: Shows the new checkout flow Type: boolean Role: flag Lifetime: temporary Availability: client and server SDKs Lifecycle state: in-use Tags: checkout test Default targeting rule: false production (live) 1. plan is one of pro, team: true 2. 30% get true, 70% get false 3. in segment Beta testers: true Default targeting rule: false ``` On an experiment, each value comes after the name of its variation, as in `40% get control (Start free trial), 60% get treatment-a (Try it free for 14 days)`. `evaluate` names the variation it served the same way, as in `treatment-a (Try it free for 14 days), served by rule 2`, and `configs create` names the variation of the default targeting rule value it starts with. Versions before 0.18.0 show the value alone. A segment condition names the segment when the token has `Segments: Read`, and shows the segment's ID otherwise. In the same way, `segments get` names the environment of each environment override by its slug when the token has `Environments: Read`, and by its ID otherwise. A renamed config can be read by an old key, and the output then starts with a line naming its current key. `configs list` takes options that narrow the list: - `--tag ` lists the configs that have that tag. Repeat it to list the configs that have any of the tags. It needs `Tags: Read` to look the names up, and a name no tag has stops the command with exit code 2. - `--untagged` lists the configs that have no tags. It cannot be combined with `--tag`. - `--state ` lists the configs in one lifecycle state: `new`, `in-use`, `settled`, `archive-ready` or `stale`. `configs activity` prints a header row, then one line per environment, SDK key kind, SDK name and SDK version, with the time of the most recent evaluation in UTC: ```txt ENVIRONMENT SDK KEY SDK LAST EVALUATED production server js-server 1.4.0 2026-10-17 14:03 UTC test client react 2.1.0 2026-10-16 09:12 UTC ``` `projects list`, `environments list`, `configs list`, `configs activity` and `segments list` print a header row that names their columns above their lines, such as `KEY TYPE ROLE LIFETIME STATE` for `configs list`, and `--no-headers` leaves it out. An empty list prints only a sentence, such as `No configs found in web-app.` `api-token get` prints one line each for the organization, with its name and id, the token's name, its permissions as the Admin API spells them, when it expires, or `never`, and where the token came from: ```txt Organization: Acme (9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d) Name: Laptop Permissions: configs:read, projects:read Expires: 2027-01-31 09:30 UTC Source: the CLI sign-in for Acme ``` With `--json`, each of these commands prints the Admin API's response as it was received, except `configs list --state`, which prints only the configs in that state. A key no config or segment has exits with 2. `projects list` and `api-token get` make one Admin API request and the others make two, plus one per `--tag` name, one for `configs get` when a rule uses a segment, and one for `segments get` when the segment has an environment override. ## Create a config ```bash configdirector configs create --key new-checkout --type boolean --role flag --default-value false ``` `configs create` creates a config in the project from options that match the Admin API's fields one to one. It never guesses a type or a role and never asks for a missing option, so you, a script or a coding agent always say exactly what to create: ```txt Created new-checkout in web-app: boolean, flag, temporary, client and server SDKs. Default targeting rule in every environment: false ```
Option Meaning
--key Required. The config's key .
--type Required. boolean , string , integer , float , timespan , enum , url or json .
--role Required. config , flag , kill-switch or experiment . Flags and kill switches are booleans, as Roles describes.
--lifetime Required for the config role. temporary or permanent . The other roles decide their own lifetime, as described below.
--default-value Required. The default targeting rule value every environment starts with. It is not the in-code default value your application passes to the SDK.
--description A description shown with the config in the dashboard.
--client-only , --server-only Serve the config to one kind of SDK only. Without either, client and server SDKs both receive it.
--tag A tag the project already has, by its name. Repeat it for each tag. A name no tag has stops the command with exit code 2.
--new-tag A tag to create in the project's tag catalog and assign, by its name. Repeat it for each tag. A name a tag already has stops the command with exit code 2.
--variation A variation of an experiment. Repeat it for each variation.
The values of `--default-value` and `--variation` are read in the config's type: `true` or `false` for a boolean, a number for an integer, float or timespan, the text as written for a string, enum or URL, and JSON for a JSON config, where `@values.json` reads the JSON from a file. A value that does not fit the type stops the command with exit code 2 before anything is sent. The role decides the [lifetime](https://docs.configdirector.com/getting-started/configs#lifetime) of a feature flag, a kill switch or an experiment, so you can leave `--lifetime` out for them: a feature flag and an experiment are always temporary, and a kill switch is always permanent. A lifetime the role does not have, such as `--role kill-switch --lifetime temporary`, stops the command with exit code 2 before anything is sent, and so does `--role config` without `--lifetime`. Versions before 0.16.0 need `--lifetime` for every role. A config can have up to 20 tags, from `--tag` and `--new-tag` together. Every `--tag` name is looked up before any tag is created, so a misspelled `--tag` creates nothing, and `--tag` never creates a tag. A `--new-tag` name given twice, or a name given with both options, stops the command with exit code 2 before anything is sent, and case doesn't matter in tag names. When a step after creating tags fails, for example because the key is taken, the tags stay in the project's tag catalog, and the command names them so you can pass them with `--tag` next time: ```txt Created the tag "Payments". It stays in web-app's tag catalog, so pass it with --tag next time. ``` Constraints have options of their own, and each one belongs to the types listed with it:
Type Options
integer , float --min and --max set bounds a value may equal; --exclusive-min and --exclusive-max set bounds a value must stay inside.
timespan --unit ( milliseconds , seconds or minutes ), --min and --max , which a timespan needs, and --allow-fractions to accept values such as 1.5 . A value may equal either bound.
enum --allowed-value for each value the config can take, and --enum-value-type number when the values are numbers. They are strings otherwise.
url --https-only to accept only HTTPS URLs. Without it, any HTTP or HTTPS URL is accepted.
json --json-schema , a file holding the JSON Schema every value must match.
An option given with another type, such as `--unit` with `--type integer`, stops the command with exit code 2 and names the types it belongs to. A timespan without `--unit`, `--min` or `--max` stops it with exit code 2 too. `configdirector configs create --help` lists the options grouped by type. A timespan read only by server SDKs, from 1 to 120 seconds and starting at 30 in every environment: ```bash configdirector configs create --key checkout-timeout --type timespan --role config --lifetime permanent \ --unit seconds --min 1 --max 120 --default-value 30 --server-only ``` `configs create` exits with 0 when it created the config. When the project already has a config with that key, as its current key or an old one, it creates nothing, prints that config and exits with 1: ```txt web-app already has a config with the key new-checkout, so no config was created: new-checkout boolean flag temporary in-use ``` It exits with 2 when ConfigDirector refuses the config, with the reasons it gave, such as a type the role does not allow or a default targeting rule value outside the bounds. With `--json`, it prints the created config as the Admin API returned it, or, when the key is taken, the existing config as `configs get --json` prints it. `configs create` needs `Projects: Read`, `Configs: Create`, and `Configs: Read` to show the config that already has the key. `--tag` also needs `Tags: Read` to look the names up, and `--new-tag` needs `Tags: Create` to create the tags. Each run makes two Admin API requests, plus one per `--tag` name, one per `--new-tag` name, and one more when ConfigDirector refuses the key, to look for the config that has it. The config shows in the [audit log](https://docs.configdirector.com/integrations/admin-api#terraform-opentofu-the-cli-and-the-audit-log) with the source **CLI**, as every change made with the CLI does. ## Copy a config ```bash configdirector configs copy search-ranking --key search-ranking --to mobile-app --environment qa=staging ``` `configs copy` creates a new config from an existing one, under the key `--key` gives, in the same project or, with `--to`, in another project of the organization. It never asks for anything, and prints the plan it followed: ```txt Copied search-ranking in web-app to search-ranking in mobile-app. Environments: production (live) from production 2 rules copied rule 3 dropped: mobile-app has no segment beta-testers qa from staging 1 rule copied Skipped source environments: test mobile-app has no environment with this slug; create it there to copy its rules Tags: search matched growth created ```
Option Meaning
Required. The key of the config to copy, in the linked project. An old key of the config works too.
--key Required. The copy's key , which no config in the destination project may have.
--to The project to copy the config into, another project of the organization. Without it, the copy is created in the config's own project.
--environment = The environment of the config's project that an environment of the destination project copies its targeting rules from. Repeat it for each one.
--dry-run Print the plan without creating anything.
The copy gets the config's role, lifetime, type, constraints, variations, SDK availability, description and tags, and every environment's targeting rules and default targeting rule value. It starts as a new config, with no old keys, evaluations or code references, and shares nothing with the config it came from. An archived config cannot be copied; restore it first. In another project, environments are matched by slug. A destination environment with no environment of the same slug in the config's project needs `--environment` to name the one it copies from, and the command stops with exit code 2, naming each such environment, until every one has a source; `--environment` can also replace a slug match. An environment of the config's project with no environment of its slug in the other project is skipped, and the plan says so. Tags are matched by name: a tag the other project doesn't have is created when the token has `Tags: Create`, and skipped otherwise. Segments are matched by key: a targeting rule that uses a segment the other project doesn't have is dropped, and the plan names it by its position and the segment's key. Create the segment in the other project first to keep the rule. Within one project, everything carries over as it is. Run it with `--dry-run` first to see the plan without creating anything. The plan then says what would be copied, dropped, skipped and created: ```txt Nothing was created: this is the plan for copying search-ranking in web-app to search-ranking in mobile-app. Environments: production (live) from production 2 rules would be copied rule 3 would be dropped: mobile-app has no segment beta-testers qa from staging 1 rule would be copied Source environments that would be skipped: test mobile-app has no environment with this slug; create it there to copy its rules Tags: search matched growth would be created ``` `configs copy` exits with 0 when it copied the config or printed the plan of a dry run, even when the plan drops a rule. It exits with 2 when ConfigDirector refuses the copy, with the reasons it gave, for example a key already in use or an environment with nothing to copy from, and when no config has the key or no project has the `--to` slug. With `--json`, it prints the Admin API's response as it was received: the plan, and, unless `--dry-run` was given, the created config with its targeting rules. `configs copy` needs `Projects: Read` and `Configs: Create`, and `Tags: Create` to create the tags another project doesn't have. It makes two Admin API requests. The copy shows in the [audit log](https://docs.configdirector.com/integrations/admin-api#terraform-opentofu-the-cli-and-the-audit-log) with the source **CLI**, noting the config it was copied from. ## Targeting rules ```bash configdirector targeting-rules set new-checkout --environment production --rules rules.json ``` `targeting-rules set` replaces a config's targeting rules in one environment with the rules in a JSON file. It shows no diff and asks for no confirmation, and it treats a live environment like any other, so look over the file before you run it. The file holds the default targeting rule value and the rules, in the format the [Admin API](https://docs.configdirector.com/integrations/admin-api#updating-targeting-rules) takes. This one serves `true` to contexts on the `pro` or `team` plan, and `false` to everyone else: ```json { "defaultValue": false, "rules": [ { "type": "conditional", "order": 0, "conditions": [ { "kind": "attribute", "attribute": "traits", "trait": "/plan", "operator": "is one of", "targetType": "text", "targetValues": ["pro", "team"] } ], "target": "value", "value": true } ] } ``` ```txt Replaced the targeting rules of new-checkout in production with 1 rule and the default targeting rule value false. ``` - **Every rule of the environment is replaced.** The file holds all the rules the environment keeps, in the order they are evaluated, and a rule left out of the file is removed. The config's other environments are not touched. - **IDs.** Every rule, condition and percentage in ConfigDirector has an `id`. Leave it out of a new one, and the CLI gives it a new ID before sending the file. An `id` in the file is sent as written, so the rules you keep keep their IDs. - **Start from the current rules.** `configs get --json` prints each environment's rules as an entry of `targets`, in the shape the file takes. Save the environment's entry, edit it, and pass it to `--rules`; its other fields, such as `environment`, are ignored:```bash configdirector configs get new-checkout --json \ | jq '.targets[] | select(.environment.slug == "production")' > rules.json # edit rules.json, then: configdirector targeting-rules set new-checkout --environment production --rules rules.json ```
`--rules -` reads the file from stdin instead, for a script that builds the rules. - **ConfigDirector checks the rules.** The CLI only checks that the file holds a JSON object. ConfigDirector checks the rules against the config's type and constraints, as the dashboard does, and when it refuses them the command exits with 2 and lists its reasons. `configdirector targeting-rules reference` prints the format the rules take: notes on how they are evaluated, then the condition kinds, the attributes a condition can test, and the operators of context kind conditions, segment conditions and each target type. With `--json`, it prints the whole [reference document](https://docs.configdirector.com/integrations/admin-api#targeting-rules-reference) as received, including the JSON Schema of the request `targeting-rules set` sends, which is the file with `environmentId` added. The notes are the ones the [MCP server](https://docs.configdirector.com/integrations/mcp) gives, so they name its tools: `update_targeting_rules` does what `targeting-rules set` does, and `get_config` what `configs get` does. `targeting-rules set` exits with 0 when it replaced the rules, and with 2 when it could not: no config in the project has the key, no environment has the slug, in which case the message lists the project's environments, or ConfigDirector refused the rules. `--environment` takes the environment's slug, as for `evaluate`, and a renamed config can be changed by an old key. With `--json`, it prints the request it sent, with the IDs it gave, since the Admin API answers with no body. `targeting-rules set` needs `Projects: Read`, `Environments: Read` and `Configs: Update targeting rules`, and makes three Admin API requests. `targeting-rules reference` needs a valid token, whatever its permissions, and no project, and makes one request. The change shows in the [audit log](https://docs.configdirector.com/integrations/admin-api#terraform-opentofu-the-cli-and-the-audit-log) with the source **CLI**. ## Segments ```bash configdirector segments create --segment beta-testers.json configdirector segments update beta-testers --segment beta-testers.json configdirector segments delete beta-testers ``` `segments create` and `segments update` write a [segment](https://docs.configdirector.com/getting-started/segments) from a JSON file, and `segments delete` deletes one. They show no diff and ask for no confirmation, so look over the file before you run them. The file holds the segment's key, its name and its condition groups, in the format the [Admin API](https://docs.configdirector.com/integrations/admin-api#segments) takes. A context is in the segment when every condition of any one group matches. `overrides` is optional, and maps an environment's ID to the condition groups that environment uses instead, as [Environment overrides](https://docs.configdirector.com/getting-started/segments#environment-overrides) describes. This segment holds the contexts whose email ends in `@example.com`, and those on the `beta` plan in New Zealand or Australia, except in the environment with the ID `7e6d5c4b-3a29-4810-8f7e-6d5c4b3a2918`, where it holds only staff: ```json { "key": "beta-testers", "name": "Beta testers", "groups": [ [ { "kind": "attribute", "attribute": "traits", "trait": "/email", "operator": "ends with any of", "targetType": "text", "targetValues": ["@example.com"] } ], [ { "kind": "attribute", "attribute": "traits", "trait": "/plan", "operator": "is one of", "targetType": "text", "targetValues": ["beta"] }, { "kind": "attribute", "attribute": "traits", "trait": "/country", "operator": "is one of", "targetType": "text", "targetValues": ["NZ", "AU"] } ] ], "overrides": { "7e6d5c4b-3a29-4810-8f7e-6d5c4b3a2918": [ [ { "kind": "attribute", "attribute": "traits", "trait": "/email", "operator": "ends with any of", "targetType": "text", "targetValues": ["@staff.example.com"] } ] ] } } ``` `segments create --segment beta-testers.json` prints what it created, the way `segments get` shows a segment: ```txt Created segment beta-testers in web-app. beta-testers Name: Beta testers Project definition 1. email ends with any of @example.com 2. plan is one of beta and country is one of NZ, AU Override in production 1. email ends with any of @staff.example.com ``` - **IDs.** Every condition in ConfigDirector has an `id`. Leave it out of a new one, in the condition groups or in an override, and the CLI gives it a new ID before sending the file. An `id` in the file is sent as written, so the conditions you keep keep their IDs. - **Start from the current segment.** `segments get --json` prints a segment in the shape the file takes. Save it, edit it, and pass it to `--segment`; its other fields, such as `usages`, are ignored:```bash configdirector segments get beta-testers --json > beta-testers.json # edit beta-testers.json, then: configdirector segments update beta-testers --segment beta-testers.json ```
`--segment -` reads the file from stdin instead, for a script that builds the segment. - **Environment IDs.** `overrides` is keyed by the environment's ID, not its slug. `environments list --json` prints the ID of each environment. - **What update replaces.** `segments update ` replaces the key, the name and the condition groups of the segment the key names, so a different `key` in the file renames it. The segment keeps its own ID whatever `id` the file holds, so a file saved from another segment updates the one you name. Without `overrides` in the file, the environment overrides stay as they are. With `"overrides": {}`, they are all removed, and with a map, exactly those environments are overridden and the others go back to the project definition. - **ConfigDirector checks the segment.** The CLI only checks that the file holds a JSON object. ConfigDirector checks the rest, as the dashboard does, and when it refuses the segment, for example because another segment has its key, the command exits with 2 and lists its reasons. `segments delete ` deletes the segment and its environment overrides: ```txt Deleted segment beta-testers from web-app. ``` It is refused while the targeting rules of any config use the segment, in any environment; archived configs are not counted. The command then exits with 2 and lists those configs with the environments whose rules use the segment, so you can remove those segment conditions with [`targeting-rules set`](#targeting-rules) first: ```txt Error: The targeting rules of 2 configs use the segment beta-testers, so it was not deleted: new-checkout in production dark-mode in test, production Remove those segment conditions first, with configdirector targeting-rules set. ``` The three commands exit with 0 when they wrote or deleted the segment, and with 2 when they could not: no segment in the project has the key given to `update` or `delete`, ConfigDirector refused the segment, or a config uses the segment you tried to delete. With `--json`, `create` and `update` print the segment as the Admin API returned it, and `delete` prints `{ "deletedSegmentKey": "beta-testers" }`, with the key as you gave it, since the Admin API answers with no body. `segments create` needs `Projects: Read` and `Segments: Create`, `segments update` needs `Projects: Read`, `Segments: Read` and `Segments: Update`, and `segments delete` needs `Projects: Read` and `Segments: Delete`. With `Environments: Read`, the output names environments by their slug, and without it, by their ID. `create` makes two Admin API requests and `update` three, plus one to name the environments when the segment has an override; `delete` makes two, plus one to name the environments when it is refused. The change shows in the [audit log](https://docs.configdirector.com/integrations/admin-api#terraform-opentofu-the-cli-and-the-audit-log) with the source **CLI**. ## Archive a config ```bash configdirector configs archive new-checkout ``` `configs archive` archives a config: it is removed from the project and from what SDKs receive, so your applications get their in-code default value for it. You can restore it from the dashboard for 30 days, with its targeting rules intact, and after that it is deleted for good, as [Archiving](https://docs.configdirector.com/monitoring/lifecycle#archiving) describes: ```txt Archived new-checkout in web-app. It can be restored from the dashboard for 30 days. ``` It asks for no confirmation and does not check whether your code still reads the key. Run [`scan`](#scan) first and look at every reference to the key, the indirect ones included, since an application that still reads it after the archive gets its in-code default value and raises a [Config not found alert](https://docs.configdirector.com/monitoring/alerts#config-not-found). A renamed config can be archived by an old key. `configs archive` exits with 0 when it archived the config, and with 2 when no config in the project has the key, which includes a config already archived. With `--json`, it prints `{ "archivedConfigKey": "new-checkout" }`, with the key as you gave it, since the Admin API answers with no body. `configs archive` needs `Projects: Read` and `Configs: Archive` (`configs:archive`), and makes two Admin API requests. The change shows in the [audit log](https://docs.configdirector.com/integrations/admin-api#terraform-opentofu-the-cli-and-the-audit-log) with the source **CLI**. ## Output Every command prints text for people by default. With `--json`, it prints JSON instead, for scripts and CI. Errors and the token prompt go to stderr, so with `--json` the JSON is all that reaches stdout: ```bash configdirector scan --json | jq -r '.references[] | select(.key == "new-checkout") | "\(.file):\(.line)"' ``` `validate --json` prints the scan's JSON with a `findings` array, and still exits with 1 when the array is not empty. Shortened to one reference of each kind and one finding: ```json { "project": "web-app", "filesScanned": 4, "references": [ { "key": "new-checkout", "file": "src/checkout.ts", "line": 5, "kind": "direct", "sdk": "js-server", "call": "getValue", "argument": "NEW_CHECKOUT", "default": { "type": "boolean", "value": false }, "test": false }, { "key": "new-checkout", "file": "src/flags.ts", "line": 1, "kind": "indirect", "test": false } ], "dynamicReferences": [ { "file": "src/theme.tsx", "line": 5, "sdk": "react", "call": "useConfigValue", "argument": "setting.key" } ], "findings": [ { "kind": "availability-mismatch", "key": "fraud-threshold", "file": "src/theme.tsx", "line": 4, "sdk": "react", "sdkType": "client", "configAvailableTo": ["server"] } ] } ``` A direct reference has `argument` when its key came from a constant, `default` when the call passes an in-code default value, and `getterType` when the getter's name sets the type. In `scan --json`, each direct and indirect reference also has its [snippet](#snippets), which `validate --json` leaves out: ```json "snippet": { "firstLine": 3, "lines": [ "const client = createClient(\"YOUR-SERVER-SDK-KEY\");", "", "export const isNewCheckout = () => client.getValue(NEW_CHECKOUT, false);", "", "export const checkoutPath = () => (isNewCheckout() ? \"/checkout/v2\" : \"/checkout\");" ], "language": "typescript", "truncatedLines": [] } ``` `firstLine` is the number of the snippet's first line in the file, `language` the language to highlight the lines as, and `truncatedLines` the numbers of the lines cut to 250 characters. Each finding has a `kind`, the `key`, the `file` and the `line`, and the config facts it was checked against:
kind Also has
unknown-key
deprecated-key currentKey
type-mismatch configType , servedAs , and default , getterType or both
availability-mismatch sdk , sdkType , configAvailableTo
enum-value-not-allowed default , allowedValues
The [JSON Schema](https://github.com/ConfigDirector/cli/blob/main/schema/scan-output.schema.json) in the package describes every field. # MCP Server > Manage projects, configs, targeting rules, and segments from AI coding tools through the Model Context Protocol ## Introduction The ConfigDirector MCP server allows AI coding assistants to read and update your projects, environments, configs, targeting rules, and segments. It runs as a hosted endpoint, so there is nothing to install: ```text https://mcp.configdirector.com ``` It speaks the [Model Context Protocol](https://modelcontextprotocol.io) over Streamable HTTP and authenticates with a ConfigDirector API token. Every update an assistant makes goes through the same validations as the dashboard and the [Admin API](https://docs.configdirector.com/integrations/admin-api). Updates to configs and segments show in the audit log with a source of **MCP**. Code assistant tool calls count as Admin API requests in your organization's usage: one tool call counts as one Admin API request. ## Create an API token 1. In the ConfigDirector dashboard, open **API tokens** and create a token. 2. Choose the permissions the assistant needs. For exploring and answering questions, `Projects: Read`, `Environments: Read`, and `Configs: Read` are enough; add `Contexts: Read` for the assistant to evaluate a config for a context ConfigDirector has stored from SDK evaluations. If you want the assistant to be able to create and update configs and flags, add `Configs: Create`, `Configs: Update settings`, and `Configs: Update targeting rules`; add `Configs: Archive` for it to archive them. For [segments](https://docs.configdirector.com/getting-started/segments), add `Segments: Read` to read them, `Segments: Create` to create them, `Segments: Update` to change them, and `Segments: Delete` to delete them. 3. It's recommended that you set an expiration on the token since it will be stored on your workstation to configure your coding tool of choice. 4. Copy the token, and optionally save it into a password manager. Do not commit the token into source control. 5. The examples below use an environment variable named `CONFIGDIRECTOR_TOKEN`, wherever the tool supports that, to provide the API token. For the project tag catalog, choose `Tags: Read` for listing and lookup and `Tags: Create` for creation. These permissions are independent of config permissions. Existing tokens do not gain tag scopes automatically. The MCP server only exposes the tools that match your token's permissions, so a read-only token produces a read-only assistant. ## What the assistant can do
Tools What they do
list_projects , get_project , create_project , update_project Retrieve projects general information. Create and update projects in your organization.
list_environments , get_environment , create_environment , update_environment Retrieve environments within a project. Create new environments, and update environments.
list_configs , get_config , get_config_activity Retrieve Configs, their targeting rules per environment, and their most recent evaluations. list_configs takes the same search and filters as the configs list in the dashboard.
evaluate_config Evaluate a config for a context in an environment and explain the result: the value served, the rule that served it, and why each rule matched or not. Takes an inline context, a saved context (which also needs Contexts: Read ), or no context at all for an anonymous visitor. Creates no SDK activity.
create_config , update_config_settings Create configs, feature flags, kill switches, or experiments; the role decides the lifetime of every role but config, so only a config needs one. Update their settings, for example, the allowed values in an enum, or update a feature flag to a kill switch and back.
copy_config Copy a config into a new config, in the same project or another project of your organization, with its tags and every environment's targeting rules. Across projects, environments are matched by slug, tags by name, and segments by key: a rule whose segment the destination project doesn't have is left out, and a missing tag is created only with Tags: Create . Each copy reports what it carried over: for each environment, where its rules came from and which rules were left out, and which tags were matched, created, or skipped. A dry run gives that report without creating anything.
archive_config Archive a config. The dashboard can restore it for 30 days.
update_targeting_rules Update the targeting rules of a config in a given environment.
get_targeting_rules_reference Retrieves the targeting rules format, operators, and target types. This is used by the assistant to write valid targeting rules
list_segments , get_segment Retrieve the segments of a project, with their condition groups, environment overrides, and the configs that use them.
create_segment , update_segment , delete_segment Create, update, and delete segments. A segment cannot be deleted while the targeting rules of a config use it.
list_tags , get_tag , find_tag_by_name Browse the shared project tag catalog and resolve a stable ID by name. Requires tags:read .
create_tag Create a shared project tag with a trimmed, case-insensitively unique name of 1–50 characters. Requires tags:create .
`list_configs` lists every config of the project unless the assistant narrows the list down. `search` lists the configs whose key or description contains some text, ignoring case. `role`, `type`, `status` and `lifetime` take one value each: a role (`config`, `flag`, `kill-switch` or `experiment`), a type (`boolean`, `string`, `integer`, `float`, `enum`, `url`, `json` or `timespan`), a lifecycle state (`new`, `in-use`, `settled`, `archive-ready` or `stale`), or `temporary` or `permanent`. `tagIds` lists the configs that have any of the given tags, and `untagged` only the configs without tags. A config is listed only when it matches every filter the assistant sends, and filtering needs only `Configs: Read`. For example, asking for the stale feature flags of a project sends `role: flag` and `status: stale`. Tag names preserve display casing and allow spaces and punctuation. Name lookup ignores differences in casing and leading or trailing whitespace. Tags do not affect evaluation or appear in SDK payloads. A tag that was deleted matches no config, so asking only for deleted tags lists no configs rather than all of them. Things you can ask once connected: - "List the flags in the checkout project." - "Which flags in the checkout project are settled and ready to remove from the code?" - "Create a kill switch called payments-v2 in the checkout project, off in all environments." - "new-checkout is staying for good. Make it a kill switch." - "In production, roll new-search out to 10 percent of users, and to everyone whose email ends in @example.com." - "Create a beta testers segment for everyone whose email ends in @example.com, and turn new-search on for it in staging." - "Which configs in production have not been evaluated in the last week?" - "What does user 123 get for new-checkout in production, and why?" The assistant sees a short guide to ConfigDirector's vocabulary when it connects (roles, lifetimes, types, how targeting rules work, and segments), so you can talk about configs, feature flags, rollouts, and segments in plain terms. ## Claude Code Add the server for yourself in the current project (the default), for yourself in every project with `--scope user`, or for the whole team with `--scope project`, which writes `.mcp.json` in the project root: ```bash claude mcp add --transport http configdirector https://mcp.configdirector.com \ --header 'Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}' ``` The single quotes keep your shell from putting the token itself in the configuration: Claude Code reads `CONFIGDIRECTOR_TOKEN` each time it starts, so a token you replace takes effect on the next start, and `--scope project` commits only the variable's name. Or write `.mcp.json` by hand. Claude Code expands environment variables in the `url` and `headers` fields, so the token stays out of the repository: ```json [.mcp.json] { "mcpServers": { "configdirector": { "type": "http", "url": "https://mcp.configdirector.com", "headers": { "Authorization": "Bearer ${CONFIGDIRECTOR_TOKEN}" } } } } ``` Type `/mcp` inside Claude Code to check: the server should show as connected with its tools listed. From a terminal, `claude mcp list` shows the same. ## Cursor Create `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for every project. Cursor expands `${env:NAME}` in this file: ```json [.cursor/mcp.json] { "mcpServers": { "configdirector": { "url": "https://mcp.configdirector.com", "headers": { "Authorization": "Bearer ${env:CONFIGDIRECTOR_TOKEN}" } } } } ``` Open Cursor's MCP settings to confirm the server is enabled and its tools are listed. The variable has to be set in the environment Cursor was launched from. ## VS Code with GitHub Copilot Create `.vscode/mcp.json`. The `inputs` entry makes VS Code prompt for the token once and store it securely, so nothing sensitive goes in the file: ```json [.vscode/mcp.json] { "inputs": [ { "type": "promptString", "id": "configdirector-token", "description": "ConfigDirector API token", "password": true } ], "servers": { "configdirector": { "type": "http", "url": "https://mcp.configdirector.com", "headers": { "Authorization": "Bearer ${input:configdirector-token}" } } } } ``` Run **MCP: List Servers** from the command palette to start the server and see its status. The tools appear in the Copilot chat tools picker in agent mode. ## Windsurf Open the MCP settings from the Cascade panel, or edit `~/.codeium/windsurf/mcp_config.json`. Windsurf expands `${env:NAME}` in this file: ```json [mcp_config.json] { "mcpServers": { "configdirector": { "serverUrl": "https://mcp.configdirector.com", "headers": { "Authorization": "Bearer ${env:CONFIGDIRECTOR_TOKEN}" } } } } ``` ## OpenAI Codex CLI Add to `~/.codex/config.toml`. Codex reads the token from the named environment variable at startup: ```toml [~/.codex/config.toml] [mcp_servers.configdirector] url = "https://mcp.configdirector.com" bearer_token_env_var = "CONFIGDIRECTOR_TOKEN" ``` Run `codex mcp list` to confirm. ## Gemini CLI Add to `~/.gemini/settings.json`, or to the project's `.gemini/settings.json`. Gemini CLI expands `$NAME` in string values: ```json [settings.json] { "mcpServers": { "configdirector": { "httpUrl": "https://mcp.configdirector.com", "headers": { "Authorization": "Bearer $CONFIGDIRECTOR_TOKEN" } } } } ``` Or add it from the command line: ```bash gemini mcp add --transport http configdirector https://mcp.configdirector.com \ --header "Authorization: Bearer ${CONFIGDIRECTOR_TOKEN}" ``` Type `/mcp` in Gemini CLI to see the server and its tools. ## Claude Desktop and other tools without header support Claude Desktop's custom connectors expect OAuth sign-in, which the ConfigDirector server does not offer yet. Until then, bridge it with [mcp-remote](https://github.com/geelen/mcp-remote), which runs locally and forwards to the hosted server with your token. Edit `claude_desktop_config.json` (**Settings → Developer → Edit Config**): ```json [claude_desktop_config.json] { "mcpServers": { "configdirector": { "command": "npx", "args": ["mcp-remote", "https://mcp.configdirector.com", "--header", "Authorization:${AUTH_HEADER}"], "env": { "AUTH_HEADER": "Bearer " } } } } ``` The header value goes through an environment variable because some desktop clients split arguments on spaces. The same bridge works for any client that can only launch a local command. ## More than one organization An API token belongs to one organization, so working for two organizations means one token each. Give each repository its own configuration that reads a variable named for the organization, such as `CONFIGDIRECTOR_TOKEN_ACME` and `CONFIGDIRECTOR_TOKEN_GLOBEX`, and set both variables in your shell. For Claude Code, add the server in each repository with the default scope, which is local to that repository and to you: ```bash cd ~/code/acme-storefront claude mcp add --transport http configdirector https://mcp.configdirector.com \ --header 'Authorization: Bearer ${CONFIGDIRECTOR_TOKEN_ACME}' cd ~/code/globex-portal claude mcp add --transport http configdirector https://mcp.configdirector.com \ --header 'Authorization: Bearer ${CONFIGDIRECTOR_TOKEN_GLOBEX}' ``` The same works for Cursor, VS Code and Gemini CLI, whose configuration file lives in the repository. Do not set `CONFIGDIRECTOR_TOKEN` for every terminal: the [CLI](https://docs.configdirector.com/integrations/cli#more-than-one-organization) uses that variable ahead of its own sign-ins, so a command in the other organization's repository would send the wrong token and stop. Windsurf, Codex and Claude Desktop read one configuration for every repository, so the variable it names has to hold the right organization's token where the tool starts, for example from a loader such as direnv that sets it per directory. Otherwise, let that tool work for one organization, and reach the other through the CLI, which keeps one sign-in per organization. ## Limits and billing - Each tool call counts as one Admin API request against the monthly [free allowance](https://docs.configdirector.com/getting-started/pricing-and-billing#the-free-allowance). Connecting and listing tools does not. - An organization can make up to 60 tool calls per minute across all of its tokens. If that limit is exceeded, the assistant receives a message asking it to wait until the next minute. - Any update the assistant makes is validated like any other: type constraints, JSON Schema, and the hourly [JSON Schema budgets](https://docs.configdirector.com/getting-started/json-schema-validation#hourly-budgets-on-free) on Free all apply, and the assistant receives the same messages you would see in the dashboard. - Every update appears in the audit log of the config or segment with the source **MCP** and the token owner's name. ## Security notes - Prefer environment variables or your editor's secret storage over tokens written into config files. Project-level files such as `.mcp.json` and `.cursor/mcp.json` are usually committed, do not add an API token to them. - Give each tool its own token with the smallest permission set needed. Revoke the token from the API tokens page when you are no longer using the tool. - You can rename a token or change its permissions from the API tokens page without replacing the token. Assistants see newly allowed tools after they reconnect. - The MCP server never returns SDK keys or API tokens. # Terraform Provider > Manage projects, environments, configs, tags, targeting rules, segments, and webhooks as code with the ConfigDirector Terraform provider ## Introduction The ConfigDirector Terraform provider allows you to manage projects, environments, configs, tags, targeting rules, segments, and webhooks via Terraform and OpenTofu. To make the same changes from an AI coding assistant instead, see the [MCP server](https://docs.configdirector.com/integrations/mcp). Refer to the [official ConfigDirector Terraform provider documentation](https://registry.terraform.io/providers/ConfigDirector/configdirector/latest/docs) for a full guide on all the features available in the provider. ## Example Here is a minimal example on how to use the provider: ```terraform [main.tf] terraform { required_providers { configdirector = { source = "ConfigDirector/configdirector" version = "~> 0.6" } } } provider "configdirector" {} resource "configdirector_project" "example" { name = "Targeting Rules Example" slug = "targeting-rules-example" } resource "configdirector_config" "beta_features" { project_id = configdirector_project.example.id key = "beta-features" role = "flag" type = "boolean" initial_value = false } resource "configdirector_config_targeting_rules" "beta_features_test" { project_id = configdirector_project.example.id config_key = configdirector_config.beta_features.key environment_slug = "test" default_value = "false" rules = [ { id = provider::configdirector::rule_id("beta-features-test-named-testers") type = "conditional" order = 0 target = "value" value = true conditions = [ { id = provider::configdirector::rule_id("beta-features-test-named-testers-identifier") attribute = "identifier" operator = "is one of" targetType = "text" targetValues = ["user-101", "user-202"] } ] } ] } resource "configdirector_config_targeting_rules" "beta_features_production" { project_id = configdirector_project.example.id config_key = configdirector_config.beta_features.key environment_slug = "production" default_value = "false" rules = [ { id = provider::configdirector::rule_id("beta-features-production-rollout") type = "percentage" order = 0 target = "percentage" percentages = [ { id = provider::configdirector::rule_id("beta-features-production-rollout-on") percentage = 10 value = true }, { id = provider::configdirector::rule_id("beta-features-production-rollout-off") percentage = 90 value = false }, ] } ] } ``` A condition can also test the [context kind](https://docs.configdirector.com/getting-started/core-concepts#contexts): set `attribute` to `"kind"`, `targetType` to `"text"`, and `operator` to `"equals"`, `"does NOT equal"`, `"is one of"`, or `"is NOT one of"`, with context kinds as the target values. A context sent without a context kind is tested as `user`. For example, this condition matches only devices: ```terraform { id = provider::configdirector::rule_id("beta-features-test-devices-kind") attribute = "kind" operator = "equals" targetType = "text" targetValues = ["device"] } ``` ## Segments The `configdirector_segment` resource manages a [segment](https://docs.configdirector.com/getting-started/segments), and is available from version 0.3.0 of the provider. `groups` holds the project definition as a list of condition groups, each a list of attribute conditions in the same shape as in targeting rules. `overrides` maps an environment id to that environment's own groups. The resource owns every override of the segment: leaving `overrides` out, or removing an environment from it, removes that override. `groups` and `overrides` are sent as written and never read back, the same as the `rules` of `configdirector_config_targeting_rules`, so changes made outside Terraform do not show as drift. A targeting rule uses the segment through a segment condition with `kind = "segment"`, an `operator` of `"in"` or `"not in"`, and the segment's `id` as `segmentId`. Continuing the example above: ```terraform [main.tf] resource "configdirector_environment" "staging" { project_id = configdirector_project.example.id name = "Staging" slug = "staging" color = "purple" live = false } resource "configdirector_segment" "beta_testers" { project_id = configdirector_project.example.id key = "beta-testers" name = "Beta testers" groups = [ [ { id = provider::configdirector::rule_id("beta-testers-acme-email") attribute = "traits" trait = "/email" operator = "ends with any of" targetType = "text" targetValues = ["@acme.com"] } ], [ { id = provider::configdirector::rule_id("beta-testers-opted-in") attribute = "traits" trait = "/betaOptIn" operator = "equals" targetType = "boolean" targetValues = ["true"] }, { id = provider::configdirector::rule_id("beta-testers-app-version") attribute = "appVersion" operator = ">=" targetType = "semver" targetValues = ["2.0.0"] } ] ] overrides = { (configdirector_environment.staging.id) = [ [ { id = provider::configdirector::rule_id("beta-testers-staging-named-users") attribute = "identifier" operator = "is one of" targetType = "text" targetValues = ["qa-user-1", "qa-user-2"] } ] ] } } resource "configdirector_config" "new_checkout_flow" { project_id = configdirector_project.example.id key = "new-checkout-flow" role = "flag" type = "boolean" initial_value = false } resource "configdirector_config_targeting_rules" "new_checkout_flow_production" { project_id = configdirector_project.example.id config_key = configdirector_config.new_checkout_flow.key environment_slug = "production" default_value = "false" rules = [ { id = provider::configdirector::rule_id("new-checkout-flow-production-beta-testers") type = "conditional" order = 0 target = "value" value = true conditions = [ { id = provider::configdirector::rule_id("new-checkout-flow-production-beta-testers-segment") kind = "segment" operator = "in" segmentId = configdirector_segment.beta_testers.id } ] } ] } ``` An existing segment is imported by the project's id or slug and the segment's key, for example `terraform import configdirector_segment.beta_testers targeting-rules-example/beta-testers`. The groups and overrides cannot be read back, so the first apply after an import sends whatever the configuration declares. ## Config tags Declare config assignments by name with `tags` on `configdirector_config`. A managed tag's `.name` reference makes Terraform or OpenTofu wait for that tag's creation or rename: ```terraform [main.tf] resource "configdirector_tag" "payments" { project_id = configdirector_project.example.id name = "Payments / EU & US" } resource "configdirector_config" "checkout" { project_id = configdirector_project.example.id key = "checkout-enabled" role = "flag" type = "boolean" initial_value = false tags = [configdirector_tag.payments.name] } ``` You can also assign existing literal names, such as `tags = ["Payments / EU & US"]`. Names are resolved within the config's project using the API's trimming and case-equivalent matching. Spaces, slashes, and punctuation are literal; do not URL-encode them. An unknown name fails with a diagnostic naming the tag and project. Assignments never create tags implicitly. Unknown `.name` references stay unknown during planning until the tag is available.
Configuration Effect
Omit tags , or use null Observe names and preserve remote assignments
tags = [] Clear every assignment and manage an empty selection
A nonempty set of names Manage that selection and correct assignment drift
Omission preserves assignments after import and during unrelated updates. Removing `tags` relinquishes ownership without clearing the selection. Import a config with `/`, for example `terraform import configdirector_config.checkout targeting-rules-example/checkout-enabled` or the same command with `tofu`. After refresh, omit `tags` to preserve assignments or declare a set to take ownership. Assignments are unordered and limited to 20 distinct project tags. Equivalent spellings share one stable ID; state retains configured spelling while the assigned tag IDs are unchanged, so casing and order alone do not cause repeated drift. A shared tag deletion detaches its assignments, and a declared name that no longer exists fails on the next write. Tags organize configs without affecting evaluation. Name resolution requires `tags:read`. Creating a tagged config needs `configs:create`, and changing its tags later needs `config-settings:update`. Provider updates also read the config back, so include `configs:read` for that step. Omitted and empty selections need no catalog lookup. The tag resource separately needs `tags:create`, `tags:update`, and `tags:delete` for the corresponding catalog operations, plus `tags:read` for refresh. Missing scopes produce errors instead of silently skipping assignments. `data.configdirector_config..tags` and each config in `data.configdirector_configs..configs` expose current assigned names under `configs:read` alone. No standalone tag lookup data source is required. The resource and data-source configuration is the same in Terraform and OpenTofu. Assignment support can deploy once the catalog and assignment APIs are available on every API instance. It does not depend on config table filters or dashboard feature flags. ## Webhooks The `configdirector_webhook` resource manages a [webhook](https://docs.configdirector.com/integrations/webhooks), and is available from version 0.5.0 of the provider. Pass its `secret` to your receiver so it can [verify each request](https://docs.configdirector.com/integrations/webhooks#verifying-requests): ```terraform [main.tf] resource "configdirector_webhook" "ci" { name = "CI pipeline" url = "https://example.com/configdirector-webhooks" project_ids = [configdirector_project.example.id] headers = { Authorization = "Bearer ${var.ci_token}" } } module "ci_receiver" { source = "./ci-receiver" signing_secret = configdirector_webhook.ci.secret } ``` A webhook receives every change event type unless `event_types` lists some, and no alert events unless `alert_kinds` lists some, the same defaults as the dashboard's form. Set `event_types = []` for a webhook that receives only alert events. Leave `project_ids`, `environment_ids`, and `tag_ids` empty to let everything through; [the filters](https://docs.configdirector.com/integrations/webhooks#creating-a-webhook) explain what each one lets through. `secret` and `headers` are sensitive, so no plan shows them. The secret stays the same across updates. After a rotation on the webhook's page, the next refresh picks up the new secret and the next apply passes it on to your receiver. ConfigDirector never shows a header value again once it is saved, so Terraform notices a header added or removed outside Terraform but not a value changed there. If ConfigDirector [disables the webhook](https://docs.configdirector.com/integrations/webhooks#when-a-webhook-is-disabled) after repeated failures, the next apply enables it again while `enabled` is `true`, its default. Import a webhook with its ID, for example `terraform import configdirector_webhook.ci 01a18564-96d6-7026-bb0e-aab59053d921` or the same command with `tofu`. The first apply after an import writes the header values the configuration declares. The API token needs the `webhooks:manage` permission, which only owners and admins can grant. ## Deletion protection A project or environment with [deletion protection](https://docs.configdirector.com/getting-started/core-concepts#deletion-protection) on cannot be deleted, so `terraform destroy`, or an apply that would remove the resource, fails with a message naming where to turn it off. The provider cannot change the setting. Projects and environments the provider creates start with it off; projects and live environments created in the dashboard start with it on, so turn it off in the dashboard before removing one of those from your configuration. ## OpenTofu The ConfigDirector provider is also [available in the OpenTofu registry](https://search.opentofu.org/provider/configdirector/configdirector/latest). # Webhooks > Get a signed HTTPS request whenever a config, its targeting rules, a segment, an environment, or a project changes, or an alert is raised or resolved. What a webhook event holds, how to verify it, every event type with a sample, and how delivery and retries work. ## Introduction A webhook is a URL you give ConfigDirector. Whenever someone changes a config, its targeting rules, a segment, an environment, or a project, or an alert is raised or resolved, ConfigDirector sends that URL an HTTPS `POST` describing what happened: a webhook event. Use webhooks to post flag changes to a chat channel, start a pipeline when production changes, or open an incident when an alert fires. A webhook event carries the entity that changed, the way the [Admin API](https://docs.configdirector.com/integrations/admin-api) returns it, together with the values it had before the change, so your receiver never has to call back to find out what happened. If you have handled Stripe webhooks, the shape will look familiar. Webhooks belong to the organization, and only owners and admins can see and manage them. They work the same on Free and Pay as you go. Webhook events come from changes that people make, in the dashboard or through an API token. Changes ConfigDirector makes on its own, such as a config's lifecycle state moving to settled, send none. To hear about those, subscribe to the matching alert, such as **Config settled**. ## What a webhook event looks like This is a `targeting_rules.updated` webhook event, sent when a member turned a flag on in Production for everyone except two users: ```json { "id": "01a18500…d66c", "data": { "object": { "id": "01a18500…b84e", "rules": [ { "id": "f3538fdb…b8b9", "type": "conditional", "order": 0, "value": false, "target": "value", "valueId": "3TUVuO…", "conditions": [ { "id": "8d6b6e0b…1ccb", "kind": "attribute", "operator": "is one of", "attribute": "identifier", "targetType": "text", "targetValues": [ "user-101", "user-202" ] } ], "percentages": [] } ], "environment": { "id": "01a184ff…ee77", "live": true, "name": "Production", "slug": "production", "color": "red" }, "defaultValue": "true", "defaultValueId": "0Br7gC…" }, "previousAttributes": { "rules": [], "defaultValue": "false", "defaultValueId": "3TUVuO…" } }, "type": "targeting_rules.updated", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "config": { "id": "01a18500…0339", "key": "checkout.one-page" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:12:50.165Z", "environment": { "id": "01a184ff…ee77", "live": true, "name": "Production", "slug": "production" }, "organization": { "id": "01a01632…6040", "name": "Acme" } } ```
Field What it holds
id The webhook event's id. It is the same on every webhook that receives the webhook event and on every resend, and it is also sent as the webhook-id header. Use it to drop duplicates.
type What happened, such as targeting_rules.updated . See Webhook event types .
createdAt When the change was made, or when the alert was raised or resolved.
organization The organization's id and name .
project The project's id , slug , and name . Every change has one.
environment The environment's id , slug , name , and whether it is live . Present on targeting rules changes, environment changes, and a segment change to one environment's override.
config The config's id and key . Present on config and targeting rules changes.
actor Who made the change. member holds the member's id , name , and email ; name and email are null if the member's account has been deleted. sourceClient is where the change came from: dashboard , api , terraform , opentofu , mcp , or cli . apiToken holds the token's id and name when the change was made with an API token. Alert and test webhook events have no actor.
alertKind The alert's kind, on alert webhook events only.
data.object The entity after the change, as the Admin API returns it. On a deletion it is the entity as it was just before, and some fields, such as createdAt , updatedAt , and a segment's overrides , can be missing.
data.previousAttributes On *.updated webhook events only: each property that changed, with its value before the change.
Build your receiver to ignore fields it does not know. New fields can appear in any webhook event without notice, while existing fields keep their names and meaning. The order of the keys means nothing either: as the samples on this page show, it is not the order of the table above. ## Creating a webhook 1. Open **Webhooks** at the bottom of the dashboard's navigation and click **New webhook**. 2. Give it a name and the `https://` URL that should receive the webhook events. 3. Add any **Custom headers** your receiver expects, such as an `Authorization` header. See [URL rules and custom headers](#url-rules-and-custom-headers). 4. Narrow it down with the **Projects**, **Environments**, and **Tags** filters, or leave them empty to receive everything. 5. Choose the **Change events**, which are all on by default, and tick the alert kinds whose **Alert events** you want, which are all off by default. 6. Save. The webhook's page shows its **Signing secret**: copy it into your receiver's configuration. 7. Click **Send test event**. A `webhook.test` webhook event is sent right away and shows under **Deliveries**, with your receiver's response. A webhook event reaches a webhook only when every filter lets it through. An empty filter lets everything through.
Filter Lets a webhook event through when Also lets through
Projects it belongs to one of the projects nothing else: alert webhook events belong to no project, so a webhook filtered to projects receives no alert webhook events
Environments it belongs to one of the environments every webhook event that belongs to no environment: config settings, segments (except a change to one environment's override), projects, and alerts
Tags it is about a config that has one of the tags every webhook event that is not about a config: segments, environments, projects, and alerts
## Verifying requests Every request is signed with the webhook's signing secret, following the [Standard Webhooks](https://www.standardwebhooks.com/) specification. Check the signature before you trust a request: anyone who finds your URL can send it a `POST`. Each request carries these headers:
Header Value
webhook-id The webhook event's id .
webhook-timestamp When this attempt was sent, in seconds since the Unix epoch. Every attempt, including retries and resends, gets a fresh one.
webhook-signature v1, followed by the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body} . The HMAC key is the signing secret's text after whsec_ , decoded from base64. While a rotated secret still signs, the header holds two signatures separated by a space.
content-type application/json
user-agent ConfigDirector-Webhooks/1
Your custom headers come with every request too. Standard Webhooks publishes a verification library for each of these languages. Each language below links to its library; install the one for your receiver:
Language Install
Node.js and TypeScript npm install standardwebhooks
Python pip install standardwebhooks
Go go get github.com/standard-webhooks/standard-webhooks/libraries/go
Java and Kotlin implementation("com.standardwebhooks:standardwebhooks:1.2.0")
C# dotnet add package StandardWebhooks.StandardWebhooks
Ruby bundle add standardwebhooks
PHP composer require standard-webhooks/standard-webhooks
Rust cargo add standardwebhooks
On NuGet, the package named plain `StandardWebhooks` is not the Standard Webhooks library; install `StandardWebhooks.StandardWebhooks`. Each receiver below reads the signing secret from the `CONFIGDIRECTOR_WEBHOOK_SECRET` environment variable, answers `400` to a request that fails verification, and `204` once it has the webhook event. Pass the secret exactly as the dashboard shows it, `whsec_` included. ```js [Node.js] import express from "express"; import { Webhook } from "standardwebhooks"; const webhook = new Webhook(process.env.CONFIGDIRECTOR_WEBHOOK_SECRET); const app = express(); app.post("/configdirector-webhooks", express.raw({ type: "application/json" }), (req, res) => { let webhookEvent; try { webhookEvent = webhook.verify(req.body, req.headers); } catch { return res.sendStatus(400); } console.log(`Received ${webhookEvent.type} ${webhookEvent.id}`); res.sendStatus(204); }); app.listen(8080); ``` ```python [Python] import os from flask import Flask, request from standardwebhooks import Webhook, WebhookVerificationError webhook = Webhook(os.environ["CONFIGDIRECTOR_WEBHOOK_SECRET"]) app = Flask(__name__) @app.post("/configdirector-webhooks") def receive_webhook_event(): try: webhook_event = webhook.verify(request.get_data(), request.headers) except WebhookVerificationError: return "", 400 print(f"Received {webhook_event['type']} {webhook_event['id']}") return "", 204 ``` ```go [Go] package main import ( "encoding/json" "io" "log" "net/http" "os" standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go" ) type WebhookEvent struct { ID string `json:"id"` Type string `json:"type"` } func main() { webhook, err := standardwebhooks.NewWebhook(os.Getenv("CONFIGDIRECTOR_WEBHOOK_SECRET")) if err != nil { log.Fatal(err) } http.HandleFunc("POST /configdirector-webhooks", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil || webhook.Verify(body, r.Header) != nil { w.WriteHeader(http.StatusBadRequest) return } var webhookEvent WebhookEvent if err := json.Unmarshal(body, &webhookEvent); err != nil { w.WriteHeader(http.StatusBadRequest) return } log.Printf("Received %s %s", webhookEvent.Type, webhookEvent.ID) w.WriteHeader(http.StatusNoContent) }) log.Fatal(http.ListenAndServe(":8080", nil)) } ``` ```java [Java] import java.util.Map; import com.standardwebhooks.Webhook; import com.standardwebhooks.exceptions.WebhookVerificationException; import org.springframework.boot.json.JsonParserFactory; import org.springframework.http.ResponseEntity; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class ConfigDirectorWebhookController { private final Webhook webhook; public ConfigDirectorWebhookController() throws Exception { webhook = new Webhook(System.getenv("CONFIGDIRECTOR_WEBHOOK_SECRET")); } @PostMapping("/configdirector-webhooks") public ResponseEntity receive(@RequestBody String body, @RequestHeader MultiValueMap headers) { try { webhook.verify(body, headers); } catch (WebhookVerificationException e) { return ResponseEntity.badRequest().build(); } Map webhookEvent = JsonParserFactory.getJsonParser().parseMap(body); System.out.println("Received " + webhookEvent.get("type") + " " + webhookEvent.get("id")); return ResponseEntity.noContent().build(); } } ``` ```csharp [C#] using System.Net; using System.Text.Json; using StandardWebhooks; using StandardWebhooks.Exceptions; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); var webhook = new Webhook(builder.Configuration["CONFIGDIRECTOR_WEBHOOK_SECRET"]!); app.MapPost("/configdirector-webhooks", async (HttpRequest request) => { using var reader = new StreamReader(request.Body); var body = await reader.ReadToEndAsync(); var headers = new WebHeaderCollection(); foreach (var name in new[] { "webhook-id", "webhook-timestamp", "webhook-signature" }) { headers.Add(name, request.Headers[name]); } try { webhook.Verify(body, headers); } catch (WebhookVerificationException) { return Results.BadRequest(); } var webhookEvent = JsonDocument.Parse(body).RootElement; app.Logger.LogInformation("Received {Type} {Id}", webhookEvent.GetProperty("type"), webhookEvent.GetProperty("id")); return Results.NoContent(); }); app.Run("http://0.0.0.0:8080"); ``` ```ruby [Ruby] # config/routes.rb: post "/configdirector-webhooks", to: "config_director_webhooks#create" class ConfigDirectorWebhooksController < ActionController::API WEBHOOK = StandardWebhooks::Webhook.new(ENV.fetch("CONFIGDIRECTOR_WEBHOOK_SECRET")) def create webhook_event = WEBHOOK.verify(request.raw_post, { "webhook-id" => request.headers["webhook-id"], "webhook-timestamp" => request.headers["webhook-timestamp"], "webhook-signature" => request.headers["webhook-signature"], }) Rails.logger.info("Received #{webhook_event["type"]} #{webhook_event["id"]}") head :no_content rescue StandardWebhooks::WebhookVerificationError head :bad_request end end ``` ```php [PHP] verify(file_get_contents('php://input'), $headers); } catch (WebhookVerificationException $e) { http_response_code(400); exit; } error_log("Received {$webhookEvent['type']} {$webhookEvent['id']}"); http_response_code(204); ``` ```rust [Rust] use std::sync::Arc; use axum::{body::Bytes, http::{HeaderMap, StatusCode}, routing::post, Router}; use serde::Deserialize; use standardwebhooks::Webhook; #[derive(Deserialize)] struct WebhookEvent { id: String, r#type: String, } #[tokio::main] async fn main() { let secret = std::env::var("CONFIGDIRECTOR_WEBHOOK_SECRET").expect("CONFIGDIRECTOR_WEBHOOK_SECRET is not set"); let webhook = Arc::new(Webhook::new(&secret).expect("the webhook secret is not valid")); let app = Router::new().route( "/configdirector-webhooks", post(move |headers: HeaderMap, body: Bytes| async move { if webhook.verify(&body, &headers).is_err() { return StatusCode::BAD_REQUEST; } let Ok(webhook_event) = serde_json::from_slice::(&body) else { return StatusCode::BAD_REQUEST; }; println!("Received {} {}", webhook_event.r#type, webhook_event.id); StatusCode::NO_CONTENT }), ); let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); axum::serve(listener, app).await.unwrap(); } ``` Kotlin uses the Java library the same way. A few rules keep verification working: - **Verify the body exactly as it arrived.** The signature covers the raw bytes, so a body your framework has parsed and serialized again no longer matches. Each receiver above reads the raw body for that reason. - **Keep your server's clock right.** The libraries refuse a request whose `webhook-timestamp` is more than five minutes away from your clock, so a request someone recorded cannot be replayed later. - **Drop duplicates by webhook-id.** The same webhook event can arrive more than once, for example when your receiver handled a request but answered too late, or when someone clicks **Resend**. Keep the ids you have handled for 30 days, the time a delivery stays resendable, and skip any you have seen. - **Answer fast.** Any `2xx` within 10 seconds counts as delivered. Do slow work after answering, for example from a queue. ### Rotating the signing secret Click **Rotate secret** on the webhook's page if the secret may have leaked. The new secret starts signing right away, and the previous one keeps signing every request too for the next 24 hours, so update your receiver at any point in that window without a failed verification. The page shows when the previous secret stops. Rotating again within those 24 hours ends the oldest secret at once. ## Webhook event types
Type Sent when data.object data.previousAttributes can hold
config.created A config is created. Its first targeting rules send nothing of their own. The config as the Admin API lists it, without targeting rules
config.updated A config's settings change, or it gets a new key Same description , role , lifetime , type , typeOptions , variations , client , server , tags , key
config.archived A config is archived The config as it was archived
config.restored An archived config is restored The config as restored
targeting_rules.updated The targeting rules of one environment change That environment's targeting rules: id , environment , rules , defaultValue , defaultValueId , and defaultVariationId on an experiment rules , defaultValue , defaultValueId , defaultVariationId
segment.created , segment.updated , segment.deleted A segment is created, changed, or deleted The segment as the Admin API returns it, without usages key , name , kind , groups , or overrides when one environment's override changed
environment.created , environment.updated , environment.deleted An environment is created, changed, or deleted The environment as the Admin API returns it name , slug , color , live , deletionProtection
project.created , project.updated , project.deleted A project is created, changed, or deleted The project as the Admin API returns it, without its environments name , slug , storeCodeSnippets , deletionProtection
alert.created , alert.resolved An alert of a kind the webhook subscribes to is raised or resolved The alert: id , kind , priority , facts , detectedAt , createdAt , resolvedAt , and url , its page in the dashboard
webhook.test Someone sends a test webhook event None: data.sample holds a sample targeting_rules.updated webhook event
A change sends a webhook event for what the person did, not one for every entity it touched. Deleting a project sends `project.deleted` only, with nothing for its environments, configs, or segments, and deleting an environment sends `environment.deleted` only. Creating a project sends `project.created` and an `environment.created` for each of the two environments it starts with. The samples below were sent by ConfigDirector, with ids shortened and names changed. ### Configs A change of key is a `config.updated` whose `previousAttributes` holds the old `key`, while the envelope's `config.key` and `data.object.key` are the new one. Changing the key and other settings in one request sends two `config.updated`: one for the key and one for the other settings. Deleting a tag sends a `config.updated` for every config that had it, with the previous `tags`. ```json [config.created] { "id": "01a18500…2b93", "data": { "object": { "id": "01a18500…0339", "key": "checkout.new-flow", "role": "flag", "tags": [ { "id": "01a18500…436d", "name": "checkout" } ], "type": "boolean", "state": "new", "client": true, "server": true, "lifetime": "temporary", "createdAt": "2026-10-29T00:12:27.344Z", "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:12:27.344Z", "variations": [], "description": "The redesigned checkout flow", "deprecatedKeys": [] } }, "type": "config.created", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "config": { "id": "01a18500…0339", "key": "checkout.new-flow" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:12:27.344Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [config.updated] { "id": "01a18500…00a8", "data": { "object": { "id": "01a18500…0339", "key": "checkout.new-flow", "role": "flag", "tags": [ { "id": "01a18500…436d", "name": "checkout" } ], "type": "boolean", "state": "new", "client": false, "server": true, "lifetime": "temporary", "createdAt": "2026-10-29T00:12:27.344Z", "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:12:35.960Z", "variations": [], "description": "The redesigned one-page checkout", "deprecatedKeys": [] }, "previousAttributes": { "client": true, "description": "The redesigned checkout flow" } }, "type": "config.updated", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "config": { "id": "01a18500…0339", "key": "checkout.new-flow" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:12:35.970Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [config.updated (new key)] { "id": "01a18500…1547", "data": { "object": { "id": "01a18500…0339", "key": "checkout.one-page", "role": "flag", "tags": [ { "id": "01a18500…436d", "name": "checkout" } ], "type": "boolean", "state": "new", "client": false, "server": true, "lifetime": "temporary", "createdAt": "2026-10-29T00:12:27.344Z", "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:12:35.960Z", "variations": [], "description": "The redesigned one-page checkout", "deprecatedKeys": [] }, "previousAttributes": { "key": "checkout.new-flow" } }, "type": "config.updated", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "config": { "id": "01a18500…0339", "key": "checkout.one-page" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:12:39.270Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [config.archived] { "id": "01a18500…4233", "data": { "object": { "id": "01a18500…0339", "key": "checkout.one-page", "role": "flag", "tags": [ { "id": "01a18500…436d", "name": "checkout" } ], "type": "boolean", "state": "new", "client": false, "server": true, "lifetime": "temporary", "createdAt": "2026-10-29T00:12:27.344Z", "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:12:35.960Z", "variations": [], "description": "The redesigned one-page checkout", "deprecatedKeys": [] } }, "type": "config.archived", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "config": { "id": "01a18500…0339", "key": "checkout.one-page" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:13:09.320Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [config.restored] { "id": "01a1850c…f9b6", "data": { "object": { "id": "01a18509…3b15", "key": "checkout.express-pay", "role": "flag", "tags": [ { "id": "01a18500…436d", "name": "checkout" } ], "type": "boolean", "state": "new", "client": true, "server": true, "lifetime": "temporary", "createdAt": "2026-10-29T00:22:08.880Z", "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:25:57.036Z", "variations": [], "description": "One-tap payment in checkout", "deprecatedKeys": [] } }, "type": "config.restored", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "sourceClient": "dashboard" }, "config": { "id": "01a18509…3b15", "key": "checkout.express-pay" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:25:57.036Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ### Targeting rules The sample at the [top of this page](#what-a-webhook-event-looks-like) is a `targeting_rules.updated`. Its `data.object` is shaped like one entry of `targets` in `GET /projects/{projectId}/configs/{configKey}`: each rule's `value` has the config's type, while `defaultValue`, the default targeting rule value, is a string. ### Segments A change to one environment's override of a segment is a `segment.updated` that names that `environment` and holds the previous `overrides`. ```json [segment.created] { "id": "01a18500…8fd5", "data": { "object": { "id": "01a18500…aca7", "key": "beta-testers", "kind": "rule-based", "name": "Beta testers", "groups": [ [ { "id": "93f47530…8c54", "kind": "attribute", "trait": "/plan", "operator": "equals", "attribute": "traits", "targetType": "text", "targetValues": [ "beta" ] } ] ], "createdAt": "2026-10-29T00:13:16.529Z", "overrides": {}, "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:13:16.529Z" } }, "type": "segment.created", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:13:16.529Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [segment.updated] { "id": "01a18501…92f3", "data": { "object": { "id": "01a18500…aca7", "key": "beta-testers", "kind": "rule-based", "name": "Beta testers", "groups": [ [ { "id": "93f47530…8c54", "kind": "attribute", "trait": "/plan", "operator": "is one of", "attribute": "traits", "targetType": "text", "targetValues": [ "beta", "early-access" ] } ] ], "createdAt": "2026-10-29T00:13:16.529Z", "overrides": {}, "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:13:23.392Z" }, "previousAttributes": { "groups": [ [ { "id": "93f47530…8c54", "kind": "attribute", "trait": "/plan", "operator": "equals", "attribute": "traits", "targetType": "text", "targetValues": [ "beta" ] } ] ] } }, "type": "segment.updated", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:13:23.375Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [segment.deleted] { "id": "01a18501…a894", "data": { "object": { "id": "01a18500…aca7", "key": "beta-testers", "kind": "rule-based", "name": "Beta testers", "groups": [ [ { "id": "93f47530…8c54", "kind": "attribute", "trait": "/plan", "operator": "is one of", "attribute": "traits", "targetType": "text", "targetValues": [ "beta", "early-access" ] } ] ], "createdAt": "2026-10-29T00:13:16.529Z", "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:13:23.392Z" } }, "type": "segment.deleted", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:13:25.632Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ### Environments ```json [environment.created] { "id": "01a18500…b284", "data": { "object": { "id": "01a18500…4e5e", "live": false, "name": "Staging", "slug": "staging", "color": "yellow", "deletionProtection": false, "createdAt": "2026-10-29T00:12:13.940Z", "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:12:13.940Z" } }, "type": "environment.created", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:12:13.940Z", "environment": { "id": "01a18500…4e5e", "live": false, "name": "Staging", "slug": "staging" }, "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [environment.updated] { "id": "01a18500…1b7f", "data": { "object": { "id": "01a18500…4e5e", "live": true, "name": "Staging", "slug": "staging", "color": "teal", "deletionProtection": false, "createdAt": "2026-10-29T00:12:13.940Z", "projectId": "01a184ff…5f07", "updatedAt": "2026-10-29T00:12:19.979Z" }, "previousAttributes": { "live": false, "color": "yellow" } }, "type": "environment.updated", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:12:19.994Z", "environment": { "id": "01a18500…4e5e", "live": true, "name": "Staging", "slug": "staging" }, "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [environment.deleted] { "id": "01a18501…3eee", "data": { "object": { "id": "01a18500…4e5e", "live": true, "name": "Staging", "slug": "staging", "color": "teal", "deletionProtection": false, "projectId": "01a184ff…5f07" } }, "type": "environment.deleted", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:13:37.099Z", "environment": { "id": "01a18500…4e5e", "live": true, "name": "Staging", "slug": "staging" }, "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ### Projects ```json [project.created] { "id": "01a184ff…258b", "data": { "object": { "id": "01a184ff…5f07", "name": "Storefront", "slug": "storefront", "createdAt": "2026-10-29T00:11:51.108Z", "updatedAt": "2026-10-29T00:11:51.108Z", "organizationId": "01a01632…6040", "storeCodeSnippets": true, "deletionProtection": false } }, "type": "project.created", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront", "slug": "storefront" }, "createdAt": "2026-10-29T00:11:51.108Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [project.updated] { "id": "01a184ff…0e23", "data": { "object": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront", "createdAt": "2026-10-29T00:11:51.108Z", "updatedAt": "2026-10-29T00:12:08.449Z", "organizationId": "01a01632…6040", "storeCodeSnippets": true, "deletionProtection": false }, "previousAttributes": { "name": "Storefront" } }, "type": "project.updated", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:12:08.445Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [project.deleted] { "id": "01a1850c…22a5", "data": { "object": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront", "organizationId": "01a01632…6040", "storeCodeSnippets": true, "deletionProtection": false } }, "type": "project.deleted", "actor": { "member": { "id": "019da45e…ec57", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "01a184fc…d78b", "name": "ci" }, "sourceClient": "api" }, "project": { "id": "01a184ff…5f07", "name": "Storefront Web", "slug": "storefront" }, "createdAt": "2026-10-29T00:26:16.261Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ### Alerts An alert webhook event carries no sentence about the alert. Write your own from `kind` and `facts`, which holds the alert's details and differs by kind; [Alerts](https://docs.configdirector.com/monitoring/alerts#alert-kinds) describes every kind. Alerts are raised by checks that run every 30 minutes or every hour, depending on the kind, so `alert.created` can arrive that long after the problem starts. Many alerts resolve the moment you fix their cause, for example when you create a config that was not found, and `alert.resolved` follows within seconds. `alert.created` and `alert.resolved` for the same alert have different `id`s, because they are different webhook events. Match them by `data.object.id`, the alert's id. ```json [alert.created] { "id": "01a18512…dac1", "data": { "object": { "id": "01a18512…dac1", "url": "https://app.configdirector.com/alerts/01a18512…dac1?organization=01a01632…6040", "kind": "config-not-found", "facts": { "sdks": [ { "name": "js-server-sdk", "count": 5, "version": "1.12.0" } ], "sdkType": "server", "sdkKeyId": "01a01632…372d", "configKey": "webhook-docs.missing-flag", "projectId": "01a01632…179b", "eventCount": 5, "lastSeenAt": "2026-10-29T00:11:23.322Z", "sdkKeyName": "sk_test_4e66••••66eb", "projectName": "Acme App", "quietWindow": { "hours": 24 }, "environmentId": "01a01632…7ba2", "countingWindow": { "hours": 24 }, "environmentName": "Test" }, "priority": "high", "createdAt": "2026-10-29T00:30:46.735Z", "detectedAt": "2026-10-29T00:30:46.735Z", "resolvedAt": null } }, "type": "alert.created", "alertKind": "config-not-found", "createdAt": "2026-10-29T00:30:46.735Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ```json [alert.resolved] { "id": "d88613ba…a0a4", "data": { "object": { "id": "01a18512…dac1", "url": "https://app.configdirector.com/alerts/01a18512…dac1?organization=01a01632…6040", "kind": "config-not-found", "facts": { "sdks": [ { "name": "js-server-sdk", "count": 5, "version": "1.12.0" } ], "sdkType": "server", "sdkKeyId": "01a01632…372d", "configKey": "webhook-docs.missing-flag", "projectId": "01a01632…179b", "eventCount": 5, "lastSeenAt": "2026-10-29T00:11:23.322Z", "sdkKeyName": "sk_test_4e66••••66eb", "projectName": "Acme App", "quietWindow": { "hours": 24 }, "environmentId": "01a01632…7ba2", "countingWindow": { "hours": 24 }, "environmentName": "Test" }, "priority": "high", "createdAt": "2026-10-29T00:30:46.735Z", "detectedAt": "2026-10-29T00:30:46.735Z", "resolvedAt": "2026-10-29T00:32:45.281Z" } }, "type": "alert.resolved", "alertKind": "config-not-found", "createdAt": "2026-10-29T00:32:45.281Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ### Test ```json { "id": "0e168f29…66c3", "data": { "sample": { "id": "019a2d5e…6a7b", "data": { "object": { "id": "019a2d5e…9e06", "rules": [ { "id": "019a2d5e…9e07", "type": "conditional", "order": 0, "value": "false", "target": "value", "valueId": "0c5nBf…", "conditions": [ { "id": "019a2d5e…9e08", "kind": "attribute", "operator": "is one of", "attribute": "identifier", "targetType": "text", "targetValues": [ "user-101", "user-202" ] } ], "percentages": [] } ], "environment": { "id": "019a2d5e…9e02", "live": true, "name": "Production", "slug": "production", "color": "red" }, "defaultValue": "true", "defaultValueId": "4hR8sK…" }, "previousAttributes": { "defaultValue": "false" } }, "type": "targeting_rules.updated", "actor": { "member": { "id": "019a2d5e…9e04", "name": "Alice Example", "email": "alice@example.com" }, "apiToken": { "id": "019a2d5e…9e05", "name": "ci" }, "sourceClient": "terraform" }, "config": { "id": "019a2d5e…9e03", "key": "checkout.new-flow" }, "project": { "id": "019a2d5e…9e01", "name": "Storefront", "slug": "storefront" }, "createdAt": "2026-10-27T14:03:12.481Z", "environment": { "id": "019a2d5e…9e02", "live": true, "name": "Production", "slug": "production" }, "organization": { "id": "01a01632…6040", "name": "Acme" } } }, "type": "webhook.test", "createdAt": "2026-10-29T00:11:34.020Z", "organization": { "id": "01a01632…6040", "name": "Acme" } } ``` ## Delivery A webhook event is normally delivered within a few seconds of the change. During maintenance on our side it can take longer, up to 30 minutes, while webhook events wait to be sent. Each delivery is one webhook event sent to one webhook. It succeeds when your receiver answers with any `2xx` status within 10 seconds. Anything else is a failed attempt: another status, a redirect (redirects are not followed), no answer within 10 seconds, a connection error, or a URL whose host now resolves to a private address. After a failed attempt the delivery is tried again after about 1 minute, 5 minutes, 30 minutes, 2 hours, and 8 hours, each up to 10% later, so six attempts over about ten and a half hours. If the last one fails too, the delivery is **failed**. - **At least once.** A webhook event can arrive more than once. [Drop duplicates by `webhook-id`](#verifying-requests). - **In no particular order.** A retried delivery can arrive after newer ones, and a failing delivery never holds back the ones after it. Order webhook events by `createdAt` when order matters. The webhook's page lists its **Deliveries** from the last 30 days, newest first, with **Failures only** to show just the failed ones. Each delivery shows its webhook event type, its status, the number of attempts, and the latest attempt's result, and expands to the webhook event that was sent and the first 1 KB of your receiver's response or the connection error. A delivery is **pending** while it waits for an attempt, **succeeded**, **failed**, or **abandoned** when the webhook was disabled before it could succeed. **Resend** sends a delivery's webhook event again right away, with the same `webhook-id` and body, and a fresh timestamp and signature. If that attempt fails, the delivery is marked failed and is not tried again on its own, even if it was still waiting for a retry. A disabled webhook cannot resend or send test webhook events until it is enabled again. ## When a webhook is disabled An owner or admin can disable a webhook from its page. ConfigDirector also disables a webhook on its own when deliveries to it were attempted on each of the last three days and none of them succeeded. Then it: - abandons the deliveries still waiting for an attempt, - marks the webhook **Disabled** in the list, saying it was disabled after repeated failures, - raises the **Webhook disabled after repeated failures** alert, which is Critical, goes to owners and admins, and is emailed right away unless they turned that off in their email preferences. While a webhook is disabled, nothing is queued for it, and enabling it again does not send what happened in between. Fix the receiver, then click **Enable** on the webhook's page; the alert resolves when the webhook is enabled again or deleted. ## URL rules and custom headers - The URL must start with `https://` and be at most 2,048 characters long. - Its host must resolve to a public address. A host that cannot be found, or that resolves to a private, loopback, or link-local address, is refused when you save, and the address is checked again before every attempt. To try webhooks against a receiver on your own machine, expose it through a tunnel that gives it a public `https://` URL. - Requests do not come from a fixed set of IP addresses, so rely on the signature, not the sender's address. A webhook can send up to 5 custom headers with every request, for example an `Authorization` header your gateway checks. Each name can be used once and follows the HTTP rules for header names. A value is at most 1,024 characters of printable Latin-1 text with no line breaks. The headers ConfigDirector sets itself (`content-type`, `content-length`, `host`, `user-agent`, `webhook-id`, `webhook-signature`, and `webhook-timestamp`) cannot be custom headers. Header values are stored encrypted and never shown again; when you edit a webhook, a value you leave blank keeps the saved one. ## Limits
Limit Value
Webhooks per organization 10
Custom headers per webhook 5, each value up to 1,024 characters
Response time 10 seconds
Delivery history 30 days, after which deliveries are deleted and can no longer be resent
Webhook event size 256 KB
A change webhook event larger than 256 KB is sent with `data` replaced by `{ "truncated": true, "url": "..." }`, where `url` opens the config, segment, or project in the dashboard; read the full entity from the Admin API. Alert webhook events are always sent whole. ## Managing webhooks with the Admin API Everything on the Webhooks page is also available through the [Admin API](https://docs.configdirector.com/integrations/admin-api#webhooks): listing, creating, updating, and deleting webhooks, rotating the secret, sending a test webhook event, listing deliveries, and resending one. The API token needs the `webhooks:manage` permission, which only owners and admins can grant. To declare webhooks as code, use the `configdirector_webhook` resource of the [Terraform provider](https://docs.configdirector.com/integrations/terraform#webhooks), which also works with OpenTofu. # Migrating from Firebase Remote Config > Move your remote configuration from Firebase Remote Config to ConfigDirector. How parameters, conditions, in-app defaults, and fetch and activate map, how to recreate the parameters, and how to switch the Flutter, Android, iOS, and web SDKs over. This guide is for apps that read their remote configuration from Firebase Remote Config and want to move it to ConfigDirector. It maps the Remote Config concepts onto ConfigDirector's, walks through recreating the parameters as configs, and shows how to switch each SDK over so that both services run side by side until you remove Remote Config. If you are new to ConfigDirector, read [Core Concepts](https://docs.configdirector.com/getting-started/core-concepts) first. It takes a few minutes and makes the mapping below easier to follow. ## Before you start 1. **Export the Remote Config template.** The Firebase console, the Firebase CLI (`firebase remoteconfig:get -o remoteconfig.template.json`), and the REST API all export the template as JSON, with every parameter, its default and conditional values, its value type, and the project's conditions. That file is the list you work from. See [Firebase's template documentation](https://firebase.google.com/docs/remote-config/templates). 2. **Decide which parameters not to move.** A migration is the best moment to clean up. A parameter that has had the same value everywhere since it was added, or that no shipped version of the app still reads, has nothing left to decide. Hardcode that value instead of recreating the parameter. 3. **Create your ConfigDirector organization and project.** [Sign up](https://app.configdirector.com/) and create one project for the app. If you kept one Firebase project per stage, such as `my-app-dev` and `my-app-prod`, they become environments of this one project: `Test` and `Production` are created for you, and you can add more. Mark the environments that serve real traffic as live. ## How the concepts map ### Parameters and values
Firebase Remote Config ConfigDirector
Firebase project, one per stage One project with an environment per stage. Each environment has its own SDK keys and its own targeting rules for every config.
Parameter Config . A boolean parameter that gates a feature is a config with the Feature flag role if it is temporary, or Kill switch if it stays; everything else is a Config .
Value type: string, boolean, number, JSON Config type : String , Boolean , Integer or Float , JSON . ConfigDirector also has Enum , URL , and Timespan , which validate the value as that type. A string parameter with a fixed set of values is an Enum ; a string holding a URL is a URL ; a number of milliseconds or seconds is a Timespan .
Default value The default targeting rule value : the value of the last rule in each environment, served when no other rule matches.
Conditional value A conditional rule above the default targeting rule, with the condition and the value it serves. Rules are checked in order; the first that matches serves its value.
Condition, named and shared across parameters Conditions are written on each rule. A condition you reuse across many configs becomes a segment , defined once per project and used by a rule as "the context is in the segment".
Parameter group Tags
Description Description
Parameter key Config key . Firebase keys made of letters, digits, and underscores are valid ConfigDirector keys, so keep them: the code change is then only the SDK call. A key can be renamed safely later, even in production.
In-app default values, from XML, plist, JSON, or defaultConfig The in-code default value , passed with every read. It is returned when the client has no values for the context yet, and when the config is missing or has the wrong type.
Template versions, diff, and rollback The config's audit log, with who changed what, from where, and the before and after values. There is no one-click rollback: restore a value by setting it again.
Template JSON, exported and published through the CLI or REST The Terraform provider , the MCP server , and the Admin API. Configs and targeting rules are managed as code with the same validations as the dashboard.
Value limits: 3,000 parameters, 1,000,000 characters 100 configs on Free, unlimited on Pay as you go. 16 KB per value and 1 MB per project. See Pricing and billing .
### Conditions Remote Config evaluates values per app instance, from signals it collects itself. ConfigDirector evaluates values per **context**: the user, device, or other entity your app says it is evaluating for, with the traits your app gives it. Most Remote Config conditions become a condition on a trait that your app sets.
Remote Config condition ConfigDirector
App version, build number An appVersion condition, compared as a semantic version. The mobile SDKs read the app version from the platform when you do not set it; the browser SDK takes it from the metadata option.
App, platform An appName condition. Give each platform build its own name through the metadata option, such as my-app-ios and my-app-android , or use one config per platform.
Country or region, language, device category, OS A condition on a trait your app sets, such as /country , /language , or /platform . ConfigDirector does not collect these on its own; put the ones your rules use into the context's traits .
User property, Analytics audience, custom signal A condition on a trait, or a segment with the audience's definition. Traits can hold strings, numbers, booleans, arrays, and nested objects, and a condition reads a trait as text, a number, a semantic version, a datetime, an array, or a boolean.
User in random percentile A percentage rollout . Each context is assigned by its context kind and identifier, so a user keeps the same value on every device they sign in on, and across app restarts.
First open time A trait your app sets, such as /firstOpenedAt , compared as a datetime.
Date and time Not available. There are no scheduled changes; change the rule when the time comes, or apply the change from a scheduled job through the Admin API, the Terraform provider, or the MCP server.
Imported segment A segment with the same definition, or a condition group with identifier is one of the listed identifiers.
Remote Config combines the clauses of a condition with AND, and so does a ConfigDirector rule: every condition of a rule must match. Where Remote Config applies the first matching conditional value in the order the conditions are listed, ConfigDirector applies the first matching rule. ### Experiments and rollouts
Firebase ConfigDirector
A/B Testing experiment on a parameter A config with the Experiment role. It defines named variations and serves them to segments or by percentage. ConfigDirector does not measure the outcome: send the variation to your analytics, using the valueId every evaluation carries if you prefer not to send the value itself.
Rollout with Crashlytics and Analytics metrics A percentage rollout that you widen as you watch your own monitoring. Alerts tell you about problems with the config itself, such as a type mismatch or the in-code default value being served too often.
### Fetching values
Firebase Remote Config ConfigDirector
Fetch, then activate initialize once, when the app starts. There is no activate step: values are ready to read as soon as initialize completes.
Minimum fetch interval, 12 hours by default None. The SDK keeps a streaming connection open and receives every change within about a second of it being saved. Polling is available for environments where a persistent connection does not work. See Architecture .
Real-time config update listener, then activate watch a config, or subscribe to the stream or flow for its key. It emits the current value and then every change. Nothing has to be activated.
Values persist on the device across restarts Client SDKs keep the last values they received on the device and serve them on the next launch until live values arrive, with the evaluation reason cached . An app keeps one set per client SDK key, for the last context it connected with, so the first launch, or a launch with another context such as another signed-in user, reads the in-code default value until live values arrive; with a network they usually arrive in well under a second. initialize waits for live values for up to its timeout, 3 seconds by default, and the client keeps connecting in the background after a timeout.
Value source: static, default, remote The evaluation reason : found-match when a live value from ConfigDirector was served, cached when a value kept on the device from an earlier launch was, or a reason such as client-not-ready , config-state-missing , or type-mismatch when the in-code default value was. Every SDK exposes it, through a hook or an evaluation method.
Firebase installation as the evaluated entity The context : give it the user's identifier when they sign in, or leave the identifier out for an anonymous context, for which ConfigDirector derives an identifier that lasts until the app is uninstalled or the site's data is cleared. A context of another kind, such as a device , sets kind .
Server-side Remote Config, in the Admin SDK The server SDKs , with a server SDK key. They receive the targeting rules and evaluate every context locally.
Pricing: 100,000 fetches a day free, then per fetch A free allowance of 1,000 monthly active users, then per user. Each distinct user, device, app install, or browser your apps evaluate configs for counts once a month, however many values it reads or updates it receives. See Pricing and billing .
Do not move secrets. Remote Config documents that its values are not for sensitive data, and the same is true of ConfigDirector: values are delivered to the app and are not protected like a secrets vault. ## Recreate the parameters Create the configs in ConfigDirector while Remote Config keeps serving your app. Nothing changes for your users until the SDKs switch over. For each parameter in the exported template: 1. Create a config with the same key, the role and type from the tables above, and a lifetime: `Temporary` for a parameter you plan to remove once a feature is fully released, `Permanent` for one that stays. Set its initial value to the parameter's default value. 2. In each environment, recreate the conditional values as conditional rules, in the same order, with the parameter's default value as the default targeting rule value. A Firebase project per stage maps onto one environment each; a single Firebase project maps onto `Production`, and `Test` gets the values you want while developing. 3. Check a few contexts with the [targeting rules tester](https://docs.configdirector.com/getting-started/testing-targeting-rules). The result should be the value Remote Config serves them. There are three ways to do this: - **The dashboard**, for an app with a few parameters. The config form and the targeting rules editor cover everything above. - **The Terraform provider**, if you want the configuration as code from the start. The [ConfigDirector Terraform provider](https://docs.configdirector.com/integrations/terraform) has `configdirector_config` for the config and `configdirector_config_targeting_rules` for the rules of each environment. - **The MCP server**, for a template with many parameters. Connect the [MCP server](https://docs.configdirector.com/integrations/mcp) to your coding assistant, give it the exported template, and ask it to create the configs and their targeting rules. Review what it created in the dashboard before switching any SDK. Conditions come first. Create a segment for each Remote Config condition that several parameters share, so the rules that use it stay in one place. ## Switch the SDKs Switch one platform at a time. Pass the parameter's in-app default as the in-code default value, so that a config that is missing, or read before the client is ready, still gives the result users get now. ### Initialize the client Create one client when the app starts, with the client SDK key of the environment the build runs against, and initialize it. Where Remote Config needs `setDefaults`, `fetchAndActivate`, and a minimum fetch interval, ConfigDirector needs the client and one call: ```dart [Flutter] import 'package:configdirector_flutter_client_sdk/configdirector_flutter_client_sdk.dart'; final client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY'); await client.initialize(); ``` ```kotlin [Android] import com.configdirector.ConfigDirectorClient val client = ConfigDirectorClient(applicationContext, "YOUR-CLIENT-SDK-KEY") // A suspend function: call it from a coroutine. client.initialize() ``` ```swift [Swift] import ConfigDirector let client = try ConfigDirectorClient(clientSDKKey: "YOUR-CLIENT-SDK-KEY") await client.initialize() ``` ```ts [Web] import { createClient } from "@configdirector/client-sdk"; export const client = createClient("YOUR-CLIENT-SDK-KEY"); await client.initialize(); ``` `initialize` returns when the first values arrive or after its timeout. If your users are often on poor connections, do not await it: until live values arrive, reads return the values kept on the device from an earlier launch with the same context, or the in-code default value when there are none. Remote Config behaves the same way, with its last activated values and, before the first activate, its in-app defaults. ### Read a value Each read takes the key and the in-code default value, and the default decides the type the value is read as: ```dart [Flutter] // Before: remoteConfig.getBool('new_checkout') final newCheckout = client.getValue('new_checkout', false); // Before: remoteConfig.getInt('max_items') final maxItems = client.getValue('max_items', 20); ``` ```kotlin [Android] // Before: remoteConfig.getBoolean("new_checkout") val newCheckout = client.value("new_checkout", false) // Before: remoteConfig.getLong("max_items") val maxItems = client.value("max_items", 20) ``` ```swift [Swift] // Before: remoteConfig["new_checkout"].boolValue let newCheckout = client.value(for: "new_checkout", default: false) // Before: remoteConfig["max_items"].numberValue let maxItems = client.value(for: "max_items", default: 20) ``` ```ts [Web] // Before: getBoolean(remoteConfig, "new_checkout") const newCheckout = client.getValue("new_checkout", false); // Before: getNumber(remoteConfig, "max_items") const maxItems = client.getValue("max_items", 20); ``` A JSON parameter that you parsed from `getString` is read as JSON directly: pass an object or a map as the default and the SDK returns the parsed value. ### Follow changes Remote Config's real-time listener tells you a new template exists, and you call `activate` and re-read. ConfigDirector's `watch`, and the streams and flows of the mobile SDKs, give you the value itself, first the current one and then every change: ```dart [Flutter] // Before: remoteConfig.onConfigUpdated.listen((event) async { await remoteConfig.activate(); ... }) final subscription = client.watch('new_checkout', false).listen((newCheckout) { // Rebuild with the new value }); ``` ```kotlin [Android] // Before: remoteConfig.addOnConfigUpdateListener(...) then remoteConfig.activate() lifecycleScope.launch { client.values("new_checkout", false).collect { newCheckout -> // Update the UI with the new value } } ``` ```swift [Swift] // Before: remoteConfig.addOnConfigUpdateListener { ... remoteConfig.activate() } for await newCheckout in client.values(for: "new_checkout", default: false) { // Update the UI with the new value } ``` ```ts [Web] // Before: onConfigUpdate(remoteConfig, ...) then activate(remoteConfig) client.watch("new_checkout", false, (newCheckout) => { // Render with the new value }); ``` In Flutter, pair `watch` with a `StreamBuilder`; in Jetpack Compose, use `configValue` from the Compose artifact; in SwiftUI, read the stream in a `task`. Each SDK page shows the pattern for its framework. ### Give the context what your rules need Remote Config knew the country, language, and app version of each installation on its own. ConfigDirector knows the app name and version through the SDK's `metadata`, and everything else from the context your app provides. Set the traits your targeting rules use, and update the context when the user signs in or out: ```dart [Flutter] await client.initialize( const ConfigDirectorContext( id: '12345', traits: {'country': 'US', 'language': 'en', 'platform': 'ios'}, ), ); // On sign out: an anonymous context await client.updateContext(const ConfigDirectorContext()); ``` ```kotlin [Android] client.initialize( ConfigDirectorContext.build { id("12345") trait("country", "US") trait("language", "en") trait("platform", "android") }, ) // On sign out: an anonymous context client.updateContext(ConfigDirectorContext.build { anonymous(true) }) ``` ```swift [Swift] await client.initialize( context: ConfigDirectorContext( id: "12345", traits: ["country": "US", "language": "en", "platform": "ios"] ) ) // On sign out: an anonymous context await client.updateContext(ConfigDirectorContext(isAnonymous: true)) ``` ```ts [Web] await client.initialize({ id: "12345", traits: { country: "US", language: "en", platform: "web" }, }); // On sign out: an anonymous context await client.updateContext({ anonymous: true }); ``` A context given without an identifier is anonymous: it is never stored or shown in the dashboard, but the values are still evaluated for it, with an identifier ConfigDirector derives from the instance ID the SDK keeps until the app is uninstalled or the site's data is cleared. ### The SDK for each platform
Firebase Remote Config SDK ConfigDirector SDK
Flutter ( firebase_remote_config ) Flutter
Android Android , for Kotlin and Java, with a Jetpack Compose artifact
Apple platforms Swift
Web JavaScript , React , Vue , Next.js , Nuxt
React Native (community library) React Native
Server-side Remote Config Node.js , Python , Java , .NET , Go
Unity, C++ Not available
Every platform also has an [OpenFeature provider](https://docs.configdirector.com/sdks/openfeature), if you prefer to code against the vendor-neutral OpenFeature API. ## Verify, then remove Remote Config Once a build reads a config from ConfigDirector: - **Evaluation Activity** on the config shows the values being served in each environment. See [Evaluation Activity](https://docs.configdirector.com/monitoring/activity). - **Alerts** catch the mistakes a migration makes: a key the app asks for that does not exist, a config read with the wrong type, an in-code default value that drifts from the value served, and the default value served more often than expected. See [Alerts](https://docs.configdirector.com/monitoring/alerts). - For a value you rolled out by percentile, the assignment starts afresh: a user who was in Remote Config's 10% is not necessarily in ConfigDirector's. Switch over at a point in the rollout where that is acceptable, or at 0% or 100%. Keep Remote Config in the app until the versions that read it have faded from your user base, then remove the dependency and delete the parameters from the template. From then on, the [config lifecycle](https://docs.configdirector.com/monitoring/lifecycle) tells you when each temporary config has settled and can be removed from the code too. # Migrating from LaunchDarkly > Move your feature flags from LaunchDarkly to ConfigDirector. How projects, environments, flags, contexts, segments, and rollouts map, how to recreate the flags, and how to switch the SDKs over without a risky release. This guide is for teams that run feature flags on LaunchDarkly and want to move them to ConfigDirector. It maps the LaunchDarkly concepts onto ConfigDirector's, walks through recreating the flags, and shows how to switch each SDK over so that both services run side by side until you remove the LaunchDarkly one. If you are new to ConfigDirector, read [Core Concepts](https://docs.configdirector.com/getting-started/core-concepts) first. It takes a few minutes and makes the mapping below easier to follow. ## Before you start 1. **Pick one LaunchDarkly project to move first.** A project with a handful of flags and one or two services is a good rehearsal. Everything below is done per project. 2. **Make a list of the flags, with their variations and targeting.** LaunchDarkly has no flag export, but its REST API lists every flag in a project with its variations and a summary of the targeting in each environment: `GET /api/v2/flags/{projectKey}`, with `?env=production&summary=0` for the full rules of that environment, and `GET /api/v2/flag-statuses/{projectKey}/{environmentKey}` for each flag's status. See the [LaunchDarkly API documentation](https://launchdarkly.com/docs/api/feature-flags/get-feature-flags). For a small project, the flag list in the LaunchDarkly dashboard is enough. 3. **Decide which flags not to move.** A migration is the best moment to clean up. A flag whose status is *launched* or *inactive* in every environment serves one value and has nothing left to decide. Hardcode that value in the code instead of recreating the flag. Only *active* flags, and the permanent ones you still rely on, need to move. 4. **Create your ConfigDirector organization and project.** [Sign up](https://app.configdirector.com/), create a project, and add an environment for each LaunchDarkly environment you use. `Test` and `Production` are created for you. Mark the environments that serve real traffic as live. ## How the concepts map ### Projects, environments, and flags
LaunchDarkly ConfigDirector
Project Project
Environment Environment . A critical environment in LaunchDarkly is a live environment in ConfigDirector.
Feature flag Config , with a role : Feature flag for a temporary boolean, Kill switch for a permanent boolean, Experiment for variations you compare, and Config for everything else.
Flag type: boolean, string, number, JSON Config type : Boolean , String , Integer or Float , JSON . ConfigDirector also has Enum , URL , and Timespan , which validate the value as that type. A string flag with a fixed set of variations is usually an Enum .
Variations The values a config serves. Experiments define them as named variations ; other roles enter the value in each targeting rule.
Temporary and permanent flags The role : a temporary boolean flag is a Feature flag and a permanent one a Kill switch , since the role decides the lifetime for every role but Config . Temporary configs are tracked through the config lifecycle , which tells you when they have settled and can be removed.
Flag key Config key . LaunchDarkly keys are valid ConfigDirector keys, so keep them: the code change is then only the SDK call. Unlike LaunchDarkly, a key can be renamed safely later, even in production.
Client-side SDK availability, mobile key availability SDK availability : client SDKs, server SDKs, or both. Client and mobile applications both use the client SDK key.
Tags Tags
JSON Schema on a flag's variations JSON Schema validation on a JSON config, on both pricing options. Changing the schema re-validates every value already set in every environment.
Flag maintainer, custom properties, flag links No equivalent. Put the owner in the description, or in a tag.
Archived flag Archived config. It can be restored for 30 days, and then it is purged.
### Targeting
LaunchDarkly ConfigDirector
Default rule (fallthrough) The default targeting rule , the last rule in every environment. Its value is served when no other rule matches.
Targeting on and off, with the off variation There is no on/off switch above the rules. To turn a flag off for everyone, remove or reorder its rules so the default targeting rule serves the off value. For a switch that stays independent of the rules, create a Kill switch and check it first in your code.
Individual targets A conditional rule with the condition identifier is one of the listed identifiers. Add a context.kind condition to the rule when the identifiers belong to a context kind other than user .
Rule with clauses A conditional rule with one attribute condition per clause. Every condition of a rule must match, as in LaunchDarkly. There is no OR inside a rule: write a second rule, or use a segment, whose condition groups are combined with OR.
Attribute operators: string, number, semver, date, regex Attribute conditions read a trait as text, a number, a semantic version, a datetime, an array, or a boolean, each with its own operators. There is no regular expression operator. Text conditions are case-sensitive.
Percentage rollout, bucketed by a chosen context kind and attribute A percentage rollout , on its own or as the value of a conditional rule. Contexts are assigned by their context kind and identifier. Choosing another attribute to bucket by is not available: evaluate with the entity you want to bucket by as the context instead, for example kind: "organization" .
Segment, rule-based, per environment A segment , defined once per project with one or more condition groups, used by any config in the project. An environment that needs its own definition gets an environment override .
Segment, list-based Not yet available. Use a condition group with identifier is one of the listed identifiers; it is kept in one place and reused the same way.
Prerequisites Not available. Put the condition the prerequisite flag expressed on the dependent flag's own rules, or check both flags in code.
Scheduled changes, approvals, guarded rollouts Not available. Changes take effect as soon as they are saved.
Test run The targeting rules tester , which evaluates saved or unsaved rules against a context and shows why each rule matched or not.
### Contexts
LaunchDarkly ConfigDirector
Context Context
Context kind: user , organization , device , custom Context kind . Leaving it out means user . Any lowercase name of up to 64 letters, digits, dashes, or underscores works.
Context key Identifier , id in the SDKs. A context is identified by its kind and identifier together, so user 42 and device 42 are different contexts.
Context name name
Custom attributes, such as email or plan Traits , an arbitrary JSON object. Targeting rules address a trait with a JSON pointer, such as /plan or /address/country .
anonymous: true An anonymous context : a context given without an identifier, or with anonymous set. It is evaluated but never stored or shown in the dashboard.
Multi-context, several kinds in one evaluation Not available. Each evaluation is for one context. To target a user by their organization, copy the organization's attributes you target by into the user's traits, for example /organization/plan . Or evaluate the config a second time with the organization as the context.
Private attributes Not available. Only put the traits your targeting rules use in the context.
### SDKs and delivery
LaunchDarkly ConfigDirector
SDK key Server SDK key , a secret, for server SDKs.
Client-side ID and mobile key Client SDK key , for browser and mobile SDKs. It is not secret, because client SDKs only receive evaluated values.
Streaming by default, with polling as an option The same. See Architecture .
Server SDKs evaluate locally The same. Server SDKs receive the rules and segments and evaluate every context in memory.
Client SDKs receive evaluated values The same.
Fallback value in code The in-code default value , passed with every read. It is returned only when the SDK cannot evaluate the config.
Flag change listeners watch in the JavaScript SDKs, a stream or flow in the mobile SDKs, and the configsUpdated hook. Each SDK page shows them.
Bootstrapping from localStorage or the server Client SDKs keep the last values they received on the device, in the browser's localStorage , and serve them on the next launch with the same context until live values arrive. Keeping them is on by default, with no bootstrap option to set, and the client is ready only once live values arrive. A server-rendered payload cannot be passed to the browser SDK: the Next.js and Nuxt SDKs evaluate configs during server-side rendering, so the first render carries real values.
Relay Proxy Not needed. Every SDK connects to ConfigDirector directly over streaming. A base url option is available for an HTTP proxy of your own.
Test data sources The test client in each SDK's testing entry point, and the in-memory providers of the OpenFeature SDKs. See Test your code on each SDK page.
Experimentation metrics and results Not available. An Experiment config serves the variations; measure them in your own analytics. Every evaluation carries a valueId , a stable identifier of the value served, that you can send instead of the value itself.
## Recreate the flags Create the configs in ConfigDirector while the LaunchDarkly flags keep serving your applications. Nothing changes for your users until the SDKs switch over. For each flag you decided to move: 1. Create a config with the same key, the role and type from the tables above, and the lifetime. Set its initial value to what LaunchDarkly serves most users in production today, so an application that reads it before its rules exist gets a safe value. 2. Set the **SDK availability** to match where the flag is read. 3. In each environment, recreate the targeting: individual targets as `identifier is one of` rules, rules as conditional rules, the rollout as a percentage rollout, and the fallthrough as the default targeting rule value. 4. Check a few contexts with the [targeting rules tester](https://docs.configdirector.com/getting-started/testing-targeting-rules). The result should be the value LaunchDarkly serves them. There are three ways to do this: - **The dashboard**, for a project with a few flags. The config form and the targeting rules editor cover everything above. - **The Terraform provider**, if your LaunchDarkly flags were managed with LaunchDarkly's Terraform provider. The [ConfigDirector Terraform provider](https://docs.configdirector.com/integrations/terraform) has `configdirector_config` for the flag and `configdirector_config_targeting_rules` for the rules of each environment, with the same structure as the tables above. Translate the resources once and apply. - **The MCP server**, for a project with many flags that you manage by hand. Connect the [MCP server](https://docs.configdirector.com/integrations/mcp) to your coding assistant, give it the flag list from the LaunchDarkly API, and ask it to create the configs and their targeting rules. Review what it created in the dashboard before switching any SDK. The `configdirector-migrate-from-launchdarkly` [agent skill](https://docs.configdirector.com/integrations/agents) holds your agent to this guide: it has you export the flags and segments in your own terminal, proposes which flags to move and which to hardcode, creates the segments and then the configs in batches you confirm, checks the rules with an evaluation, and afterwards switches an application's SDK calls over as the next section shows. Segments come first. Create the segments a project's rules use before the configs, and translate the LaunchDarkly segment of each environment into a project definition plus an environment override where the environments differ. ## Switch the SDKs Switch one application at a time. Pass the value LaunchDarkly serves today as the in-code default value, so that a config that is missing, or read before the client is ready, still gives the result users get now. ### Server SDKs A Node.js service reading a flag with the LaunchDarkly server SDK: ```ts [Before: LaunchDarkly] import * as LaunchDarkly from "@launchdarkly/node-server-sdk"; const ldClient = LaunchDarkly.init("YOUR-LAUNCHDARKLY-SDK-KEY"); await ldClient.waitForInitialization({ timeout: 10 }); const context = { kind: "user", key: "user-123", name: "Example User", email: "user@example.com", plan: "enterprise", }; const newCheckout = await ldClient.variation("new-checkout", context, false); ``` The same flag with the ConfigDirector [Node.js SDK](https://docs.configdirector.com/sdks/server/node-js). The context's custom attributes move under `traits`, and the key becomes `id`: ```ts [After: ConfigDirector] import { createClient } from "@configdirector/server-sdk"; const client = createClient("YOUR-SERVER-SDK-KEY"); await client.initialize(); const context = { id: "user-123", name: "Example User", traits: { email: "user@example.com", plan: "enterprise", }, }; const newCheckout = client.getValue("new-checkout", false, context); ``` A context of another kind sets `kind`: ```ts const organization = { id: "acme", kind: "organization", name: "Acme Inc.", traits: { plan: "enterprise" } }; const newCheckout = client.getValue("new-checkout", false, organization); ``` `getValue` is synchronous: the server SDK evaluates the rules it already holds in memory. The type of the value is the type of the in-code default value you pass, so a string flag is read with a string default and a JSON flag with an object. The [Python](https://docs.configdirector.com/sdks/server/python), [Java](https://docs.configdirector.com/sdks/server/java), [.NET](https://docs.configdirector.com/sdks/server/dotnet), and [Go](https://docs.configdirector.com/sdks/server/go) SDKs follow the same shape. ### Browser and mobile SDKs A web application with the LaunchDarkly JavaScript client SDK: ```ts [Before: LaunchDarkly] import * as LDClient from "launchdarkly-js-client-sdk"; const ldClient = LDClient.initialize("YOUR-CLIENT-SIDE-ID", { kind: "user", key: "user-123", name: "Example User", plan: "enterprise", }); await ldClient.waitForInitialization(5); const newCheckout = ldClient.variation("new-checkout", false); ldClient.on("change:new-checkout", (value) => render(value)); await ldClient.identify({ kind: "user", key: "user-456" }); ``` The same with the ConfigDirector [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript). The context is given to `initialize`, changes arrive through `watch`, and `identify` becomes `updateContext`: ```ts [After: ConfigDirector] import { createClient } from "@configdirector/client-sdk"; const client = createClient("YOUR-CLIENT-SDK-KEY"); await client.initialize({ id: "user-123", name: "Example User", traits: { plan: "enterprise" }, }); const newCheckout = client.getValue("new-checkout", false); client.watch("new-checkout", false, (value) => render(value)); await client.updateContext({ id: "user-456" }); ``` `initialize` resolves when the first values arrive or after its timeout, 3 seconds by default; after a timeout the client keeps connecting in the background and, until live values arrive, reads return the values kept on the device from an earlier launch with the same context, or the in-code default value when there are none. The [React](https://docs.configdirector.com/sdks/browser/react), [Vue](https://docs.configdirector.com/sdks/browser/vue), [React Native](https://docs.configdirector.com/sdks/mobile/react-native), [Flutter](https://docs.configdirector.com/sdks/mobile/flutter), [Swift](https://docs.configdirector.com/sdks/mobile/swift), and [Android](https://docs.configdirector.com/sdks/mobile/android) SDKs follow the same shape, with a hook, composable, stream, or flow in place of `watch`. ### Applications on OpenFeature If your code reads flags through OpenFeature with the LaunchDarkly provider, the code does not change: register the ConfigDirector provider instead. The `targetingKey` becomes the context's identifier. Attributes the ConfigDirector provider reads are `kind`, `name`, `traits`, and `anonymous`, so move the custom attributes your rules use into a `traits` attribute. See [OpenFeature with ConfigDirector](https://docs.configdirector.com/sdks/openfeature) for the providers and the full mapping. ### Which SDK replaces which
LaunchDarkly SDK ConfigDirector SDK
JavaScript client JavaScript
React Web React
Vue Vue
React Native React Native
Flutter Flutter
iOS Swift
Android Android
Node.js server, Python, Java, .NET server, Go Node.js , Python , Java , .NET , Go
Next.js integration Next.js . ConfigDirector also has a Nuxt SDK.
OpenFeature providers OpenFeature providers for the web, Swift, Android, Flutter, Node.js, Python, Java, .NET, and Go
ConfigDirector has no SDK for Ruby, PHP, Rust, C++, Erlang, Haskell, Lua, Roku, or Electron, and no edge SDKs. If you need one of them, write to [sdks@configdirector.com](mailto:sdks@configdirector.com) with the SDK you are looking for and we will let you know the projected date for its release. ## Verify, then remove LaunchDarkly Once an application reads a config from ConfigDirector: - **Evaluation Activity** on the config shows the values being served in each environment. See [Evaluation Activity](https://docs.configdirector.com/monitoring/activity). - **Alerts** catch the mistakes a migration makes: a key the application asks for that does not exist, a config read with the wrong type, an in-code default value that drifts from the value served, and the default value served more often than expected. See [Alerts](https://docs.configdirector.com/monitoring/alerts). - For a flag you rolled out by percentage, the same identifier gets the same value on every evaluation, as in LaunchDarkly. The split itself is assigned afresh, so a user who was in LaunchDarkly's 10% is not necessarily in ConfigDirector's. Switch the SDK over at a point in the rollout where that is acceptable, or at 0% or 100%. When every application has switched, remove the LaunchDarkly SDK and its keys from the code, and archive the flags in LaunchDarkly. From then on, the [config lifecycle](https://docs.configdirector.com/monitoring/lifecycle) tells you when each temporary config has settled and can be removed from the code too. ## Metering Both services bill client SDK usage in monthly active users. One difference matters when your apps evaluate more than one context kind: LaunchDarkly bills only the kind with the most monthly active users, while ConfigDirector adds every kind together, so a `device` context before sign-in and a `user` context after it count as two. For server SDKs, LaunchDarkly counts service connections and ConfigDirector counts the hours they spend connected. ConfigDirector also counts Admin API requests. [Pricing and billing](https://docs.configdirector.com/getting-started/pricing-and-billing) explains the three and the free allowance, which applies on both pricing options. # Evaluation Activity > See how often each config is evaluated, which values are served, and which SDKs are asking Every time one of your applications evaluates a config, the SDK reports it back to ConfigDirector. The dashboard turns those reports into per-config activity for each environment, so you can confirm that a rollout is doing what you expect, see which value your users are actually getting, and find configs that nothing evaluates anymore. Activity is available on Free and Pay as you go and needs no setup beyond using an SDK. ## What is recorded For each evaluation the SDK reports: - The config key and the environment, taken from the SDK key in use. - The value that was served. - Whether the application fell back to its in-code default value instead, and why (for example the config was not found, or it was requested as an incompatible type). - The SDK name and version. - The context the value was evaluated for, by context kind and identifier, when the application provided an identifier. Client SDKs report evaluations in batches shortly after they happen. Server SDKs evaluate targeting rules locally and report aggregated counts periodically. In both cases the activity appears in the dashboard within moments of the report arriving, usually well under a minute after the evaluation. Evaluations of keys that do not exist in the project are recorded separately. They never appear in a config's activity, because there is no config to attach them to, but they do raise a [Config not found alert](https://docs.configdirector.com/monitoring/alerts#config-not-found). ## The Activity tab Open a config and select the **Activity** tab. Pick the environment at the top left and a time window at the top right: the last 8 hours, 24 hours, 48 hours, 7 days, or 30 days. Times are shown in your local time zone. Three charts are shown for the selected environment and window: - **Total evaluations**: how many times the config was evaluated over time. Use it to confirm an application is live in an environment, to see traffic patterns, and to notice when evaluations stop. - **Evaluations by value**: the same evaluations broken down by the value that was served. During a percentage rollout this is where you check that the split matches the percentages you configured. After a rollout reaches everyone, one value should account for all of the evaluations. - **Evaluations by SDK**: the evaluations broken down by SDK name and version. Use it to see which applications have picked up a change, and which are still running an older SDK. Evaluations that fell back to the in-code default value are included in the totals. If a large share of evaluations are defaults, the [In-code default value served too often alert](https://docs.configdirector.com/monitoring/alerts#in-code-default-value-served-too-often) tells you before you have to notice it in the chart. ## Activity in the configs list The configs list shows an **Evaluations (7 days)** column for the selected environment, with a small chart and the total for the week, next to the config's [lifecycle state](https://docs.configdirector.com/monitoring/lifecycle). Under the chart, each row says when the config was last evaluated in that environment, such as "Last evaluated 3 hours ago". A config that has gone 60 days without an evaluation in that environment shows "Not evaluated in 60 days", and a **New** config shows nothing yet. A config that shows "No evaluations" in every environment is a candidate for cleanup. A temporary config becomes **Archive Ready** once its evaluations stop for well beyond their usual pause, as [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection) explains. ### Finding configs The search box lists the configs whose key or description contains what you type, ignoring case. The **Role**, **Type**, **Status** and **Lifetime** filters take one choice each, and **Tags** lists the configs that have any of the tags you pick, or, with **Untagged**, only the configs without tags. A config has to match the search and every filter you set, and while any of them is set, the list says how many configs match. For example, choose **Role: Feature flag** and **Status: Stale** to list the flags that were created more than 15 days ago and never evaluated. **Clear filters** resets the search and every filter at once. Select the **Key**, **Status** or **Evaluations (7 days)** header to sort the list by that column, and select it again to reverse the order. The list is sorted by key until you choose another column. Sorting by status groups the configs by lifecycle state, and sorting by evaluations puts the configs evaluated most in the selected environment over the last 7 days first. The list shows 50 configs per page, with page controls under it when there are more. Reloading the page, coming back from a config or sharing the link with a teammate shows the list with the environment, search, filters, sort and page you chose. ## Last evaluated When you archive a config or change its SDK availability, the confirmation dialog shows when the config was last evaluated in each environment and through which SDK key, so you can see whether anything still depends on it before you go ahead. The same information is available to AI coding tools through the [MCP server](https://docs.configdirector.com/integrations/mcp)'s `get_config_activity` tool. ## Activity per context Contexts that your applications identify with an `id` appear on the **Contexts** page for their environment, with their context kind. To list only some context kinds, such as devices, select **Kinds** and pick them from the context kinds seen in that environment. The list then shows the contexts of any context kind you picked, and keeps your choice when you open a context and come back, or reload the page. Open a context to see which configs were evaluated for it in a time window, and which value each one returned. This is the quickest way to answer "what did this user get" when someone reports that a feature looks different for them than expected. To recognize a context at a glance, such as a teammate's phone you use for testing, give it a label from its details page. The label appears next to the context's identifier on the **Contexts** page and in the targeting rules tester. A labelled context stays on the **Contexts** page however long your applications go without sending it. ## Retention The longest activity window available is 30 days, and the charts always have the full history they can display. A context your applications have not sent for 180 days is removed from the **Contexts** page, and comes back the next time they send it. Archiving a config removes its activity along with it. If you need the numbers, take them from the Activity tab before archiving. # Alerts > What ConfigDirector watches for in your applications' use of configs, how alerts are prioritized and delivered, and what to do about each one ConfigDirector watches the evaluations your applications report and alerts you when they show a problem: a config key that does not exist, a config requested as the wrong type, an in-code default value that the targeting rules can never serve, an SDK that is out of date or too old to evaluate the segments your rules use. It also tells you when a flag is ready to be cleaned up, when your spend is heading for the spending limit, when a Free organization's usage is heading past the free allowance, and when a webhook was disabled because its receiver kept failing. There is nothing to configure. Alerts are raised for every organization and appear in the dashboard; you choose which ones also reach your email. ## Where alerts appear **Alerts** in the sidebar shows a count of unread alerts, colored by the highest priority among them. The alerts page has three tabs: - **Inbox**: open alerts. Select one or more to mark them as read or dismiss them. - **History**: alerts you dismissed and alerts the system resolved, kept for 30 days. - **Muted**: what your organization has muted; see [Muting](#muting). Each alert has a detail page with a plain-language summary of what was found, the environment and SDK key involved, how many evaluations were affected and from which SDKs and versions, when it was last seen, and a **What to do** section. Read and dismissed states are per user, so dismissing an alert does not hide it from your teammates. ## How alerts are detected and resolved Application, outdated SDK version, and lifecycle alerts are checked every 30 minutes. Usage alerts are checked every hour, and so are the alert for server SDKs that cannot evaluate segments and the alert for a webhook disabled after repeated failures. An alert is raised when a check finds its condition. You do not resolve alerts yourself: an alert is **resolved automatically** when a later check no longer finds the condition. A Config not found alert resolves once the applications have stopped requesting the missing key for the alert's quiet window, a wait sized from how often they usually requested it; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#application-request-alerts). A Config settled alert resolves once the config is no longer settled, or once the applications stop evaluating it or a code scan no longer finds it in the code; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#alerts-that-ask-for-a-code-change). Fixing the cause in the dashboard does not wait for the next check. Saving targeting rules, changing SDK availability, changing a config's lifetime, updating a feature flag to a kill switch or back, archiving a config, restoring a config from the archive, renaming a key, changing the spending limit or a billing alert threshold, switching between Free and Pay as you go, or enabling or deleting a webhook each trigger an immediate check, so the alert resolves right away. The one exception is the SDK cannot evaluate segments alert, which resolves at its next hourly check. Some kinds wait until the condition has been present continuously for a grace period before alerting, so a brief blip during a deployment does not alert anyone. The grace periods are listed with each kind below. ## Priorities Every alert has a priority: **Low**, **Medium**, **High**, or **Critical**. Some kinds always have the same priority. Others carry a range, and the priority is decided by what was found: how large a share of evaluations were affected, whether the config is a kill switch, how close spend is to the spending limit, or the priority chosen for a billing alert threshold. ## Dismissing Dismissing an alert removes it from your inbox. For application and SDK version alerts, a dismissal works like a snooze: if the condition is still occurring after 7 days (14 days for the in-code default value mismatch), the alert returns to your inbox. Lifecycle, usage, and webhook alerts stay dismissed until they resolve. ## Muting Muting is for a problem your team has decided is fine, such as an in-code default value you keep on purpose. A mute stops one kind of alert about one subject, for example an in-code default value mismatch on one config, and it applies to everyone in your organization until someone unmutes it. To mute an alert, open it and choose **Mute**. You can mute it in the alert's environment only, or in all environments of the project, including environments created later. You can add a note to tell your teammates why. Muting resolves the alert right away. Muting in all environments also resolves the same alert in the project's other environments. ConfigDirector keeps checking for the problem while it is muted, so if it is still happening when someone unmutes it, the alert comes back at the next check. Billing, usage, lifecycle, outdated SDK version, and webhook alerts cannot be muted. The **Muted** tab on the alerts page lists your organization's mutes with who muted each one, when, and their note. Unmute one from there. Everyone in the organization can see the mutes. Operators, developers, admins, and owners can mute and unmute alerts. ## Email delivery Open **Email preferences** on the alerts page to choose, for each kind, the lowest priority worth an email. The setting is per organization and applies to you only. Every alert still appears in the dashboard regardless of the email setting. The list follows the organization's pricing option. A Free organization lists the two free allowance alerts, and a Pay as you go organization lists the two spending limit alerts instead; a preference saved under one option is kept and applies again if the organization switches back. Billing alert thresholds have no email preference: each threshold is emailed to owners, admins, and billing managers at the priority chosen for it on the **Billing & usage** page. The service suspended alert of a Free organization has no email preference either: it is always emailed right away. - **Critical** alerts are emailed as soon as they are raised. - Everything else is collected into one **daily digest**. By default the integrity alerts (config not found, availability violations, type mismatch) are emailed, in-code default value and SDK version alerts are emailed at Medium and up, the billing alerts of the organization's option are emailed, the webhook disabled alert is emailed, and the two lifecycle alerts are not emailed. ## Who receives which alerts
Group Received by
Application requests, SDK versions, config lifecycle Everyone in the organization: viewers, operators, developers, admins, and owners
Billing Owners, admins, and billing managers
Integrations Owners and admins
## Alert kinds ### Application requests These are the integrity alerts. They are detected from the evaluations themselves, so they catch mistakes in code that compiles and deploys fine, in any of the SDKs, without any instrumentation on your side. #### Config not found **Priority:** High. **Grace period:** none. An application requested a config key that does not exist in the project, and received its in-code default value. The usual causes are a typo in the key, a config created in a different project, a config that was archived while code still reads it, or code that shipped before the config was created. What to do: create the config, restore it from the archive, or fix the key in the application. The alert names the SDK key and the SDKs that asked, which tells you which application to look at. The alert resolves when the config is created, or once no application has requested the key for the alert's quiet window. The quiet window is sized from how often the key was usually requested, kept between 1 hour and 7 days; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#application-request-alerts). #### Client SDK requested a server-only config **Priority:** High. **Grace period:** none. An application using a client SDK requested a config whose [SDK availability](https://docs.configdirector.com/getting-started/configs#sdk-availability) excludes client SDKs. The application received its in-code default value. What to do: if the config is meant to be used in the client, add client SDKs to its availability. If it is not, remove the evaluation from the client application; the alert is telling you that a value you chose to keep server-side is being asked for from the client. The alert resolves when the config is made available to client SDKs, or once no client SDK has requested it for the alert's quiet window. The quiet window is sized from how often client SDKs usually requested the config, kept between 1 hour and 7 days; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#application-request-alerts). #### Server SDK requested a client-only config **Priority:** High. **Grace period:** none. The reverse case: a server SDK requested a config that is only available to client SDKs. It resolves the same way: when the config is made available to server SDKs, or once no server SDK has requested it for the alert's quiet window. #### Config type mismatch **Priority:** High. **Grace period:** none. An application requested a config as a data type that does not match the config's type, for example `getString` on a boolean flag, or a JSON value that could not be parsed. The SDK returned the in-code default value instead. What to do: fix the call in the application to use the config's type, or change the config's type if the code is right. The alert lists the SDKs and versions that made the mismatched requests. The alert resolves once no application has requested the config as that type for the alert's quiet window. The quiet window is sized from how often the mismatched requests usually happened, kept between 1 hour and 7 days; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#application-request-alerts). #### In-code default value mismatch **Priority:** Medium, or High if the default was actually served. **Grace period:** 5 days. An application carries an in-code default value that none of the targeting rules in that environment can serve. For a boolean flag that means the in-code default is `true` while every rule serves `false`, or the other way around; for other types it means a value that no rule mentions. The in-code default value is what your users get when the SDK cannot evaluate the config, so a default the rules would never serve is a behavior your users can hit and you never tested. Kill switches are not covered, because a kill switch's in-code default value is chosen for safety and is expected to differ from what the rules serve day to day. A feature flag that becomes a kill switch has its open mismatch alerts resolved right away. What to do: align the in-code default value with the [default targeting rule value](https://docs.configdirector.com/getting-started/core-concepts#targeting-rules), or add the value to the rules if it is intentional. The 5-day grace period exists because the mismatch is often temporary while a rollout changes the rules. The alert also resolves once no application has used a mismatching in-code default for the alert's quiet window. The quiet window is sized from how often the mismatch usually happened, kept between 1 day and 14 days; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#in-code-default-alerts). If you archive the config, the alert resolves right away. The alert is raised, and stays open, only while the config is still in use: applications still evaluate it, or a recent code scan still finds it in the code, judged by how often it is usually evaluated; see [Alerts that ask for a code change](https://docs.configdirector.com/monitoring/dynamic-detection#alerts-that-ask-for-a-code-change). #### In-code default value served too often **Priority:** Medium; High when defaults are half or more of evaluations; one step higher for kill switches, up to Critical. **Grace period:** 2 days, or none when Critical. An application fell back to its in-code default value for at least 10% of its evaluations of a config in an environment, over at least 100 evaluations in the last 24 hours. The alert lists the reasons the SDK reported, such as the config not being available to that SDK, a type mismatch, or evaluation before the client was ready. What to do: the reasons point at the cause. Evaluations before the client is ready are usually fixed by awaiting initialization in the application; the other reasons correspond to the alerts above. The alert resolves once no application receives the in-code default value for the alert's quiet window. The quiet window is sized from how often this usually happened, kept between 1 day and 14 days; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#in-code-default-alerts). If you archive the config, the alert resolves right away. The alert is raised, and stays open, only while the config is still in use: applications still evaluate it, or a recent code scan still finds it in the code, judged by how often it is usually evaluated; see [Alerts that ask for a code change](https://docs.configdirector.com/monitoring/dynamic-detection#alerts-that-ask-for-a-code-change). ### SDK versions #### Outdated SDK version **Priority:** Medium. **Grace period:** 2 days. An SDK version that ConfigDirector no longer recommends is connected to one of your environments. The alert names the SDK, the version, the SDK key, and how many connections were seen. What to do: upgrade the SDK. The [SDK reference](https://docs.configdirector.com/sdks/available-sdks) lists the current versions. The alert resolves once no application has connected with that version for the alert's quiet window. A connection counts until it closes, so an application that stays connected keeps the alert open. The quiet window is sized from how often applications usually connected with that version, kept between 1 day and 14 days; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#sdk-connection-alerts). #### SDK cannot evaluate segments **Priority:** Medium, or High in a live environment. **Grace period:** none. In the last 24 hours, an application connected to an environment with a server SDK version too old to evaluate [segments](https://docs.configdirector.com/getting-started/segments), and the targeting rules of at least one config in that environment use a segment. ConfigDirector never sends that version a rule with a segment condition, so for the applications running it those rules are skipped and the next rule applies. The alert names the SDK, the version, the SDK keys it connected with and how many connections each had, and how many configs in the environment use segments. Client SDKs never raise it, because ConfigDirector evaluates segments for them. What to do: upgrade every application that connects to the environment with that version. [Server SDK versions](https://docs.configdirector.com/getting-started/segments#server-sdk-versions) lists the versions that evaluate segments. The alert resolves once that version has not connected to the environment for the alert's quiet window, or once no config in the environment uses a segment. Saving targeting rules does not resolve it right away; the next hourly check does. The quiet window is sized from how often applications usually connected to the environment with that version, kept between 1 day and 14 days; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection#sdk-connection-alerts). ### Config lifecycle These two alerts turn the [config lifecycle](https://docs.configdirector.com/monitoring/lifecycle) into a to-do list. They are raised for **temporary** configs only: a permanent config is never reported as settled or ready to archive, because it is expected to stay. They are Low priority and not emailed by default; turn them on in Email preferences to get each new one in the daily digest. #### Config settled **Priority:** Low. **Grace period:** none. A temporary config has served the same value in every live environment for its role's horizon (7 days for a feature flag, 14 for an experiment, 30 for a config) and can be hardcoded and removed. The settled value is shown on the config page. What to do: hardcode the value in your applications and deploy. The alert resolves once the applications stop evaluating the config or a code scan no longer finds it in the code, judged by how often it is usually evaluated; see [Alerts that ask for a code change](https://docs.configdirector.com/monitoring/dynamic-detection#alerts-that-ask-for-a-code-change). Once the applications stop evaluating the config, it also becomes ready to archive. If you archive the config before then, the alert resolves right away. If you are keeping the config, update a feature flag's role to kill switch, or a config's lifetime to permanent, and the alert resolves right away too. #### Config ready to archive **Priority:** Low. **Grace period:** none. A temporary config has gone its quiet window without an evaluation in its live environments and can be archived. The alert shows how long the config's quiet window is and says which environments were counted: only the live ones, or every environment when the config has no recent live use to judge by. The quiet window is sized from how often the config is usually evaluated; see [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection). What to do: archive it from the config's **Settings** tab, and the alert resolves right away. If an application still needs it, the alert resolves on its own once the config is evaluated again in a live environment, or once an application anywhere evaluates it every working day. If you are keeping the config, update a feature flag's role to kill switch, or a config's lifetime to permanent, and the alert resolves right away. ### Billing Billing alerts go to owners, admins, and billing managers. The three spend alerts belong to Pay as you go organizations and the two free allowance alerts to Free organizations; switching between them resolves the open alerts of the other option. They are checked every hour and raised once per billing cycle for each spending limit amount, billing alert threshold, or billing metric. Raising a spending limit or a threshold during the month arms its alerts again, and lowering one below the month's spend fires its reached alert at the next check. Changing or removing a limit or a threshold resolves its open alerts right away. #### Spend projected to reach the spending limit **Priority:** High at 70%, Critical at 90%. **Grace period:** none. A Pay as you go organization's spend for the month is above 70% or 90% of its spending limit and, at the current rate, on track to reach it before the billing cycle ends. What to do: review the month's spend on the **Billing & usage** page. Raise or remove the spending limit if the growth is expected, or leave it in place to cap the invoice. #### Spending limit reached **Priority:** Critical. **Grace period:** none. Spend for the month has reached the spending limit. Service is suspended for the rest of the billing cycle: SDK connections are refused, applications fall back to their in-code default values, and the Admin API and the MCP server refuse requests. Nothing above the limit is charged. See [Spending limit](https://docs.configdirector.com/getting-started/pricing-and-billing#spending-limit). What to do: raise or remove the spending limit on the **Billing & usage** page to resume service right away, or leave it until the next billing cycle starts. #### Billing alert threshold reached **Priority:** the priority chosen for the threshold: Medium, High, or Critical. **Grace period:** none. Spend for the month has reached one of the organization's billing alert thresholds. Reaching a threshold never affects the service; it is a heads-up about the invoice to come. A threshold above the spending limit never fires, so the thresholds follow the invoice, which never exceeds the limit. This kind has no email preference: the priority chosen for the threshold decides the email, Critical right away and Medium or High in the daily digest. What to do: review the month's spend and adjust the thresholds on the **Billing & usage** page. #### Usage projected to exceed the free allowance **Priority:** Critical. **Grace period:** none. A Free organization's usage of a billing metric (monthly active users, service connection hours, or Admin API requests) for the month is above 70% or 90% of the free allowance and, at the current rate, on track to exceed it. What to do: review the **Billing & usage** page, which shows each metric against the free allowance. Start pay as you go if the growth is expected. #### Free allowance exceeded **Priority:** Critical. **Grace period:** none. A Free organization's usage of a billing metric for the month has exceeded the free allowance. The billing grace period of 7 days starts, and the alert names the date and time it ends. After that, service is suspended until pay as you go starts or the next billing cycle begins. What to do: start pay as you go on the **Billing & usage** page within the billing grace period. #### Service suspended: free allowance exceeded **Priority:** Critical. **Grace period:** none. The billing grace period of a Free organization has ended with usage still over the free allowance, and service is suspended: SDK connections are refused, applications fall back to their in-code default values, and the Admin API and the MCP server refuse requests. The alert names the metrics over the allowance and when the suspension started. This kind has no email preference: it is always emailed right away. See [The free allowance](https://docs.configdirector.com/getting-started/pricing-and-billing#the-free-allowance). What to do: start pay as you go on the **Billing & usage** page to reinstate the service right away, or wait for the next billing cycle. ### Integrations This alert goes to owners and admins, who manage the organization's [webhooks](https://docs.configdirector.com/integrations/webhooks). #### Webhook disabled after repeated failures **Priority:** Critical. **Grace period:** none. Deliveries to one of the organization's webhooks were attempted on each of the last three days and none of them succeeded, so ConfigDirector disabled the webhook and abandoned the deliveries still waiting. Webhook events that happen while the webhook is disabled are not delivered later. The alert names the webhook and its URL, and shows the receiver's last response status or connection error. What to do: check that the receiver at the webhook's URL is reachable and answers with a `2xx` status, then enable the webhook again on its page, or delete it if you no longer need it. The alert resolves right away when the webhook is enabled again or deleted. Alerts cover what ConfigDirector can observe from the SDKs. They do not replace error monitoring in your application: an exception thrown after a value was served correctly is not something the evaluation report can see. # Dynamic Detection > How ConfigDirector decides how long a config must go without evaluations before it is ready to archive, how long an old key is kept after a rename, and how long an application request alert waits before resolving, based on how often each usually happens Some of what ConfigDirector tells you is concluded from silence: a temporary config that your applications have stopped evaluating is [ready to archive](https://docs.configdirector.com/monitoring/lifecycle#archive-ready-temporary-configs), an old key left behind by a [rename](https://docs.configdirector.com/getting-started/configs#key) is removed once your applications stop using it, and an [alert about requests your applications make](#application-request-alerts), such as requesting a config key that does not exist, resolves once they stop. How long that silence has to last depends on how often the thing being watched usually happens. A flag that is read on every app request and then goes for two entire days without an evaluation is almost certainly gone from the code. A flag read by a weekly job that goes a week without evaluations, is simply waiting for the next job run. So instead of one fixed wait for everything, ConfigDirector gives each config its own wait, called its **quiet window**, based on how often it has been evaluated recently. A config that your applications read all the time gets a short window, and one read on a schedule gets a window long enough to cover the usual pause between runs, with room to spare. The window is worked out again every 30 minutes, so it follows changes in how your applications use the config. To see a temporary config's current quiet window once it has been evaluated, hover its status in the configs list. ## An example Take two temporary feature flags that have both been evaluated in production for months: - `new-checkout` is read by the web app on every page load, many times a minute. Its quiet window is 4 days, the shortest allowed. - `invoice-layout-v2` is read only by a billing job that runs every Monday. A week without an evaluation is normal for it, so its quiet window is 14 days. When the code that reads `new-checkout` is removed, the flag becomes **Archive Ready** 4 days after its last evaluation. `invoice-layout-v2` becomes **Archive Ready** only after two weeks without an evaluation, so a single skipped run does not mark it ready to archive. With one fixed wait of 4 days for both, `invoice-layout-v2` would be marked ready to archive every Friday while the job still depends on it, and archiving it would leave the next Monday's run with its in-code default value. ## Give each application its own app name A flag is often read by more than one application, or by more than one component within an application. Say the web app also reads `invoice-layout-v2` all day, every day, while the billing job keeps reading it every Monday. Give the web app and the job different app names when they initialize the ConfigDirector SDK. ConfigDirector can then tell them apart and see the two distinct usage patterns, so the flag keeps the 14-day quiet window that covers the weekly job. Even after the web app stops reading the flag, it stays **In use** for as long as the job keeps running. Without different app names, the web app and the job would look like a single application to ConfigDirector. The job's evaluations then mix in with the web app's constant stream, the flag looks like one that is read all the time, and it gets the 4-day quiet window. Once the web app stops reading the flag, it would be marked ready to archive between the job's runs. The setting is called `appName` in most SDKs, and each [SDK's page](https://docs.configdirector.com/sdks/available-sdks) shows where to set it. ## Safety limits Every quiet window is kept between a minimum and a maximum: - The **minimum** is the shortest the window can be, however often the config is evaluated. It is a safety margin for uses that are easy to miss, such as a daily job inside the same application as busy web traffic, whose evaluations mix in with that traffic. - The **maximum** is the longest the window can be, however rarely the config is evaluated. A config read only every few months, such as by a quarterly job, can become **Archive Ready** between runs, so check when it was last evaluated before you archive it. Until a config has been evaluated for long enough to show its usual pace, its quiet window has a fixed length instead. A config first evaluated 3 days ago has a quiet window of 7 days, so it cannot be marked ready to archive until a week after that first evaluation. The same applies to an application that starts reading a config, or starts reporting a new app name: for its first week, ConfigDirector allows for it being one that runs rarely, so the config's quiet window can be longer.
Quiet window Minimum Maximum Until there is enough history
Archive Ready 4 days 45 days 7 days
Old keys 35 days None 35 days
Application request alerts 1 hour 7 days 24 hours
In-code default alerts 1 day 14 days 7 days
Outdated SDK version alerts 1 day 14 days 5 days
SDK cannot evaluate segments alerts 1 day 14 days 7 days
[Archive Ready](https://docs.configdirector.com/monitoring/lifecycle#archive-ready-temporary-configs) explains which environments' evaluations count toward a config's quiet window. ## Old keys When you [rename a config](https://docs.configdirector.com/getting-started/configs#key), its old key keeps working alongside the new one, unless the config was created less than two hours ago, has never been evaluated and no code scan references it, in which case the key is replaced outright. ConfigDirector removes an old key once it has gone its quiet window without an evaluation in any environment, and your applications have evaluated a newer key since. The wait never starts before the rename, so every old key is kept for at least 35 days. If no newer key is ever evaluated, the old key stays. Old keys are checked twice a day. The 35-day minimum covers jobs that run at least once a month. An old key read less often, such as by a quarterly job, can be removed between runs, so update those jobs to the new key before their next run. ## Application request alerts The [Config not found](https://docs.configdirector.com/monitoring/alerts#config-not-found), [Client SDK requested a server-only config](https://docs.configdirector.com/monitoring/alerts#client-sdk-requested-a-server-only-config), [Server SDK requested a client-only config](https://docs.configdirector.com/monitoring/alerts#server-sdk-requested-a-client-only-config) and [Config type mismatch](https://docs.configdirector.com/monitoring/alerts#config-type-mismatch) alerts resolve once ConfigDirector no longer detects the requests behind them: those requests have stopped for the alert's quiet window. That window is sized from how often those particular requests usually happened, not from how often the config is evaluated overall: a key that a nightly job keeps misspelling is requested once a day, so its alert waits about two days of silence before resolving, while an alert about a typo in a page that is loaded all day resolves about an hour after the fix ships. The window is worked out again at every check. Until the requests have been seen for a full day, the window is 24 hours. It is never shorter than 1 hour or longer than 7 days, so an alert about requests that only a weekly job makes can resolve between runs and be raised again at the next one. ## In-code default alerts The [In-code default value mismatch](https://docs.configdirector.com/monitoring/alerts#in-code-default-value-mismatch) and [In-code default value served too often](https://docs.configdirector.com/monitoring/alerts#in-code-default-value-served-too-often) alerts resolve once ConfigDirector no longer detects the problem behind them: the problem has stopped for the alert's quiet window, sized from how often it usually happened. The window is worked out again at every check. Until the problem has been seen for a full week, the window is 7 days. It is never shorter than 1 day or longer than 14 days. ## SDK connection alerts The [Outdated SDK version](https://docs.configdirector.com/monitoring/alerts#outdated-sdk-version) and [SDK cannot evaluate segments](https://docs.configdirector.com/monitoring/alerts#sdk-cannot-evaluate-segments) alerts resolve once ConfigDirector no longer detects applications connecting with the SDK version behind them: no application has connected with that version for the alert's quiet window, sized from how often applications usually connected with it. A connection counts from when it opens until it closes, so an application that stays connected for weeks keeps its alert open, while a job that connects every few days gets a window long enough to cover the wait between runs. The window is worked out again at every check. Until the version has been seen connecting for long enough, the window is 5 days for an outdated SDK version and 7 days for a version that cannot evaluate segments. It is never shorter than 1 day or longer than 14 days. ## Alerts that ask for a code change The [Config settled](https://docs.configdirector.com/monitoring/alerts#config-settled), [In-code default value mismatch](https://docs.configdirector.com/monitoring/alerts#in-code-default-value-mismatch) and [In-code default value served too often](https://docs.configdirector.com/monitoring/alerts#in-code-default-value-served-too-often) alerts ask you to change code: hardcode a value, or fix an in-code default value. They are raised, and stay open, only while the config is still in use. ConfigDirector judges that by how often the config's busiest application usually evaluates it, so a flag read on every page load counts as out of use soon after the evaluations stop, while a flag read by a nightly job is given time for the next run. An alert whose config goes out of use resolves at the next check. If the problem is still there once the config is evaluated again, a new alert is raised. A [code scan](https://docs.configdirector.com/integrations/cli#scan) that stops finding a config an earlier scan found counts as the config being removed from the code, and ConfigDirector gives that change time to reach every application. From that scan until the config has been evaluated for a while past it, these three alerts are not raised and open ones resolve, even while an older deployment still evaluates the config. Deleting a scanned repository is not a scan, and a config no scan has ever found is judged by its evaluations alone. A config read in a way the scan cannot see, such as through a key built at runtime or from a repository that is never scanned, is judged by its evaluations again after that time. Its alerts can arrive late, but they do arrive. # Config Lifecycle > The states a config moves through from creation to cleanup, how the lifetime decides which of them apply, and what to do in each one Feature flags are meant to be removed once the feature has shipped, and configs that nothing reads anymore are clutter that eventually misleads someone. ConfigDirector tracks a lifecycle state for every config from the evaluations your applications report, and tells you when a config has served its purpose. The state is shown on the config page and in the **Status** column of the configs list, where you can [filter and sort by it](https://docs.configdirector.com/monitoring/activity#finding-configs). States are recalculated every 30 minutes. ## The lifetime decides the lifecycle Every config has a [lifetime](https://docs.configdirector.com/getting-started/configs#lifetime): **Temporary** or **Permanent**. The lifetime is the single most important input to the lifecycle, because it tells ConfigDirector whether "this config is no longer needed" is a conclusion it is allowed to draw. - A **temporary** config is expected to be removed one day. Feature flags and experiments are always temporary. ConfigDirector tracks temporary configs for cleanup: it detects when they have **settled** and when they are **ready to archive**, and raises the two lifecycle alerts. - A **permanent** config is expected to stay. Kill switches are always permanent. A permanent config that has not been read for a while is not unused, it is waiting, so ConfigDirector never marks it settled or ready to archive, and never suggests removing it. - A config of the **Config** role chooses its lifetime, and starts as permanent. Both lifetimes share the states that describe plain usage: **New**, **In use**, and **Stale**. A config of the Config role can change its lifetime at any time from its **Settings** tab. A feature flag or a kill switch updates its role instead, from the same tab: a flag that has become a long-term toggle becomes a kill switch, which takes it out of cleanup tracking and returns a settled or ready to archive flag to **In use**; a kill switch that turns out to be short-lived becomes a feature flag, which puts it in. ## States
State Applies to Meaning
New All configs The config was created and has not been evaluated in any environment yet.
In use All configs The config has been evaluated in at least one environment and has not gone quiet.
Stale All configs The config was created more than 15 days ago and has never been evaluated.
Settled Temporary only The config has served a single value in every live environment long enough that it can be hardcoded and removed.
Archive Ready Temporary only The config was in use and its live environments have not evaluated it for its quiet window, which is sized from how often it is usually evaluated.
States transition on their own, and there is no manual reset. A **New** or **Stale** config returns to **In use** as soon as an SDK evaluates it again, and so does an **Archive Ready** config evaluated in a [live environment](https://docs.configdirector.com/getting-started/core-concepts#environments). An evaluation in another environment brings an **Archive Ready** config back only when it has no recent real use to judge by, or when an application in that environment is clearly still evaluating it regularly (see [Archive Ready](#archive-ready-temporary-configs)). A **Settled** config returns to **In use** at the next check once it no longer meets the conditions below. A permanent config therefore only ever shows **New**, **In use**, or **Stale**. If you expect to see **Settled** or **Archive Ready** on a config and never do, check its lifetime first. ## How each state is decided ### In Use A config is in use from the moment any environment receives an evaluation for it. Evaluations that fell back to the in-code default value count too: the application is still asking for the config. A temporary config leaves this state when it settles or becomes ready to archive; a permanent config stays in use. ### Stale A config that is more than 15 days old and has never been evaluated is stale, whatever its lifetime. It was created and then never wired into an application, or the application that was going to use it never shipped. The state exists so these configs do not blend in with the ones that are simply new. ### Settled (temporary configs) A settled config is one whose job is done: in your live environments, the feature flag has been on for everyone for a while, or the config has returned the same value to every application for long enough that it might as well live in the code. The horizon depends on the role:
Role Horizon
Feature flag 7 days
Experiment 14 days
Config 30 days
Kill switch Never settles
A temporary config settles when, over the whole horizon and in every live environment that has recent activity: - Every evaluation served the same value. Evaluations that fell back to the in-code default value are tolerated as long as they are no more than 20% of the total. - The config was already being evaluated in at least one of those environments when the horizon began, and it is still being evaluated: its latest evaluation is more recent than the config's quiet window (see [Archive Ready](#archive-ready-temporary-configs)), or than the horizon when that is shorter. - The current targeting rules in every one of those environments can only serve that value. A rule or percentage split that could still return a different value to someone, even if nobody matched it during the horizon, means the config is not done yet. - For feature flags and boolean configs, the value is `true`. A flag that has been off in every live environment for a week has not shipped, it has been abandoned, and that is a cleanup decision for you rather than a settled verdict. Only live environments count: those with **Live environment** turned on in their settings. A staging environment without it that still serves the old value does not hold the config back, and a config in a project without a live environment never settles. Kill switches are excluded even when they are temporary, because serving the same value for a long time is exactly what a kill switch is supposed to do. When a config settles, the config page shows the value to hardcode with a copy button, and a [Config settled alert](https://docs.configdirector.com/monitoring/alerts#config-settled) is raised. ### Archive Ready (temporary configs) A temporary config that was **In use** or **Settled** and then receives no evaluations in its live environments for its quiet window becomes **Archive Ready**. This is the usual end of a feature flag: the code that read it was removed, the SDKs stopped asking, and the config can go. The question is whether real traffic still reads the config, so the decision looks at the [live environments](https://docs.configdirector.com/getting-started/core-concepts#environments), the same ones that decide **Settled**. A developer trying the flag now and then in a test environment neither keeps it in use nor stretches its quiet window. A config with no recent live use at all, because it was never released or its project has no live environment, is judged on whichever environments do evaluate it, so it is not marked ready to archive just for never having gone live. And an application in any environment that still evaluates the config every working day, such as a nightly job in staging, keeps it in use: the code that reads the flag is clearly still there. Each config has its own quiet window, sized from how often it is usually evaluated in the environments that count, so a config that applications read all day goes quiet within a few days, while one read by a weekly job is given longer. Once a temporary config has been evaluated, hover its status in the configs list to see how long its quiet window is. [Dynamic Detection](https://docs.configdirector.com/monitoring/dynamic-detection) explains how the window is sized and how long it can be. The check runs every 30 minutes, and an **Archive Ready** config whose evaluations turn out to be more recent than its quiet window, or that an application evaluates every working day, returns to **In use**. A [Config ready to archive alert](https://docs.configdirector.com/monitoring/alerts#config-ready-to-archive) is raised when a config enters this state. A permanent config that stops being evaluated simply stays **In use** and shows no recent evaluations in the configs list; it is never marked for archival. If you want cleanup tracking for it, update a kill switch's role to feature flag, or a config's lifetime to temporary. ## What to do in each state - **Settled**: hardcode the settled value in your applications, deploy, wait for the state to move to **Archive Ready**, then archive. If the flag is going to be turned back off again, it is not temporary: update its role to kill switch, or a config's lifetime to permanent. - **Archive Ready**: archive the config from the **Settings** tab of the config page. The confirmation dialog shows the config's quiet window and when it was last evaluated in each environment. - **Stale**: either wire it into the application or archive it. - **In use** for a long time with a temporary lifetime: consider whether the flag has become permanent in practice, and if so update its role to kill switch, or a config's lifetime to permanent, so it stops being tracked for cleanup. ## Archiving Archiving removes the config from the project and from the SDK payloads. An SDK that still asks for an archived config receives its in-code default value and raises a [Config not found alert](https://docs.configdirector.com/monitoring/alerts#config-not-found), which is how you find out if the cleanup ran ahead of a deployment. Archived configs are listed on the archived configs page — reachable from the **View** link in the archived pill beside **Create config** on the Configs page — for 30 days and can be restored from there with their targeting rules intact. After 30 days they are purged. Renaming a config key follows the same principle as the lifecycle: each old key keeps working alongside the new one for at least 35 days and until it is no longer used, and only then is it removed. See [Key](https://docs.configdirector.com/getting-started/configs#key). # ConfigDirector's SDKs > Browse the ConfigDirector SDKs for browsers, mobile apps, Next.js and Nuxt, servers, and OpenFeature providers Select one of the SDKs below to view its documentation. Every SDK can also run without a connection, serving values you give it; the Node.js SDK's [Run without a connection](https://docs.configdirector.com/sdks/server/node-js#run-without-a-connection) section shows an example. Every SDK and OpenFeature provider for browsers and mobile apps keeps the last values it received on the device and serves them on the next launch with the same context until live values arrive, even without a network; the JavaScript SDK's [Values kept on the device](https://docs.configdirector.com/sdks/browser/javascript#values-kept-on-the-device) section shows how it works. We are adding new SDKs regularly. If you don't find your SDK, drop us a line to [sdks@configdirector.com](mailto:sdks@configdirector.com) with the SDK you are looking for and we'll let you know the projected date for its release. Javascript React Vue React Native Flutter Swift Android Next.js Nuxt Node.js Python Java .NET Go New to OpenFeature, or choosing between a provider and a native SDK? Start with [OpenFeature with ConfigDirector](https://docs.configdirector.com/sdks/openfeature). # Javascript Browser SDK > Install and configure the Javascript browser SDK, and evaluate configs and feature flags in web applications ## Introduction The Javascript browser SDK is intended to be used by web applications running on modern web browsers. All modern web browsers on popular platforms should be supported. Browser SDKs are tested on the latest versions of Chrome, Firefox, Safari, and Edge. Telemetry collection within browser SDKs uses Web Workers to aggregate and send telemetry data. If the SDK runs on older browsers without Web Workers support, config evaluation will continue to work but no telemetry data will be collected for that session. ## Installation The SDK can be installed from NPM: [https://www.npmjs.com/package/@configdirector/client-sdk](https://www.npmjs.com/package/@configdirector/client-sdk) ```bash [npm] npm install --save @configdirector/client-sdk ``` ```bash [yarn] yarn add @configdirector/client-sdk ``` ```bash [pnpm]Javascript pnpm add @configdirector/client-sdk ``` ```bash [bun] bun add @configdirector/client-sdk ``` ## Configure and initialize the client 1. Create an instance of the client providing your client SDK key. You can retrieve a client SDK key for each environment under `SDK keys` in the dashboard's navigation panel. 2. Initialize the client to initiate its connection lifecycle. ```ts [config-director-setup.ts (Initialize the client)] import { createClient } from "@configdirector/client-sdk"; export const client = createClient("YOUR-CLIENT-SDK-KEY"); await client.initialize(); ``` Before the client is initialized, config values will evaluate to the default value provided in code. ### Additional configuration options #### `metadata` The `metadata` option allows you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application. ```ts [config-director-setup.ts] import { createClient } from "@configdirector/client-sdk"; export const client = createClient("YOUR-CLIENT-SDK-KEY", { metadata: { appName: "YOUR-APP-NAME", appVersion: "1.0.2", }, }); await client.initialize(); ``` #### `logger` By default, the SDK logs to the console and it is set to log warnings and errors only. You can configure a `logger` by either creating a ConfigDirector console logger with a different log level, or by implementing the `ConfigDirectorLogger` interface to provide your own logger. The interface can be used to create an adapter to another logging library. Configure the ConfigDirector console logger to a different level: ```ts [config-director-setup.ts] import { createClient, createConsoleLogger } from "@configdirector/client-sdk"; export const client = createClient("YOUR-CLIENT-SDK-KEY", { logger: createConsoleLogger("debug"), }); await client.initialize(); ``` Implement your own logger adapter: ```ts [config-director-setup.ts] import { createClient, ConfigDirectorLogger } from "@configdirector/client-sdk"; const myLogger: ConfigDirectorLogger = { debug: function (message: string, ...args: any): void { // your specific logging library implementation here }, info: function (message: string, ...args: any): void { // your specific logging library implementation here }, warn: function (message: string, ...args: any): void { // your specific logging library implementation here }, error: function (message: string, ...args: any): void { // your specific logging library implementation here }, }; export const client = createClient("YOUR-CLIENT-SDK-KEY", { logger: myLogger, }); await client.initialize(); ``` #### `connection` The connection object accepts the following optional values: - `mode` - The connection mode, one of `streaming`, `polling` or `offline`. It is recommended to use the default of `streaming` unless you have a specific need to use `polling` instead. In `offline` mode the client never connects to ConfigDirector and serves the values you give it; see [Run without a connection](#run-without-a-connection). - Defaults to `streaming` - `values` - The values to serve in `offline` mode, keyed by config key. It can be given only in `offline` mode. See [Run without a connection](#run-without-a-connection). - `pollingInterval` - The interval, in seconds, between requests for config updates when `mode` is `polling`. It has no effect in the other modes. A value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). - Defaults to `60` seconds - `timeout` - The timeout, in milliseconds, to be used in initialization and when updating the context. This is how long the `initialize` method will wait for data from ConfigDirector services before resolving its Promise. If the timeout is reached, `initialize` will return but the client will still be in an unready status and returning default values. The client will continue to attempt to connect and retrieve config values in the background. It has no effect in `offline` mode. - If your application is used in environments where the users frequently have poor or no connection, you may want to use a lower timeout, or simply don't await the `initialize` method call - Defaults to `3000` milliseconds - `url` - The base URL used to connect to ConfigDirector services - This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. It has no effect in `offline` mode. ```ts [config-director-setup.ts] import { createClient } from "@configdirector/client-sdk"; export const client = createClient("YOUR-CLIENT-SDK-KEY", { connection: { mode: "streaming", timeout: 2_000, // 2,000 milliseconds initialization timeout }, }); await client.initialize(); ``` ### Hooks Hooks are events you can subscribe to in order to be notified of some key actions from the client. In order to subscribe to hooks you can provide a handler (or an array of handlers) in the options: ```ts [config-director-setup.ts] import { createClient } from "@configdirector/client-sdk"; export const client = createClient("YOUR-CLIENT-SDK-KEY", { hooks: { configEvaluated: (event) => { console.log("Received configEvaluated:", event); }, }, }); await client.initialize(); ``` The following hooks are available: - `clientReady: { action: ClientConnectAction }` - Emitted when the client is initialized, or after reconnection due to a context update. When this event is emitted it means the client has received a payload from the ConfigDirector servers and is ready to evaluate configs. The event payload is an object with a single property: - `action` - The action that triggered the client to connect/reconnect, can be either `initialization` or `context update`. - `contextUpdated: { context: ConfigDirectorContext | undefined }` - Emitted whenever the context is updated, including during initialization. `contextUpdated` is emitted before `clientReady`, therefore should not be relied upon as a lifecycle event but rather as an informational event to track updates to the context. - `context` - The context the client is being updated to. It can be `undefined`. - `configsUpdated: { keys: string }` - Emitted when a payload is received from the ConfigDirector servers with config data. It is emitted during initialization or context update when an entire config payload is received. After initialization or context update, it is emitted when updates are pushed from the server (or discovered via polling if that connection mode is used). - `keys` - A string array listing the config keys that were included in the payload from the server. - `removedKeys` - A string array listing the keys of configs the payload removed. A config is removed when a full payload no longer includes it. Empty when nothing was removed. - `cachedValuesLoaded: {}` - Emitted when the values kept on the device from the last payload are loaded and served, before live values arrive. See [Values kept on the device](#values-kept-on-the-device). The event carries nothing. - `configEvaluated: { evaluation: ConfigEvaluation }` - Emitted whenever a config is evaluated. This includes calls to `getValue` and `evaluate`, and evaluations delivered to listeners via `watch`. - `evaluation` - A `ConfigEvaluation` object containing the details of the evaluation: - `key: string` - The config key that was evaluated - `value: string | number | boolean | object | ConfigEnumLikeType` - The value the config evaluated to. It can be the default value provided to `getValue` or `watch`. For example, if the config was evaluated before the client was initialized. - `valueId: string | undefined` - The value ID, which is a stable hash of the value that can be used for analytics or other third parties rather than sending the `value`. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves to third parties. It may be `undefined` if the evaluation had to fall back to the default value. - `isDefaultValue: boolean` - Whether or not the evaluation fell back to the default value provided in code. - `reason: EvaluationReason` - The reason for the evaluation resolution. In the case the config successfully evaluated based on server-provided targeting rules, it will be `found-match`. A value served from the values kept on the device, before live values arrive, reports `cached`; see [Values kept on the device](#values-kept-on-the-device). If the evaluation had to fall back to the default value, the `reason` will encode why the fallback was required: - `client-not-ready` - The evaluation happened before the client finished initialization - `config-state-missing` - The requested config key was not present in the payload received from the server. This could be due to an incorrect config key, or a config that was not enabled to be available to client SDKs. - `value-missing` - The config key was present in the payload received from the server, but it carried no value or an empty string. - `invalid-number` - The config was requested as a number but the value received from the server could not be converted to a number. - `invalid-boolean` - The config was requested as a boolean but the value received from the server could not be converted to a boolean. - `invalid-json` - The config was requested as a JSON object but the value received from the server could not be converted to a JSON object. - `type-mismatch` - The config was requested with a data type that did not match the type of the config and no reasonable type conversion was possible. ### Run without a connection Set the connection `mode` to `offline` to run your application without connecting to ConfigDirector, for example in a suite of UI tests where each run pins every flag to a known value. An offline client needs no client SDK key, opens no connection and sends nothing, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no lifecycle state changes. Every context receives the same value and targeting rules are not applied. Give the values inline, with one value per config key. Your application decides where they come from, for example a module bundled with the test build or a document it fetched before creating the client: ```ts [config-director-setup.ts] import { createClient } from "@configdirector/client-sdk"; export const client = createClient({ connection: { mode: "offline", values: { "new-checkout": true, "max-items": 20 } }, }); await client.initialize(); ``` The config type of each value follows from the value itself: a boolean, an integral number (`integer`), any other number (`float`), a string, or an object or array (`json`). A value read with an in-code default value of another type returns the in-code default value, as it does against ConfigDirector. `watch` callbacks run once with the offline value, when `initialize` loads it, and never again. `updateContext` stores the new context and changes no value. `pollingInterval`, `timeout` and `url` have no effect. With `mode: "offline"` and no `values`, the client is ready at once and every config returns its in-code default value. `initialize` rejects with a `ConfigDirectorInitializationError`, which the SDK exports, when the values cannot be loaded: a value such as `null`, or `values` given in `streaming` or `polling` mode. The client stays not ready, so a test run with a wrong value fails instead of passing on in-code default values. If you come from LaunchDarkly, a map of values is what LaunchDarkly calls "flags from files", and `offline` with no values is its "offline mode". ### Values kept on the device By default, the client keeps the last values it received from ConfigDirector in the browser's `localStorage`, and serves them on the next launch of the same context until live values arrive. An application opened without a network, after at least one successful connection, shows the values ConfigDirector last sent instead of the default values provided in code. Every evaluation served this way reports the reason `cached`, with `isDefaultValue` set to `false`. The client is not ready while it serves values kept on the device: `isReady` stays `false`, `initialize` still waits for live values or its `timeout`, and `clientReady` fires once live values arrive and replace them. The `cachedValuesLoaded` hook fires when the kept values are loaded, together with `configsUpdated` listing their keys, so watchers and hooks pick them up. The values are kept for the context they were evaluated for, and served on the next launch only when the launch context has the same `kind` and `id`. A launch with another context serves the in-code default values until live values arrive, which then replace the kept values. A context without an `id` matches on the same site, as long as the site's data has not been cleared, because ConfigDirector derives its identifier from the instance ID the SDK stores; a context without an `id` keeps its percentage rollout bucket for as long as that instance ID lasts. Name, traits, app name and app version take no part in the match: live values correct them within a second when there is a network. Once the client is running, updating the context keeps serving the current values until the new context's values arrive, as it does with live values. Turn it off, or supply your own store, with the `caching` option: - `enabled` - Whether to keep the last values received. With `false`, nothing is read or written, even when a `provider` is given. - Defaults to `true` - `provider` - A store of your own, for example an encrypted one, implementing `CachingProvider`: `read()` returns the stored entry or `undefined`, `write(value)` replaces it, `remove()` deletes it, and `sizeLimit()` returns the largest entry, in bytes, the store accepts. Each of `read`, `write` and `remove` may return a promise. One provider instance serves one client. - Defaults to the browser's `localStorage`, under a key derived from the client SDK key, with a size limit of 256 KB ```ts [config-director-setup.ts] import { createClient } from "@configdirector/client-sdk"; export const client = createClient("YOUR-CLIENT-SDK-KEY", { caching: { enabled: true, provider: myEncryptedStore }, }); await client.initialize(); ``` The entry is one string whose format belongs to the SDK and may change between SDK versions; an entry a newer SDK cannot read is removed. Before every write the SDK measures the entry against `sizeLimit()`; above it, the entry is removed and a warning is logged once, so the device never serves values that no longer reflect the set. A provider that fails to read, write or remove is logged once and never stops the client: the worst outcome is the in-code default values. Keeping values on the device is ignored in `offline` mode. The entry, and the `localStorage` key the SDK uses by default, carry a SHA-256 digest of the client SDK key, never the key itself. Browsers compute that digest only on pages served over `https` or from `localhost`, so on a plain `http` page on another host nothing is kept and a `provider` of your own is not called. If you come from Firebase Remote Config, this is what activated values persisting across restarts gives you; if you come from LaunchDarkly, it is the browser SDK's bootstrapping from `localStorage`, without the bootstrap option. ## Retrieve config values To synchronously retrieve config values, use the client's `getValue` method. It requires two arguments. The first argument is the config key, and the second is the default value to be returned if the client has not yet received config values from ConfigDirector services. You can also subscribe to config value changes via the `watch` method. It requires three arguments, the config key, the default value, and a callback function to be called when the config value is updated. When the config is removed, the callback is called with the default value. The `watch` method itself returns a function to remove that specific listener. Alternatively, you may also call the `unwatch` method to remove all listeners for a specific config key. ```ts [main.ts (Use the client)] import { client } from "./config-director-setup" // Retrieve the current value const value = client.getValue("my-config-key", false); // Subscribe to value updates const unwatchMyKey = client.watch( "my-config-key", false, (newValue) => { console.log("Value updated:", newValue) }, ); unwatchMyKey(); // Call the unwatch function returned to remove the listener client.unwatch("my-config-key"); // Removes all listeners for that key ``` Both, `getValue` and `watch` accept default values for any type supported by that specific config (boolean, string, number, enums), and the default value decides the type the config is read as. A string config requested as a boolean or a number is converted when its value spells one. A boolean or number config requested as a string, or any config requested as a type its value cannot be converted to, evaluates to the default value with the `type-mismatch` reason. Mismatched types at runtime are captured by the telemetry collector and will surface as warnings in the ConfigDirector dashboard. A JSON config read with an object or an array as its in-code default value takes its shape from that default: an object reads only a JSON object, and an array only a JSON array. A JSON array read with an object, or a JSON object read with an array, evaluates to the in-code default value with the `type-mismatch` reason. ```ts [main.ts] client.getValue("my-string-config-key", "Default"); client.getValue("my-integer-config-key", 100); client.getValue("my-boolean-config-key", false); client.getValue("my-enum-config-key", MyEnum.SomeDefaultValue); ``` ## Update the context A context can be provided when initializing the client: ```ts [config-director-setup.ts] import { createClient } from "@configdirector/client-sdk"; export const client = createClient("YOUR-CLIENT-SDK-KEY"); await client.initialize({ id: "12345", name: "Example User", traits: { region: "North America", // Any arbitrary traits which can be referenced in targeting rules } }); ``` The context can also be updated via `updateContext`: ```ts [main.ts] import { client } from "./config-director-setup" await client.updateContext({ id: "654321", name: "Another User", traits: { region: "Australia", }, }); // Update it to `undefined` (anonymous user context) when a user signs out await client.updateContext(); ``` To evaluate configs for an entity other than a user, such as a tenant, set the context kind with `kind`: ```ts [main.ts] await client.updateContext({ id: "acme", kind: "tenant", name: "Acme Inc.", traits: { plan: "enterprise", }, }); ``` Leaving `kind` out means `user`. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given. Context kinds are available from version 1.7.0 of the SDK. In client SDKs (browser and mobile), updating the context re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior context.
Awaiting `updateContext` will wait until the new config values are downloaded or the connection times out. In the case of a timeout, the client will continue to attempt to connect with the new context in the background.
The SDK keeps an instance ID in the browser's `localStorage` until the site's data is cleared, and ConfigDirector derives the identifier of a context without an `id` from it. A visitor who has not signed in keeps that identifier, and their percentage rollout bucket, from one visit to the next, and counts as one [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) however often they visit. ## Other useful client features The client provides additional properties and methods. ### `evaluate` Evaluates a config the way `getValue` does and returns the `ConfigEvaluation` behind it — the value, whether it is the in-code default value, the `reason`, and the `valueId` — instead of the value alone. It is the same object the [`configEvaluated` hook](#hooks) reports, so use it where the reason matters at the point of the read, such as when reporting an evaluation to another system; `getValue` is enough everywhere else. Like every read, it is reported to `configEvaluated` and counted in telemetry. The SDK exports the `ConfigEvaluation` and `EvaluationReason` types. ```ts [main.ts] const evaluation = client.evaluate("my-config-key", false); if (evaluation.reason === "client-not-ready") { // Config values have not arrived yet. } ``` `evaluate` is available from version 1.10.0 of the SDK. ### `context` Returns the current context (which may be undefined). When calling `updateContext`, the `context` is not immediately updated. The `context` is updated after the new connection for the new context succeeds (or times out and goes on retry). ### `isReady` Returns a boolean indicating if the client has been successfully initialized and is ready to evaluate configs for the given context. It is initially `false` and becomes `true` after `initialize` succeeds. Upon calling `updateContext` it becomes `false` again and it is set to `true` once `updateContext` succeeds. It stays `false` while the client serves the [values kept on the device](#values-kept-on-the-device). ### `unwatchAll` Removes all listeners for all config keys that were previously created via `watch`. ### `dispose` Removes all listeners and observers, and closes all connections to ConfigDirector services. Only call `dispose` when your application shuts down and it will no longer make use of the client instance. It is generally not needed for applications to call `dispose` explicitly. ## Test your code Use the SDK's testing entry point, `@configdirector/client-sdk/testing`, to test the code that reads your configs and flags. It creates a **test client**: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, so the code under test behaves exactly as it does against ConfigDirector, and no network connection is opened and no telemetry is sent. ```ts import { createTestClient } from "@configdirector/client-sdk/testing"; const testClient = createTestClient({ values: { "new-checkout": true, "max-items": 20 } }); await testClient.client.initialize(); const checkout = new Checkout(testClient.client); expect(checkout.isNewCheckoutEnabled()).toBe(true); testClient.setValue("new-checkout", false); expect(checkout.isNewCheckoutEnabled()).toBe(false); ``` `testClient.client` is a `ConfigDirectorClient`, so it goes anywhere your code accepts one. It starts uninitialized, like a production client, because the code under test usually owns the call to `initialize`. ### Values `values`, `setValue`, and `replaceValues` take native values, and the config type follows from each value: a boolean, an integral number (`integer`), any other number (`float`), a string, or a plain object or array (`json`). Every value is served to every context. A `null` or `undefined` value, a non-finite number, an integer beyond `Number.isSafeInteger`, an empty key, or JSON contents that cannot be encoded (functions, `Date` or `Map` instances, `bigint`) throw a `ConfigDirectorValidationError`, which the entry point exports. Reads behave as they do in production: `setValue("k", true)` read as a string returns the in-code default value with the `type-mismatch` reason, and `setValue("k", "")` serves the in-code default value with the `value-missing` reason. ### Controls
Control Effect
setValue(key, value) Stores the value and, once the client is connected, delivers an update carrying only key . Watchers of key , configsUpdated handlers, and reads see it at once.
removeValue(key) Removes the value and delivers a full update without it. Reads return the in-code default value with the config-state-missing reason, watchers of key receive the default, and configsUpdated lists key in removedKeys .
replaceValues(values) Replaces every stored value, disarms any armed hold or failure, and delivers a full update. Use it to reset a test client shared across tests.
holdInitialization() The next initialize waits until completeInitialization() or failInitialization() , or until the client's timeout elapses.
completeInitialization() Delivers the stored values to the held initialize , which completes with the client ready. Called while a hold is armed but not picked up, it disarms the hold.
failInitialization() Fails the held initialize the way an invalid SDK key does: it completes promptly, the client is not ready, connectionError fires, and an error is logged. Called with no initialize held, it arms the next one to fail.
holdContextUpdate() , completeContextUpdate() , failContextUpdate() The same for the next updateContext .
contextUpdates The context of every initialize and updateContext call, in order. resumeNetwork is not recorded.
The initialization controls act only on `initialize` and the context update controls only on `updateContext`; `resumeNetwork` is never held or failed. After a failed attempt, the next attempt succeeds and delivers the values stored in the meantime. ```ts const testClient = createTestClient({ values: { "new-checkout": true }, timeout: 500 }); testClient.holdInitialization(); const initialization = testClient.client.initialize(); expect(page.showsLoadingState()).toBe(true); testClient.completeInitialization(); await initialization; expect(page.showsLoadingState()).toBe(false); ``` ### Options `createTestClient` takes `values`, `timeout` (the client's connection timeout in milliseconds, which bounds how long a held attempt waits; defaults to the SDK's production timeout), and `logger`. ### What to expect - Under the test client, `initialize` emits `contextUpdated`, then `configsUpdated`, then `clientReady`. The first update arrives right after the connection is established, so watchers registered before `initialize` run before `isReady` becomes `true`. - A held attempt that times out, or that is interrupted by `pauseNetwork`, `dispose`, or a new attempt, leaves the client not ready until the next attempt; production streaming would keep retrying in the background. - Watchers fire on every update carrying their key, whether or not the value changed, as in production. `removeValue` and `replaceValues` deliver a full update, so they also fire the watchers of every remaining key. - An exception thrown by a watcher during a delivery is logged and does not propagate, as with the production transports. - The `configEvaluated` event is emitted from a `setTimeout(0)`. With fake timers, advance them to receive it. - Creating a client writes an instance ID to `localStorage` when there is none. Under jsdom this is shared by every test in one environment and is harmless. - After `dispose`, the test client's controls are silent no-ops, and nothing from the SDK is left running. # React SDK > Install and configure the React SDK provider, and evaluate configs and feature flags in React components ## Introduction The React SDK is intended to be used by React web applications running on modern web browsers. All modern web browsers on popular platforms should be supported. The minimum supported version of React is `16.8.0`. Browser SDKs are tested on the latest versions of Chrome, Firefox, Safari, and Edge. Telemetry collection within browser SDKs uses Web Workers to aggregate and send telemetry data. If the SDK runs on older browsers without Web Workers support, config evaluation will continue to work but no telemetry data will be collected for that session. ## Installation The SDK can be installed from NPM: [https://www.npmjs.com/package/@configdirector/react-web-sdk](https://www.npmjs.com/package/@configdirector/react-web-sdk) ```bash [npm] npm install --save @configdirector/react-web-sdk ``` ```bash [yarn] yarn add @configdirector/react-web-sdk ``` ```bash [pnpm]Javascript pnpm add @configdirector/react-web-sdk ``` ```bash [bun] bun add @configdirector/react-web-sdk ``` ## Configure and initialize the provider Initialize the provider with your client SDK key: ```tsx [main.tsx] import { createRoot } from "react-dom/client" import App from "./App" import { ConfigDirectorProvider } from "@configdirector/react-web-sdk"; createRoot(document.getElementById("root")!).render( ); ``` Alternatively, you can initialize it with the HOC asynchronously and wait for initialization before starting your application. This can prevent the config values to flicker from the default value to the evaluated value during initialization. An optional timeout can be provided to configure how long to await for the client to initialize before rendering the application. In the event of a timeout, the client will continue to attempt to connect in the background and config values will return the default value provided in code until a connection to ConfigDirector services is established. ```tsx [main.tsx] import { createRoot } from "react-dom/client" import App from "./App" import { withProvider } from "@configdirector/react-web-sdk"; (async () => { const ConfigDirectorProvider = await withProvider({ sdkKey: "YOUR-CLIENT-SDK-KEY", timeout: { 2_000 }, // 2,000 milliseconds timeout, defaults to 3,000 }); createRoot(document.getElementById('root')!).render( ); })(); ``` ### Additional configuration options These configuration options can be passed in to the `ConfigDirectorProvider` or to the `withProvider` HOC as an object in the second argument. #### `context` The context to be used during targeting rules evaluation: ```tsx [main.tsx] import { createRoot } from "react-dom/client"; import App from "./App"; import { ConfigDirectorProvider } from "@configdirector/react-web-sdk"; createRoot(document.getElementById("root")!).render( , ); ``` #### `logger` By default, the SDK logs to the console and it is set to log warnings and errors only. You can configure a `logger` by either creating a ConfigDirector console logger with a different log level, or by implementing the `ConfigDirectorLogger` interface to provide your own logger. The interface can be used to create an adapter to another logging library. Configure the ConfigDirector console logger to a different level: ```tsx [main.tsx] import { createRoot } from "react-dom/client"; import App from "./App"; import { ConfigDirectorProvider, createConsoleLogger } from "@configdirector/react-web-sdk"; createRoot(document.getElementById("root")!).render( , ); ``` Implement your own logger adapter: ```tsx [main.tsx] import { createRoot } from "react-dom/client"; import App from "./App"; import { ConfigDirectorProvider, ConfigDirectorLogger } from "@configdirector/react-web-sdk"; const myLogger: ConfigDirectorLogger = { debug: function (message: string, ...args: any): void { // your specific logging library implementation here }, info: function (message: string, ...args: any): void { // your specific logging library implementation here }, warn: function (message: string, ...args: any): void { // your specific logging library implementation here }, error: function (message: string, ...args: any): void { // your specific logging library implementation here }, }; createRoot(document.getElementById("root")!).render( , ); ``` #### `appName` and `appVersion` These options allow you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application. ```tsx [main.tsx] import { createRoot } from "react-dom/client"; import App from "./App"; import { ConfigDirectorProvider } from "@configdirector/react-web-sdk"; createRoot(document.getElementById("root")!).render( , ); ``` #### `timeout` The timeout, in milliseconds, to be used in initialization and when updating the context. This is how long the `withProvider` HOC will wait for data from ConfigDirector services before resolving its Promise. If the timeout is reached, `withProvider` will return but the client will still be in an unready status and returning default values. The client will continue to attempt to connect and retrieve config values in the background. When using the `ConfigDirectorProvider`, the timeout is not as relevant since initialization will happen asynchronously. ```tsx [main.tsx] import { createRoot } from "react-dom/client" import App from "./App" import { withProvider } from "@configdirector/react-web-sdk"; (async () => { const ConfigDirectorProvider = await withProvider({ sdkKey: "YOUR-CLIENT-SDK-KEY", timeout: 2_000, // 2,000 milliseconds timeout, defaults to 3,000 }); createRoot(document.getElementById('root')!).render( ); })(); ``` #### `mode` The connection mode, which can be `streaming`, `polling` or `offline`. It is recommended to use the default of `streaming` unless you have a specific need to use `polling` instead. Defaults to `streaming`. Set `mode` to `offline` to run your application without connecting to ConfigDirector, for example in a suite of UI tests where each run pins every flag to a known value. An offline provider needs no `sdkKey`, opens no connection and sends nothing, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no lifecycle state changes. Every context receives the same value and targeting rules are not applied. Give the values in the `values` prop, with one value per config key. Your application decides where they come from, for example a module bundled with the test build: ```tsx [main.tsx] import { createRoot } from "react-dom/client"; import App from "./App"; import { ConfigDirectorProvider } from "@configdirector/react-web-sdk"; createRoot(document.getElementById("root")!).render( , ); ``` The config type of each value follows from the value itself: a boolean, an integral number (`integer`), any other number (`float`), a string, or an object or array (`json`). A value read with an in-code default value of another type returns the in-code default value, as it does against ConfigDirector. The status turns `ready` right after the provider mounts, and a new `context` keeps the same values. `values` can be given only in `offline` mode, and `pollingInterval`, `timeout` and `url` have no effect in it. With `mode="offline"` and no `values`, the provider is ready at once and every config returns its in-code default value. When the values cannot be loaded, because of a value such as `null` or because `values` is given in `streaming` or `polling` mode, the client never becomes ready: every config returns its in-code default value, the status stays `loading`, and the provider reports a `ConfigDirectorInitializationError` as an unhandled promise rejection. `withProvider` rejects with the same error. If you come from LaunchDarkly, a map of values is what LaunchDarkly calls "flags from files", and `offline` with no values is its "offline mode". #### `values` The values to serve in `offline` mode, one value per config key. It can be given only in `offline` mode; see [`mode`](#mode). #### `pollingInterval` The interval, in seconds, between requests for config updates when `mode` is `polling`. It has no effect in `streaming` mode. A value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. Defaults to `60` seconds. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). ```tsx [main.tsx] import { createRoot } from "react-dom/client"; import App from "./App"; import { ConfigDirectorProvider } from "@configdirector/react-web-sdk"; createRoot(document.getElementById("root")!).render( , ); ``` #### `url` The base URL used to connect to ConfigDirector services. This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. ### Values kept on the device By default, the client keeps the last values it received from ConfigDirector in the browser's `localStorage`, and serves them on the next launch of the same context until live values arrive. An application opened without a network, after at least one successful connection, shows the values ConfigDirector last sent instead of the default values provided in code. Every evaluation served this way reports the reason `cached`, with `isDefaultValue` set to `false`. The client is not ready while it serves values kept on the device: the status stays `loading` until the provider's initialization completes with live values or its `timeout`, then `default`, and `ready` once live values arrive and replace them. The `cachedValuesLoaded` hook fires when the kept values are loaded, together with `configsUpdated` listing their keys, so watchers and hooks pick them up. The values are kept for the context they were evaluated for, and served on the next launch only when the launch context has the same `kind` and `id`. A launch with another context serves the in-code default values until live values arrive, which then replace the kept values. A context without an `id` matches on the same site, as long as the site's data has not been cleared, because ConfigDirector derives its identifier from the instance ID the SDK stores; a context without an `id` keeps its percentage rollout bucket for as long as that instance ID lasts. Name, traits, app name and app version take no part in the match: live values correct them within a second when there is a network. Once the client is running, updating the context keeps serving the current values until the new context's values arrive, as it does with live values. Turn it off, or supply your own store, with the `caching` prop, which takes the same object the [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript#values-kept-on-the-device) takes: - `enabled` - Whether to keep the last values received. With `false`, nothing is read or written, even when a `provider` is given. - Defaults to `true` - `provider` - A store of your own, for example an encrypted one, implementing `CachingProvider`: `read()` returns the stored entry or `undefined`, `write(value)` replaces it, `remove()` deletes it, and `sizeLimit()` returns the largest entry, in bytes, the store accepts. Each of `read`, `write` and `remove` may return a promise. One provider instance serves one client. - Defaults to the browser's `localStorage`, under a key derived from the client SDK key, with a size limit of 256 KB ```tsx [main.tsx] import { createRoot } from "react-dom/client"; import App from "./App"; import { ConfigDirectorProvider } from "@configdirector/react-web-sdk"; createRoot(document.getElementById("root")!).render( , ); ``` The entry is one string whose format belongs to the SDK and may change between SDK versions; an entry a newer SDK cannot read is removed. Before every write the SDK measures the entry against `sizeLimit()`; above it, the entry is removed and a warning is logged once, so the device never serves values that no longer reflect the set. A provider that fails to read, write or remove is logged once and never stops the client: the worst outcome is the in-code default values. Keeping values on the device is ignored in `offline` mode. The entry, and the `localStorage` key the SDK uses by default, carry a SHA-256 digest of the client SDK key, never the key itself. Browsers compute that digest only on pages served over `https` or from `localhost`, so on a plain `http` page on another host nothing is kept and a `provider` of your own is not called. ## Retrieve config values Retrieve a config value with the `useConfigValue` hook: ```tsx [YourComponent.tsx] import { useConfigValue } from "@configdirector/react-web-sdk"; function YourComponent() { const { value } = useConfigValue("my-config-key", false); return (

my-config-key: {value}

); } export default YourComponent; ``` You can also determine if the client is still initializing. This can be useful in the event of a slow connection where retrieving the initial config values may be slow, so rather than transition from the in-code default value to the evaluated value, you can show a loading state until the client is ready and config values are evaluated: ```tsx [YourComponent.tsx] import { useConfigValue } from "@configdirector/react-web-sdk"; function YourComponent() { const { value, loading } = useConfigValue("my-config", "default value"); if (loading) { return

Loading...

; } else { return

My config value: {value}

; } } export default YourComponent; ``` ## Update the context The browser connects with the context given to the provider's `context` prop. Give it there when you already know the context as the page loads, so the first connection carries it: ```tsx [main.tsx] import { createRoot } from "react-dom/client" import App from "./App" import { ConfigDirectorProvider } from "@configdirector/react-web-sdk"; createRoot(document.getElementById("root")!).render( ); ``` To change the context afterwards, for example when a user signs in, call `updateContext` from the `useContext` hook where the change happens. Each `updateContext` call opens a new connection with the new context: ```tsx [YourComponent.tsx] import { useConfigValue, useContext } from "@configdirector/react-web-sdk"; function YourComponent() { const { value: myConfigValue } = useConfigValue("my-config", "default value"); const { updateContext } = useContext(); const onUpdate = async () => { await updateContext({ id: "654321", name: "Another User", traits: { region: "Australia", }, }); }; return (

My config value: {myConfigValue}

); } export default YourComponent; ``` Rendering the provider with a different `context` prop updates the context the same way. To evaluate configs for an entity other than a user, such as a tenant, set the context kind with `kind`: ```tsx [YourComponent.tsx] updateContext({ id: "acme", kind: "tenant", name: "Acme Inc.", traits: { plan: "enterprise", }, }); ``` Leaving `kind` out means `user`. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given. Context kinds are available from version 1.7.0 of the SDK. In client SDKs (browser and mobile), updating the context re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior context. Give the provider the context you know when the page loads, so the first connection carries it. A context set only afterwards replaces a first connection that had none, and that first connection counts as a separate [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics). The SDK keeps an instance ID in the browser's `localStorage` until the site's data is cleared, and ConfigDirector derives the identifier of a context without an `id` from it. A visitor who has not signed in keeps that identifier, and their percentage rollout bucket, from one visit to the next, and counts as one [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) however often they visit. ## `useClient` hook In the event that you need to have access to the underlying Javascript ConfigDirectorClient instance for more complex behaviors, you can access the instance via the `useClient` hook: ```tsx [YourComponent.tsx] import { useClient } from "@configdirector/react-web-sdk"; function YourComponent() { const { client } = useClient(); // Utilize the client for more involved logic return

A more complex component

; } export default YourComponent; ``` Proceed with caution when using the `useClient` hook. The `useConfigValue` and `useContext` hooks manage listeners and cleanup automatically. However, if you make use of the `useClient` hook, you must manage cleaning up any listeners yourself.
Additionally, any calls to `dispose`, `unwatch`, or `unwatchAll` on the client instance can have unintended side effects and may result in subtle bugs.
### `evaluate` The client's `evaluate(key, defaultValue)` reads a config the way `useConfigValue` does and returns the evaluation behind the value: the value, whether it is the in-code default value, the `reason`, and the `valueId`. The [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript#evaluate) describes it. Use it where the reason matters at the point of the read, such as when reporting an evaluation to another system. It reads once and does not re-render the component when the value changes, so keep rendering with `useConfigValue`. The SDK exports the `ConfigEvaluation` and `EvaluationReason` types, and `evaluate` is available from version 1.10.0. ```tsx [CheckoutButton.tsx] import { useClient } from "@configdirector/react-web-sdk"; function CheckoutButton() { const { client } = useClient(); const startCheckout = () => { const evaluation = client.evaluate("new-checkout", false); analytics.track("checkout-started", { newCheckout: evaluation.value, reason: evaluation.reason }); }; return ; } ``` ## Test your components Use the SDK's testing entry point, `@configdirector/react-web-sdk/testing`, to test components that read your configs and flags. It creates a **test client**: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, including `ConfigDirectorProvider` and the hooks, so your components behave exactly as they do against ConfigDirector, and no network connection is opened and no telemetry is sent. Render the production `ConfigDirectorProvider` with its `client` prop: ```tsx [Checkout.test.tsx] import { act, render, screen } from "@testing-library/react"; import { ConfigDirectorProvider } from "@configdirector/react-web-sdk"; import { createTestClient } from "@configdirector/react-web-sdk/testing"; import { Checkout } from "./Checkout"; test("shows the new checkout when the flag is on", async () => { const testClient = createTestClient({ values: { "new-checkout": true } }); render( , ); expect(await screen.findByText("New checkout")).toBeInTheDocument(); act(() => testClient.setValue("new-checkout", false)); expect(screen.getByText("Classic checkout")).toBeInTheDocument(); }); ``` The provider renders its children before initialization completes, so the first assertion uses `findBy…` rather than `getBy…`. Wrap `setValue`, `removeValue`, and `replaceValues` in `act()`, since they re-render the components under the provider at once. ### Components that render their own provider A component that contains the production provider, such as an `App` that renders ``, would build a real client. `installTestClient` makes every provider mounted without a `client` prop use the test client instead: ```tsx [App.test.tsx] import { createTestClient, installTestClient } from "@configdirector/react-web-sdk/testing"; let testClient: TestClient; let uninstallTestClient: () => void; beforeEach(() => { testClient = createTestClient({ values: { "new-checkout": true } }); uninstallTestClient = installTestClient(testClient); }); afterEach(() => uninstallTestClient()); test("renders the app with the flag on", async () => { render(); expect(await screen.findByText("New checkout")).toBeInTheDocument(); }); ``` While installed, the provider's construction props (`sdkKey`, `url`, `mode`, `values`, `timeout`, `pollingInterval`, `logger`, `appName`, `appVersion`) are ignored; `context` and `hooks` apply to the installed client as they do with a `client` prop. `withProvider` also uses the installed client. A provider reads the installed client when it is constructed, so install before rendering. Installing again replaces the installed test client, and each uninstall function restores the one installed before it. ### What the provider does with a given client - It never disposes a client it was given, whether through the `client` prop or `installTestClient`. It disposes only a client it built from its own props. - It does not call `initialize` on a client that is ready or initializing. If the client is ready and the `context` prop is given and differs from the client's context, it calls `updateContext`. A provider given a ready client reports the `ready` status on its first render. - `hooks` given with a `client` are registered on it while the provider is mounted and removed when it unmounts, like every other handler the provider registers. - The `context` prop is compared by deep equality, on mount and on later renders, so a new object with the same contents does not reconnect. - A `client` prop that changes after the provider mounted is ignored with a warning. Remount the provider, for example with a new `key`, to use another client. ### Test client controls `values`, `setValue`, `removeValue`, and `replaceValues` take native values, and the config type follows from each value: a boolean, an integral number (`integer`), any other number (`float`), a string, or a plain object or array (`json`). `holdInitialization`, `completeInitialization`, and `failInitialization` drive `initialize`, so loading and error states can be tested, and `holdContextUpdate`, `completeContextUpdate`, and `failContextUpdate` do the same for `updateContext`. `contextUpdates` records the context of every `initialize` and `updateContext` call. The [JavaScript SDK's testing section](https://docs.configdirector.com/sdks/browser/javascript#test-your-code) describes every control and what to expect from the client. # Vue SDK > Install and configure the Vue SDK, and evaluate configs and feature flags in Vue components ## Introduction The Vue SDK is intended to be used by Vue web applications running on modern web browsers. All modern web browsers on popular platforms should be supported. The minimum supported version of Vue is `3.2.24`. Browser SDKs are tested on the latest versions of Chrome, Firefox, Safari, and Edge. Telemetry collection within browser SDKs uses Web Workers to aggregate and send telemetry data. If the SDK runs on older browsers without Web Workers support, config evaluation will continue to work but no telemetry data will be collected for that session. ## Installation The SDK can be installed from NPM: [https://www.npmjs.com/package/@configdirector/vue-sdk](https://www.npmjs.com/package/@configdirector/vue-sdk) ```bash [npm] npm install --save @configdirector/vue-sdk ``` ```bash [yarn] yarn add @configdirector/vue-sdk ``` ```bash [pnpm]Javascript pnpm add @configdirector/vue-sdk ``` ```bash [bun] bun add @configdirector/vue-sdk ``` ## Configure the plugin Configure the Vue plugin with your client SDK key. This approach installs the ConfigDirectorPlugin and initializes the client asynchronously while your app is rendering: ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin } from "@configdirector/vue-sdk"; const app = createApp(App); app.use(ConfigDirectorPlugin, { sdkKey: "YOUR-CLIENT-SDK-KEY" }); app.mount("#app"); ``` Alternatively, you can explicitly await on initializing the client before mounting the Vue app. The initialization timeout can be provided in the timeout option in milliseconds (defaults to 3,000ms). If initialization times out, the client will be returned and it will continue to attempt to initialize in the background: ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin, initializeClient } from "@configdirector/vue-sdk"; async function bootstrap() { const app = createApp(App); const client = await initializeClient({ sdkKey: "YOUR_CLIENT_SDK_KEY" }); app.use(ConfigDirectorPlugin, client); app.mount("#app"); } bootstrap(); ``` ### Additional configuration options These configuration options can be passed in as plugin options to the `ConfigDirectorPlugin` or as additional options to `initializeClient`. #### `context` The context to be used during targeting rules evaluation: ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin } from "@configdirector/vue-sdk"; const app = createApp(App); app.use(ConfigDirectorPlugin, { sdkKey: "YOUR-CLIENT-SDK-KEY", context: { id: "12345", name: "Example User", traits: { region: "North America", // Any arbitrary traits which can be referenced in targeting rules }, }, }); app.mount("#app"); ``` #### `logger` By default, the SDK logs to the console and it is set to log warnings and errors only. You can configure a `logger` by either creating a ConfigDirector console logger with a different log level, or by implementing the `ConfigDirectorLogger` interface to provide your own logger. The interface can be used to create an adapter to another logging library. Configure the ConfigDirector console logger to a different level: ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin, createConsoleLogger } from "@configdirector/vue-sdk"; const app = createApp(App); app.use(ConfigDirectorPlugin, { sdkKey: "YOUR-CLIENT-SDK-KEY", logger: createConsoleLogger("debug"), }); app.mount("#app"); ``` Implement your own logger adapter: ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin, ConfigDirectorLogger } from "@configdirector/vue-sdk"; const myLogger: ConfigDirectorLogger = { debug: function (message: string, ...args: any): void { // your specific logging library implementation here }, info: function (message: string, ...args: any): void { // your specific logging library implementation here }, warn: function (message: string, ...args: any): void { // your specific logging library implementation here }, error: function (message: string, ...args: any): void { // your specific logging library implementation here }, }; const app = createApp(App); app.use(ConfigDirectorPlugin, { sdkKey: "YOUR-CLIENT-SDK-KEY", logger: myLogger, }); app.mount("#app"); ``` #### `appName` and `appVersion` These options allow you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application. ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin } from "@configdirector/vue-sdk"; const app = createApp(App); app.use(ConfigDirectorPlugin, { sdkKey: "YOUR-CLIENT-SDK-KEY", appName: "YOUR-APP-NAME", appVersion: "1.0.2", }); app.mount("#app"); ``` #### `timeout` The timeout, in milliseconds, to be used in initialization and when updating the context. This is how long the `initializeClient` function will wait for data from ConfigDirector services before resolving its Promise. If the timeout is reached, `initializeClient` will return but the client will still be in an unready status and returning default values. The client will continue to attempt to connect and retrieve config values in the background. ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin, initializeClient } from "@configdirector/vue-sdk"; async function bootstrap() { const app = createApp(App); const client = await initializeClient({ sdkKey: "YOUR_CLIENT_SDK_KEY", timeout: 2_000, // 2,000 milliseconds timeout, defaults to 3,000 }); app.use(ConfigDirectorPlugin, client); app.mount("#app"); } bootstrap(); ``` #### `mode` The connection mode, which can be `streaming`, `polling` or `offline`. It is recommended to use the default of `streaming` unless you have a specific need to use `polling` instead. Defaults to `streaming`. Set `mode` to `offline` to run your application without connecting to ConfigDirector, for example in a suite of UI tests where each run pins every flag to a known value. An offline client needs no `sdkKey`, opens no connection and sends nothing, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no lifecycle state changes. Every context receives the same value and targeting rules are not applied. Give the values in the `values` option, with one value per config key. Your application decides where they come from, for example a module bundled with the test build: ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin } from "@configdirector/vue-sdk"; const app = createApp(App); app.use(ConfigDirectorPlugin, { mode: "offline", values: { "new-checkout": true, "max-items": 20 }, }); app.mount("#app"); ``` The config type of each value follows from the value itself: a boolean, an integral number (`integer`), any other number (`float`), a string, or an object or array (`json`). A value read with an in-code default value of another type returns the in-code default value, as it does against ConfigDirector. The status turns `ready` right after the plugin is installed, and updating the context keeps the same values. `values` can be given only in `offline` mode, and `pollingInterval`, `timeout` and `url` have no effect in it. `initializeClient` takes the same options. With `mode: "offline"` and no `values`, the client is ready at once and every config returns its in-code default value. When the values cannot be loaded, because of a value such as `null` or because `values` is given in `streaming` or `polling` mode, the client never becomes ready: every config returns its in-code default value, the status stays `loading`, and the plugin reports a `ConfigDirectorInitializationError` as an unhandled promise rejection. `initializeClient` rejects with the same error. If you come from LaunchDarkly, a map of values is what LaunchDarkly calls "flags from files", and `offline` with no values is its "offline mode". #### `values` The values to serve in `offline` mode, one value per config key. It can be given only in `offline` mode; see [`mode`](#mode). #### `pollingInterval` The interval, in seconds, between requests for config updates when `mode` is `polling`. It has no effect in `streaming` mode. A value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. Defaults to `60` seconds. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin } from "@configdirector/vue-sdk"; const app = createApp(App); app.use(ConfigDirectorPlugin, { sdkKey: "YOUR_CLIENT_SDK_KEY", mode: "polling", pollingInterval: 120, }); app.mount("#app"); ``` #### `url` The base URL used to connect to ConfigDirector services. This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. ### Values kept on the device By default, the client keeps the last values it received from ConfigDirector in the browser's `localStorage`, and serves them on the next launch of the same context until live values arrive. An application opened without a network, after at least one successful connection, shows the values ConfigDirector last sent instead of the default values provided in code. Every evaluation served this way reports the reason `cached`, with `isDefaultValue` set to `false`. The client is not ready while it serves values kept on the device: the status stays `loading` until the client's initialization completes with live values or its `timeout`, then `default`, and `ready` once live values arrive and replace them. The `cachedValuesLoaded` hook fires when the kept values are loaded, together with `configsUpdated` listing their keys, so watchers and hooks pick them up. The values are kept for the context they were evaluated for, and served on the next launch only when the launch context has the same `kind` and `id`. A launch with another context serves the in-code default values until live values arrive, which then replace the kept values. A context without an `id` matches on the same site, as long as the site's data has not been cleared, because ConfigDirector derives its identifier from the instance ID the SDK stores; a context without an `id` keeps its percentage rollout bucket for as long as that instance ID lasts. Name, traits, app name and app version take no part in the match: live values correct them within a second when there is a network. Once the client is running, updating the context keeps serving the current values until the new context's values arrive, as it does with live values. Turn it off, or supply your own store, with the `caching` option, which takes the same object the [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript#values-kept-on-the-device) takes: - `enabled` - Whether to keep the last values received. With `false`, nothing is read or written, even when a `provider` is given. - Defaults to `true` - `provider` - A store of your own, for example an encrypted one, implementing `CachingProvider`: `read()` returns the stored entry or `undefined`, `write(value)` replaces it, `remove()` deletes it, and `sizeLimit()` returns the largest entry, in bytes, the store accepts. Each of `read`, `write` and `remove` may return a promise. One provider instance serves one client. - Defaults to the browser's `localStorage`, under a key derived from the client SDK key, with a size limit of 256 KB ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin } from "@configdirector/vue-sdk"; const app = createApp(App); app.use(ConfigDirectorPlugin, { sdkKey: "YOUR_CLIENT_SDK_KEY", caching: { enabled: false }, }); app.mount("#app"); ``` The entry is one string whose format belongs to the SDK and may change between SDK versions; an entry a newer SDK cannot read is removed. Before every write the SDK measures the entry against `sizeLimit()`; above it, the entry is removed and a warning is logged once, so the device never serves values that no longer reflect the set. A provider that fails to read, write or remove is logged once and never stops the client: the worst outcome is the in-code default values. Keeping values on the device is ignored in `offline` mode. The entry, and the `localStorage` key the SDK uses by default, carry a SHA-256 digest of the client SDK key, never the key itself. Browsers compute that digest only on pages served over `https` or from `localhost`, so on a plain `http` page on another host nothing is kept and a `provider` of your own is not called. ## Retrieve config values Retrieve a config value with the `useConfigValue` composable: ```vue [YourComponent.vue] ``` You can also determine if the client is still initializing, so rather than transition from the default value to the evaluated value, you can show a loading state until the client is ready and config values are evaluated: ```vue [YourComponent.vue] ``` Alternatively, you can also use the `useClientStatus` composable to retrieve just the status: ```vue [YourComponent.vue] ``` ## Update the context A context can be provided when initializing the plugin: ```ts [main.ts] import { createApp } from "vue"; import App from "./App.vue"; import { ConfigDirectorPlugin } from "@configdirector/vue-sdk"; const app = createApp(App); app.use(ConfigDirectorPlugin, { sdkKey: "YOUR-CLIENT-SDK-KEY", context: { id: "12345", name: "Example User", traits: { region: "North America", // Any arbitrary traits which can be referenced in targeting rules }, }, }); app.mount("#app"); ``` The context can also be updated via the `useContext` composable: ```vue [YourComponent.vue] ``` To evaluate configs for an entity other than a user, such as a tenant, set the context kind with `kind`: ```ts updateContext({ id: "acme", kind: "tenant", name: "Acme Inc.", traits: { plan: "enterprise", }, }); ``` Leaving `kind` out means `user`. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given. Context kinds are available from version 1.7.0 of the SDK. In client SDKs (browser and mobile), updating the context re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior context. The SDK keeps an instance ID in the browser's `localStorage` until the site's data is cleared, and ConfigDirector derives the identifier of a context without an `id` from it. A visitor who has not signed in keeps that identifier, and their percentage rollout bucket, from one visit to the next, and counts as one [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) however often they visit. ## `useClient` composable In the event that you need to have access to the underlying Javascript ConfigDirectorClient instance for more complex behaviors, you can access the instance via the `useClient` composable: ```vue [YourComponent.vue] ``` Proceed with caution when using the `useClient` composable. The `useConfigValue` and `useContext` composables manage listeners and cleanup automatically. However, if you make use of the `useClient` composable, you must manage cleaning up any listeners yourself.
Additionally, any calls to `dispose`, `unwatch`, or `unwatchAll` on the client instance can have unintended side effects and may result in subtle bugs.
### `evaluate` The client's `evaluate(key, defaultValue)` reads a config the way `useConfigValue` does and returns the evaluation behind the value: the value, whether it is the in-code default value, the `reason`, and the `valueId`. The [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript#evaluate) describes it. Use it where the reason matters at the point of the read, such as when reporting an evaluation to another system. It reads once and does not update when the value changes, so keep rendering with `useConfigValue`. The SDK exports the `ConfigEvaluation` and `EvaluationReason` types, and `evaluate` is available from version 1.10.0. ```vue [CheckoutButton.vue] ``` ## Test your components Use the SDK's testing entry point, `@configdirector/vue-sdk/testing`, to test components that read your configs and flags. It creates a **test client**: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, including the plugin and the composables, so your components behave exactly as they do against ConfigDirector, and no network connection is opened and no telemetry is sent. Install the production plugin with the test client's client, the same way you would install it with a pre-initialized client: ```ts [Checkout.test.ts] import { nextTick } from "vue"; import { render, screen } from "@testing-library/vue"; import { ConfigDirectorPlugin } from "@configdirector/vue-sdk"; import { createTestClient } from "@configdirector/vue-sdk/testing"; import Checkout from "./Checkout.vue"; test("shows the new checkout when the flag is on", async () => { const testClient = createTestClient({ values: { "new-checkout": true } }); render(Checkout, { global: { plugins: [[ConfigDirectorPlugin, testClient.client]] } }); await screen.findByText("New checkout"); testClient.setValue("new-checkout", false); await nextTick(); screen.getByText("Classic checkout"); }); ``` The plugin calls `initialize` on a client that is neither ready nor initializing, so the first assertion uses `findBy…` rather than `getBy…`; a test client whose client was initialized before `app.use` is ready on the first render, as with `initializeClient`. Await `nextTick()` after `setValue`, `removeValue`, and `replaceValues`, since they update the composables' refs synchronously and Vue renders on the next tick. The plugin's `clientReady` and `contextUpdated` handlers stay registered for the app's lifetime, as in production. ### Test client controls `values`, `setValue`, `removeValue`, and `replaceValues` take native values, and the config type follows from each value: a boolean, an integral number (`integer`), any other number (`float`), a string, or a plain object or array (`json`). `holdInitialization`, `completeInitialization`, and `failInitialization` drive `initialize`, so the `loading` and `default` statuses can be tested, and `holdContextUpdate`, `completeContextUpdate`, and `failContextUpdate` do the same for `updateContext`. `contextUpdates` records the context of every `initialize` and `updateContext` call. The [JavaScript SDK's testing section](https://docs.configdirector.com/sdks/browser/javascript#test-your-code) describes every control and what to expect from the client. # Maven Repository > Add the ConfigDirector Maven repository to a Gradle or Maven build to install the Android and Java SDKs, and verify the files it serves The ConfigDirector Android and Java SDKs, and the OpenFeature providers built on them, are published to the ConfigDirector Maven repository: ```txt https://maven.configdirector.com ``` Anyone can download from it, without an account or credentials. Once a version is published there it never changes: its files are never replaced or deleted. ## Artifacts
Artifact Contents
com.configdirector:android-sdk The Android SDK
com.configdirector:android-sdk-compose Jetpack Compose bindings for the Android SDK
com.configdirector:android-sdk-testing Tools for testing code that uses the Android SDK
com.configdirector:openfeature-android-provider The OpenFeature Android provider
com.configdirector:server-sdk The Java server SDK
com.configdirector:server-sdk-testing Tools for testing code that uses the Java server SDK
com.configdirector:openfeature-server-provider The OpenFeature Java provider
### Older versions on Maven Central Versions up to Android SDK 1.6.0, OpenFeature Android provider 1.2.0, Java server SDK 1.7.0 and OpenFeature Java provider 1.4.0 were published to Maven Central under older names. Newer versions are only in this repository.
Name on Maven Central Name in this repository
configdirector-android android-sdk
configdirector-android-compose android-sdk-compose
configdirector-android-testing android-sdk-testing
configdirector-openfeature-android-provider openfeature-android-provider
configdirector-server-sdk server-sdk
configdirector-server-sdk-testing server-sdk-testing
configdirector-openfeature-server-provider openfeature-server-provider
To upgrade from a version on Maven Central, add the repository to your build and change the artifact names. Change all of them at once: an artifact under its old name and the same artifact under its new name contain the same classes, so they cannot be on the same classpath. ## Add the repository to your build ### Gradle If your build declares its repositories in `settings.gradle.kts` or `settings.gradle`, under `dependencyResolutionManagement`, add the repository there. Android Studio sets up new projects this way, and their builds fail if a module's build file declares a repository. ```kotlin [settings.gradle.kts] dependencyResolutionManagement { repositories { google() mavenCentral() exclusiveContent { forRepository { maven { name = "ConfigDirector" url = uri("https://maven.configdirector.com") } } filter { includeGroup("com.configdirector") } } } } ``` ```groovy [settings.gradle] dependencyResolutionManagement { repositories { google() mavenCentral() exclusiveContent { forRepository { maven { name = 'ConfigDirector' url = 'https://maven.configdirector.com' } } filter { includeGroup 'com.configdirector' } } } } ``` Otherwise, add it to the `repositories` block of your build file: ```kotlin [build.gradle.kts] repositories { mavenCentral() exclusiveContent { forRepository { maven { name = "ConfigDirector" url = uri("https://maven.configdirector.com") } } filter { includeGroup("com.configdirector") } } } ``` ```groovy [build.gradle] repositories { mavenCentral() exclusiveContent { forRepository { maven { name = 'ConfigDirector' url = 'https://maven.configdirector.com' } } filter { includeGroup 'com.configdirector' } } } ``` With `exclusiveContent`, Gradle looks for ConfigDirector artifacts only in this repository, and looks for nothing else there. Keep the other repositories your build already lists. ### Maven Add the repository to `pom.xml`: ```xml [pom.xml] configdirector https://maven.configdirector.com true false ``` ## Dependabot On GitHub.com, `.github/dependabot.yml` needs no registry entry for this repository. Dependabot finds the repository in `pom.xml`, or in your Gradle settings file or top-level build file, and opens pull requests for new versions of the SDKs. Add the registry entry, without credentials, if your repositories are on GitHub Enterprise Server, or if your Gradle build adds this repository only in a module's build file or in a convention plugin: ```yaml [.github/dependabot.yml] version: 2 registries: configdirector: type: maven-repository url: https://maven.configdirector.com updates: - package-ecosystem: "gradle" directory: "/" registries: - configdirector schedule: interval: "weekly" ``` For a Maven build, the `package-ecosystem` is `"maven"`. If the file already has `updates` entries, add `configdirector` to the `registries` of each entry that covers a build using the SDKs. If `.github/dependabot.yml` already has a `configdirector` registry and neither case applies, you can remove it, along with `configdirector` in the `registries` of each `updates` entry. ## Company repository managers If your builds download their dependencies through a repository manager such as Sonatype Nexus Repository or JFrog Artifactory, rather than from public repositories directly, its administrator adds this repository once: 1. Create a Maven proxy repository (a remote repository in Artifactory) with `https://maven.configdirector.com` as its URL. It only serves release versions and needs no credentials. 2. Limit it to the `com.configdirector` group: in Nexus with a routing rule that allows `^/com/configdirector/.*`, in Artifactory with the include pattern `com/configdirector/**`. 3. Add it to the group repository (the virtual repository in Artifactory) that your builds use. Your builds keep listing only the repository manager, and get the SDKs through it like any other dependency. ## Verify what you download Every jar, AAR and POM in the repository is signed with the ConfigDirector signing key. The signature is the `.asc` file next to each one. - Fingerprint: `779A DCFB 334D A3FD 2E00 5AB4 3CD4 0757 E05C 6B4C` - User ID: `ConfigDirector ` - Expires: 2028-08-20 The key is published on keys.openpgp.org and keyserver.ubuntu.com, and in full in the `KEYS.md` file of the [Android SDK](https://github.com/ConfigDirector/android-sdk/blob/main/KEYS.md) and [Java SDK](https://github.com/ConfigDirector/java-sdks/blob/main/KEYS.md) repositories. ### Gradle dependency verification If your build verifies signatures with [Gradle's dependency verification](https://docs.gradle.org/current/userguide/dependency_verification.html), trust the key for the `com.configdirector` group by adding this entry to `` in `gradle/verification-metadata.xml`. Gradle downloads the key from the key servers above. ```xml ``` ### gpg Download a file and its signature, and check one against the other: ```bash gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 779ADCFB334DA3FD2E005AB43CD40757E05C6B4C curl -O https://maven.configdirector.com/com/configdirector/server-sdk/1.10.0/server-sdk-1.10.0.jar curl -O https://maven.configdirector.com/com/configdirector/server-sdk/1.10.0/server-sdk-1.10.0.jar.asc gpg --verify server-sdk-1.10.0.jar.asc server-sdk-1.10.0.jar ``` Check that gpg reports a good signature and shows the fingerprint above. ### GitHub artifact attestations Every jar, AAR and POM also has a GitHub artifact attestation, which records the SDK's source repository on GitHub and the release workflow run that built the file. Check a downloaded file with the [GitHub CLI](https://cli.github.com): ```bash gh attestation verify server-sdk-1.10.0.jar --owner ConfigDirector ``` The command succeeds only when the file was built by a workflow in a ConfigDirector repository on GitHub. # Next.js SDK > Evaluate configs and feature flags in the browser, in server routes, and during SSR with the Next.js SDK ## Introduction The Next.js SDK combines a client and a server SDK to provide configs to the browser, to server routes, and to SSR. It is intended to be used by Next.js web applications running on modern web browsers. All modern web browsers on popular platforms should be supported. The minimum supported version of Next.js is `14.0`. Browser SDKs are tested on the latest versions of Chrome, Firefox, Safari, and Edge. Telemetry collection within the browser uses Web Workers to aggregate and send telemetry data. If the SDK runs on older browsers without Web Workers support, config evaluation will continue to work but no telemetry data will be collected for that session. ## Installation The SDK can be installed from NPM: [https://www.npmjs.com/package/@configdirector/nextjs-sdk](https://www.npmjs.com/package/@configdirector/nextjs-sdk) ```bash [npm] npm install --save @configdirector/nextjs-sdk ``` ```bash [yarn] yarn add @configdirector/nextjs-sdk ``` ```bash [pnpm]Javascript pnpm add @configdirector/nextjs-sdk ``` ```bash [bun] bun add @configdirector/nextjs-sdk ``` ## Configure the SDK ### Set up the server instance to initialize at startup The Next.js SDK maintains a singleton server SDK instance used in server routes and server side rendering (SSR). In order to register this instance at startup, you can make use of the Next.js `instrumentation.ts` file: ```ts [instrumentation.ts] export async function register() { const { register } = await import("@configdirector/nextjs-sdk/server"); await register({ // The server SDK key is a secret value, do not commit it to your source code repository. // In this example, we are assuming the environment variable CONFIGDIRECTOR_SERVER_SDK_KEY // will be populated at runtime with your server SDK key in the server environment serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]!, }); } ``` If you are using a version of Next.js older than *15*, instrumentation is an experimental feature that must be enabled explicitly: ```js [next.config.js] module.exports = { experimental: { instrumentationHook: true, }, }; ``` ### Configure the client provider The ConfigDirector hooks are only available within a `ConfigDirectorProvider`. The simplest approach is to wrap your application in a `ConfigDirectorProvider` in your layout. There are a couple of options for this. Using a dynamic layout and a server-side environment variable for the client key: ```tsx [app/layout.tsx] import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server"; import type { ReactNode } from "react"; export const dynamic = "force-dynamic"; export default async function RootLayout({ children }: { children: ReactNode }) { return (
{children}
); } ``` `force-dynamic` makes the layout read the environment variable on every request, so one build can be deployed to several environments. The trade-off is that the pages lose static generation. If you build once per environment instead, you can leave `force-dynamic` out: the environment variable set at build time is baked into that build's static output. Do not use a `NEXT_PUBLIC_` environment variable for the SDK key, even in a static layout. Next.js replaces every `NEXT_PUBLIC_` reference with its build-time value, which freezes the key into the build and exposes it to all browser code. A plain environment variable reaches the browser only through the provider. Additionally, you can configure the provider with a context. In this example, we retrieve the userId from a cookie: ```tsx [app/layout.tsx] import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server"; import type { ReactNode } from "react"; import { cookies } from "next/headers"; export const dynamic = "force-dynamic"; export default async function RootLayout({ children }: { children: ReactNode }) { const userId = (await cookies()).get("userId")?.value; return (
{children}
); } ``` ### Additional configuration options #### `appName` and `appVersion` These `metadata` options allow you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application. ```ts [instrumentation.ts] export async function register() { const { register } = await import("@configdirector/nextjs-sdk/server"); await register({ serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]!, metadata: { appName: "YOUR-APP-NAME", appVersion: "1.0.2", }, }); } ``` The `appName` and `appVersion` are automatically propagated to the client SDK instance via the `ConfigDirectorProvider` RSC. However, if needed they can be overridden to a different value when configuring the provider as well: ```tsx [app/layout.tsx] import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server"; import type { ReactNode } from "react"; export default async function RootLayout({ children }: { children: ReactNode }) { return (
{children}
); } ``` #### `logger` (server only) By default, the SDK on the server side logs to the console and it is set to log warnings and errors only. You can configure a `logger` by either creating a ConfigDirector console logger with a different log level, or by implementing the `ConfigDirectorLogger` interface to provide your own logger. The interface can be used to create an adapter to another logging library. Configure the ConfigDirector console logger to a different level: ```ts [instrumentation.ts] export async function register() { const { register, createConsoleLogger } = await import("@configdirector/nextjs-sdk/server"); await register({ serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]!, logger: createConsoleLogger("info"), }); } ``` Implement your own logger adapter: ```ts [instrumentation.ts] import type { ConfigDirectorLogger } from "@configdirector/nextjs-sdk/server"; export async function register() { const { register } = await import("@configdirector/nextjs-sdk/server"); const myLogger: ConfigDirectorLogger = { debug: function (message: string, ...args: any): void { // your specific logging library implementation here }, info: function (message: string, ...args: any): void { // your specific logging library implementation here }, warn: function (message: string, ...args: any): void { // your specific logging library implementation here }, error: function (message: string, ...args: any): void { // your specific logging library implementation here }, }; await register({ serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]!, logger: myLogger, }); } ``` #### `logLevel` (client only) The SDK on the client side logs to the browser console. You can adjust the ConfigDirector's SDK logging level for the client side via the `logLevel` option (defaults to `warn`): ```tsx [app/layout.tsx] import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server"; import type { ReactNode } from "react"; export default async function RootLayout({ children }: { children: ReactNode }) { return (
{children}
); } ``` #### `mode`, `pollingInterval` and `values` (client only) The connection mode of the client-side provider, one of `streaming`, `polling` or `offline`. It is recommended to use the default of `streaming` unless you have a specific need to use `polling` instead. `pollingInterval` is the interval, in seconds, between requests for config updates when `mode` is `polling`, and has no effect in the other modes. It defaults to `60` seconds, and a value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). In `offline` mode the browser never connects to ConfigDirector and serves the `values` you give it, one value per config key; see [Run without a connection](#run-without-a-connection). The browser half keeps the last values it received on the device by default and serves them on the next page load of the same context until live values arrive, as the [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript#values-kept-on-the-device) does. ```tsx [app/layout.tsx] import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server"; import type { ReactNode } from "react"; export default async function RootLayout({ children }: { children: ReactNode }) { return (
{children}
); } ``` #### `deferInitialization` (client only) By default, the browser connects to ConfigDirector as soon as the page has rendered, with the context the provider has at that point: the `context` prop, or one given through `updateContext` while the page first renders. An application that learns its context only after an asynchronous step in the browser, such as client-side authentication, therefore opens a first connection with no context and a second one once it calls `updateContext`. The visitor of that first connection counts as a [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) in addition to the signed-in user, and the values kept on the device for the signed-in user are not served until the second connection opens. Turn on `deferInitialization` to make the browser wait for its first context instead: ```tsx [app/layout.tsx] import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/server"; import type { ReactNode } from "react"; export default async function RootLayout({ children }: { children: ReactNode }) { return (
{children}
); } ``` With the prop on, nothing connects and nothing is sent until the provider is given a context, through the `context` prop or an `updateContext` call. Until then, `useConfigDirectorStatus` reports `loading` and the hooks return the values the page was rendered with on the server, or the in-code default values. The first context opens one connection that carries it: the values kept on the device for that context are served until live values arrive, and the status becomes `ready`, or `default` when the connection `timeout` passes without values, as at startup without the prop. Later contexts update the context as usual. A context given while the page first renders, through the prop or an effect that runs when a component mounts, starts the browser right away, as it does without the prop. A page that never gives a context never connects and stays `loading`, showing the server-rendered values or the in-code default values, so turn the prop on only when every page that reads configs gives a context. The prop has no effect in `offline` mode, where the browser is ready at once with its values. #### Additional server configuration The `register` function accepts the same configuration values as the Node.js server SDK. You can [refer to the Node.js SDK](https://docs.configdirector.com/sdks/server/node-js#additional-configuration-options) for a full list of those configuration options. The server instance uses the server SDK polling defaults: `connection.pollingInterval` defaults to `300` seconds with a minimum of `60` seconds. `serverSdkKey` is required unless the connection `mode` is `offline`. ### Run without a connection Set the connection `mode` to `offline` to run your application without connecting to ConfigDirector, for example in a matrix of UI tests where each cell pins every flag to a known value. Neither the server nor the browser needs an SDK key, no connection is opened and nothing is sent, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no lifecycle state changes. Every context receives the same value and targeting rules are not applied. Give the values once, to the server half, in a JSON file with one value per config key. Your application decides where the path comes from; here `instrumentation.ts` reads an environment variable of the application's own: ```ts [instrumentation.ts] export async function register() { const { register } = await import("@configdirector/nextjs-sdk/server"); const file = process.env["CONFIGDIRECTOR_OFFLINE_FILE"]; await register( file ? { connection: { mode: "offline", file } } : { serverSdkKey: process.env["CONFIGDIRECTOR_SERVER_SDK_KEY"]! }, ); } ``` ```json [flags/new-checkout-on.json] { "new-checkout": true, "max-items": 20, "greeting": "hello", "theme": { "color": "blue" } } ``` ```sh CONFIGDIRECTOR_OFFLINE_FILE=./flags/new-checkout-on.json next start ``` The file path is resolved against the process working directory, and the values can also be given inline as `connection.values` instead of a file. The config type of each value follows from the value itself: a boolean, an integral number (`integer`), any other number (`float`), a string, or an object or array (`json`). A value read with an in-code default value of another type returns the in-code default value, as it does against ConfigDirector. The layout does not change between a live run and an offline run. When `register()` ran in `offline` mode, the server `ConfigDirectorProvider` hands the server's values to the browser half, which serves them in `offline` mode too, so server-rendered pages, hydration and the browser show the same values, and `sdkKey` can be left out. Give the provider its own `mode` and `values` props to override this; then the browser half serves those instead, whatever mode the server runs in, and the server half serves its own, with no check that the two agree. An application that renders the client `ConfigDirectorProvider` without the server one gives it `mode="offline"` and `values` itself. With `mode: "offline"` and neither `file` nor `values`, the server is ready at once and every config returns its in-code default value on both halves. `register()` rejects with a `ConfigDirectorInitializationError`, which the SDK exports, when the values cannot be loaded: the file is missing, cannot be read, is not a JSON object or holds a value such as `null`; `file` and `values` are both given; or either is given in `streaming` or `polling` mode. The server does not start, so a test cell with a misspelled path fails instead of passing on in-code default values. `useConfigDirectorStatus` reports `loading` during server rendering and hydration and `ready` as soon as the provider mounts. `pollingInterval`, `timeout` and `url` have no effect. A page prerendered at build time carries the values present at build time. For a test matrix, either build once per cell or keep `force-dynamic` on the layout so every request reads the running server's values. If you come from LaunchDarkly, a file of values is what LaunchDarkly calls "flags from files", and `offline` with no values is its "offline mode". ## Retrieve config values In your client components, retrieve a config value with the `useConfigValue` hook. On the server side, `getConfigClient` returns the global server instance of the client (an instance of the [Node.js SDK](https://docs.configdirector.com/sdks/server/node-js)). Retrieving config values in client components: ```tsx [YourComponent.tsx] "use client"; import { useConfigValue } from "@configdirector/nextjs-sdk/client"; function YourComponent() { const { value } = useConfigValue("my-config-key", false); return (

my-config-key: {value}

); } export default YourComponent; ``` Retrieving config values in server routes: ```ts [route.ts] import { getConfigClient } from "@configdirector/nextjs-sdk/server"; export async function GET() { const client = getConfigClient(); const value = client.getValue("my-config-key", false); return Response.json({ "my-config-key": value }); } ``` ### `evaluate` Both halves can return the evaluation behind a value instead of the value alone: the value, whether it is the in-code default value, the `reason`, and the `valueId`. In client components, the client from the `useClient` hook of `@configdirector/nextjs-sdk/client` has `evaluate(key, defaultValue)`, as described for the [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript#evaluate). The browser client exists only once the provider has mounted in the browser, and `useClient` throws before that, including during server rendering, so render a component that calls it only when `useConfigDirectorStatus` no longer reports `loading`. On the server, the client from `getConfigClient` has `evaluate(key, defaultValue, context)`, with the context optional as in `getValue`, as described for the [Node.js SDK](https://docs.configdirector.com/sdks/server/node-js#evaluate). Use it where the reason matters at the point of the read, such as when reporting an evaluation to another system. Both entry points export the `ConfigEvaluation` and `EvaluationReason` types, and `evaluate` is available from version 1.13.0 of the SDK. ```ts [route.ts] import { getConfigClient } from "@configdirector/nextjs-sdk/server"; export async function GET() { const evaluation = getConfigClient().evaluate("new-checkout", false, { id: "user-id" }); return Response.json({ newCheckout: evaluation.value, reason: evaluation.reason }); } ``` ### SSR config evaluation Evaluation of config values during server side rendering (SSR) utilizes the global *server* instance of the ConfigDirector Node.js client. That means there are no additional network requests that take place since the server SDK evaluates targeting rules locally for any given context. **For SSR to work correctly, configs need to be made available to both, the client and the server, in the ConfigDirector dashboard.** ## Update the context The browser connects with the context given to the provider's `context` prop. Give it there when the page loads, for example from a cookie read in the root layout as shown in [Configure the client provider](https://docs.configdirector.com/sdks/meta/nextjs#configure-the-client-provider), so the first connection carries it. To change the context afterwards, for example when a user signs in, call `updateContext` from the `useContext` hook where the change happens: ```tsx [YourComponent.tsx] "use client"; import { useConfigValue, useContext } from "@configdirector/nextjs-sdk/client"; function YourComponent() { const { value } = useConfigValue("my-config-key", false); const { updateContext } = useContext(); const onUpdate = async () => { await updateContext({ id: "654321", name: "Another User", traits: { region: "Australia", }, }); }; return (

my-config-key: {String(value)}

); } export default YourComponent; ``` Rendering the provider with a different `context` prop updates the context the same way. A context given while the page first renders goes out with the first connection, whether it comes from the provider's `context` prop or from an `updateContext` call in an effect that runs when a component mounts: the browser connects once the page's first render and its effects are done. `updateContext` is the same function on every render, so it is safe to list in an effect's dependencies. A context equal to the one the provider already has, given through the prop or through `updateContext`, opens no new connection. On server routes, the context is provided at evaluation time as the third argument to `getValue`. Unlike client SDKs, the server SDKs are able to evaluate targeting rules for the given context locally without additional network calls. ```ts [route.ts] import { getConfigClient } from "@configdirector/nextjs-sdk/server"; export async function GET() { const client = getConfigClient(); const value = client.getValue("my-config-key", false, { id: "654321", name: "Another User", traits: { region: "Australia", }, }); return Response.json({ "my-config-key": value }); } ``` To evaluate configs for an entity other than a user, such as a tenant, set the context kind with `kind`. It works the same in `updateContext` and in `getValue`: ```ts [route.ts] const value = client.getValue("my-config-key", false, { id: "acme", kind: "tenant", name: "Acme Inc.", traits: { plan: "enterprise", }, }); ``` Leaving `kind` out means `user`. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given. Context kinds are available from version 1.10.0 of the SDK. Updating the context on the client side re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior context. Give the provider the context you know when the page loads, so the first connection carries it. A context set only afterwards replaces a first connection that had none, and that first connection counts as a separate [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics). If your application learns its context only after an asynchronous step in the browser, turn on [`deferInitialization`](#deferinitialization-client-only) so that the browser waits for the first `updateContext` call and its first connection already carries the context. In the browser, the SDK keeps an instance ID in `localStorage` until the site's data is cleared, and ConfigDirector derives the identifier of a context without an `id` from it. A visitor who has not signed in keeps that identifier, and their percentage rollout bucket, from one visit to the next, and counts as one [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) however often they visit. ## Test your code The SDK ships two testing entry points. `@configdirector/nextjs-sdk/client/testing` is for Client Components that read your configs and flags, and `@configdirector/nextjs-sdk/server/testing` is for server code that uses `getConfigClient()`, `generateSsrConfigSet()`, or the server `ConfigDirectorProvider`. Each creates a **test client**: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, so the code under test behaves exactly as it does against ConfigDirector, and no network connection is opened and no telemetry is sent. ### Client Components Render the production client `ConfigDirectorProvider` with its `client` prop: ```tsx [Checkout.test.tsx] import { act, render, screen } from "@testing-library/react"; import { ConfigDirectorProvider } from "@configdirector/nextjs-sdk/client"; import { createTestClient } from "@configdirector/nextjs-sdk/client/testing"; import { Checkout } from "./Checkout"; test("shows the new checkout when the flag is on", async () => { const testClient = createTestClient({ values: { "new-checkout": true } }); render( , ); expect(await screen.findByText("New checkout")).toBeInTheDocument(); act(() => testClient.setValue("new-checkout", false)); expect(screen.getByText("Classic checkout")).toBeInTheDocument(); }); ``` Until the client is ready, the hooks show `initialConfigs` when the provider was given them, or the in-code default value, so the first assertion uses `findBy…` rather than `getBy…`; a test client whose client was initialized before rendering is ready on the first render. Wrap `setValue`, `removeValue`, and `replaceValues` in `act()`. For a component that renders the client provider itself with an `sdkKey`, `installTestClient` makes every provider mounted without a `client` prop use the test client instead, until the returned function uninstalls it: ```tsx [App.test.tsx] import { createTestClient, installTestClient } from "@configdirector/nextjs-sdk/client/testing"; let uninstallTestClient: () => void; beforeEach(() => { uninstallTestClient = installTestClient(createTestClient({ values: { "new-checkout": true } })); }); afterEach(() => uninstallTestClient()); ``` While installed, the provider's construction props (`sdkKey`, `url`, `mode`, `timeout`, `pollingInterval`, `logLevel`, `appName`, `appVersion`) are ignored; `context`, `hooks`, and `initialConfigs` apply as they do with a `client` prop. A provider given a client never disposes it, removes its handlers when it unmounts, does not call `initialize` on a client that is ready or initializing, updates the context only when the `context` prop is given and differs from the client's context, compares contexts by deep equality, and ignores a `client` prop that changes after mount with a warning. With `deferInitialization` on, it leaves a client that is neither ready nor initializing untouched until the first context, from the prop or from `updateContext`, initializes it. ### Server code Server code reads the singleton that `register()` creates in `instrumentation.ts` through `getConfigClient()`. `register()` never runs in unit tests, so `installServerTestClient` puts a server test client's client in its place and returns a function that restores the previous one. It does not initialize the client; the test does, as `register()` would: ```ts [flags.test.ts] import { getConfigClient } from "@configdirector/nextjs-sdk/server"; import { createTestClient, installServerTestClient } from "@configdirector/nextjs-sdk/server/testing"; let uninstallServerTestClient: () => void; beforeEach(async () => { const testClient = createTestClient({ values: { "new-checkout": true } }); uninstallServerTestClient = installServerTestClient(testClient); await testClient.client.initialize(); }); afterEach(() => uninstallServerTestClient()); test("serves the flag to every user", () => { expect(getConfigClient().getValue("new-checkout", false, { id: "user-1" })).toBe(true); }); ``` `generateSsrConfigSet()` and the server `ConfigDirectorProvider` then serve the test client's values. The server provider is an async Server Component, which React Testing Library cannot render; call it as an async function and assert on the `initialConfigs` prop of the element it returns. Every value is served as an unconditional config, so every context receives the same value. ### Test client controls `values`, `setValue`, `removeValue`, and `replaceValues` take native values, and the config type follows from each value: a boolean, an integral number (`integer`), any other number (`float`), a string, or a plain object or array (`json`). `holdInitialization`, `completeInitialization`, and `failInitialization` drive `initialize`; the client test client also holds, completes, or fails `updateContext` and records every context in `contextUpdates`. The [JavaScript SDK's testing section](https://docs.configdirector.com/sdks/browser/javascript#test-your-code) and the [Node.js SDK's testing section](https://docs.configdirector.com/sdks/server/node-js#test-your-code) describe every control and what to expect from each client. # Nuxt SDK > Install the Nuxt module and evaluate configs and feature flags in the browser, in server handlers, and during SSR ## Introduction The Nuxt SDK combines a client and a server SDK to provide configs to the browser, to server resource handlers, and to SSR. It is intended to be used by Nuxt web applications running on modern web browsers. All modern web browsers on popular platforms should be supported. The minimum supported version of Nuxt is `3.7.0`. Browser SDKs are tested on the latest versions of Chrome, Firefox, Safari, and Edge. Telemetry collection within the browser uses Web Workers to aggregate and send telemetry data. If the SDK runs on older browsers without Web Workers support, config evaluation will continue to work but no telemetry data will be collected for that session. ## Installation The SDK can be installed from NPM: [https://www.npmjs.com/package/@configdirector/nuxt-sdk](https://www.npmjs.com/package/@configdirector/nuxt-sdk) ```bash [npm] npm install --save @configdirector/nuxt-sdk ``` ```bash [yarn] yarn add @configdirector/nuxt-sdk ``` ```bash [pnpm]Javascript pnpm add @configdirector/nuxt-sdk ``` ```bash [bun] bun add @configdirector/nuxt-sdk ``` ## Configure the Nuxt module The Nuxt SDK requires the client SDK key for evaluation in the browser, and the server SDK key for server side rendering (SSR) and for server routes. Neither key is needed when the application [runs without a connection](#run-without-a-connection). You can provide the keys in `nuxt.config.ts`: ```ts [nuxt.config.ts] export default defineNuxtConfig({ //... modules: ["@configdirector/nuxt-sdk"], runtimeConfig: { public: { configdirector: { // You can also provide the key at runtime to match the given environment // via the NUXT_PUBLIC_CONFIGDIRECTOR_CLIENT_SDK_KEY environment variable clientSdkKey: "YOUR-CLIENT-SDK-KEY", }, }, configdirector: { // IMPORTANT: This is a secret, do not commit to your source repository // You can provide the server key at runtime via the // NUXT_CONFIGDIRECTOR_SERVER_SDK_KEY environment variable serverSdkKey: "YOUR-SERVER-SDK-KEY", }, }, }); ``` Since the server SDK key is a secret value, the recommended approach is to provide it via an environment variable: ```bash export NUXT_CONFIGDIRECTOR_SERVER_SDK_KEY=YOUR-SERVER-SDK-KEY ``` The client key can also be provided via an environment variable, which overrides the key provided in: `nuxt.config.ts`: ```bash export NUXT_PUBLIC_CONFIGDIRECTOR_CLIENT_SDK_KEY=YOUR-CLIENT-SDK-KEY ``` ### Additional configuration options #### `appName` and `appVersion` These options allow you to provide your application's name and version. These values can be used in targeting rules conditionals, both in the browser and on the server, during SSR and in server routes. For example, if a certain feature should only be enabled starting with a certain version of your application. ```ts [nuxt.config.ts] export default defineNuxtConfig({ //... modules: ["@configdirector/nuxt-sdk"], runtimeConfig: { public: { configdirector: { appName: "YOUR-APP-NAME", appVersion: "1.0.2", }, }, }, }); ``` #### `logLevel` The SDK uses the *consola* logger provided via Nuxt. You can adjust the ConfigDirector's SDK logging level independently for both the client and server side: ```ts [nuxt.config.ts] export default defineNuxtConfig({ //... modules: ["@configdirector/nuxt-sdk"], runtimeConfig: { public: { configdirector: { /** * Log level for the client/browser side of the ConfigDirector Nuxt SDK, * using consola's numeric levels. * 0 = error, 1 = warn, 2 = log, 3 = info, 4 = debug, 5 = trace. */ logLevel: 4, // debug }, }, configdirector: { /** * Log level for the server side of the ConfigDirector Nuxt SDK, * using consola's numeric levels. * 0 = error, 1 = warn, 2 = log, 3 = info, 4 = debug, 5 = trace. */ logLevel: 3, // info }, }, }); ``` #### `baseUrl` The base URL used to connect to ConfigDirector services. It can be configure independently for the client and server side. This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. #### `connection` Connection options for the server-side client used during SSR and in server routes. These mirror the [`connection` options of the Node.js SDK](https://docs.configdirector.com/sdks/server/node-js#connection) and accept the following optional values, plus `file` and `values` for `offline` mode (see [Run without a connection](#run-without-a-connection)): - `mode` - The connection mode, which can be `streaming`, `polling` or `offline`. It is recommended to use the default of `streaming` unless you have a specific need to use `polling` instead. In `offline` mode the server never connects to ConfigDirector and serves the values of a file or of `connection.values`; see [Run without a connection](#run-without-a-connection). - Defaults to `streaming` - `pollingInterval` - The interval, in seconds, between requests for config updates when `mode` is `polling`. It has no effect in `streaming` mode. A value below the minimum of 60 seconds is raised to 60 seconds and a warning is logged. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). - Defaults to `300` seconds (5 minutes) - `timeout` - How long, in milliseconds, the server-side client waits for its initial config payload from ConfigDirector services during initialization. Until the payload is received, configs evaluate to their default values. This timeout also bounds how long incoming requests are held while the client initializes (see [`waitForInitialization`](#waitforinitialization) below). - Defaults to `3000` milliseconds ```ts [nuxt.config.ts] export default defineNuxtConfig({ //... modules: ["@configdirector/nuxt-sdk"], runtimeConfig: { configdirector: { connection: { mode: "streaming", timeout: 5_000, // 5,000 milliseconds initialization timeout }, }, }, }); ``` Each option can also be provided at runtime via environment variables: `NUXT_CONFIGDIRECTOR_CONNECTION_MODE`, `NUXT_CONFIGDIRECTOR_CONNECTION_POLLING_INTERVAL`, `NUXT_CONFIGDIRECTOR_CONNECTION_FILE`, and `NUXT_CONFIGDIRECTOR_CONNECTION_TIMEOUT`. The client-side (browser) client accepts the same options under `runtimeConfig.public.configdirector.connection`, plus `values` for `offline` mode, with the client SDK defaults: `mode` defaults to `streaming` unless the server runs in `offline` mode, which the browser then follows, `pollingInterval` defaults to `60` seconds with a minimum of `30` seconds, and `timeout` defaults to `2000` milliseconds. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). The browser client keeps the last values it received on the device by default and serves them on the next page load of the same context until live values arrive, as the [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript#values-kept-on-the-device) does. ```ts [nuxt.config.ts] export default defineNuxtConfig({ //... modules: ["@configdirector/nuxt-sdk"], runtimeConfig: { public: { configdirector: { connection: { mode: "polling", pollingInterval: 120, // seconds }, }, }, }, }); ``` The client-side options can also be provided at runtime via environment variables: `NUXT_PUBLIC_CONFIGDIRECTOR_CONNECTION_MODE`, `NUXT_PUBLIC_CONFIGDIRECTOR_CONNECTION_POLLING_INTERVAL`, and `NUXT_PUBLIC_CONFIGDIRECTOR_CONNECTION_TIMEOUT`. #### Run without a connection Set the server-side connection `mode` to `offline` to run your application without connecting to ConfigDirector, for example in a matrix of UI tests where each cell pins every flag to a known value. Neither the server nor the browser needs an SDK key, no connection is opened and nothing is sent, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no lifecycle state changes. Every context receives the same value and targeting rules are not applied. Give the values once, to the server side, in a JSON file with one value per config key. The mode and the file can both be set through environment variables, so one build runs every cell with no code change: ```json [flags/new-checkout-on.json] { "new-checkout": true, "max-items": 20, "greeting": "hello", "theme": { "color": "blue" } } ``` ```sh NUXT_CONFIGDIRECTOR_CONNECTION_MODE=offline NUXT_CONFIGDIRECTOR_CONNECTION_FILE=./flags/new-checkout-on.json node .output/server/index.mjs ``` The file path is resolved against the working directory of the server process. The values can also be given inline in `nuxt.config.ts` as `connection.values` instead of a file: ```ts [nuxt.config.ts] export default defineNuxtConfig({ //... modules: ["@configdirector/nuxt-sdk"], runtimeConfig: { configdirector: { connection: { mode: "offline", values: { "new-checkout": true, "max-items": 20 }, }, }, }, }); ``` The config type of each value follows from the value itself: a boolean, an integral number (`integer`), any other number (`float`), a string, or an object or array (`json`). A value read with an in-code default value of another type returns the in-code default value, as it does against ConfigDirector. The browser follows the server. When the server runs in `offline` mode and `runtimeConfig.public.configdirector.connection` sets neither `mode` nor `values`, the browser fetches the server's values once from `/_configdirector/offline-values`, a route the module serves only in `offline` mode, and serves them in `offline` mode too, so SSR, hydration and the browser show the same values and `clientSdkKey` can be left out. `useConfigDirectorStatus` reports `loading` until the values arrive and `ready` right after, as it does while a live connection opens. Give the public `connection` its own `mode` and `values` to override this; the browser then serves those, whatever mode the server runs in, and the server serves its own, with no check that the two agree. In the same way, a browser given `mode: "streaming"` and a `clientSdkKey` connects to ConfigDirector as usual while the server runs in `offline` mode. With `mode: "offline"` and neither `file` nor `values`, the server is ready at once and every config returns its in-code default value on both sides. When the values cannot be loaded, every request fails with a `ConfigDirectorInitializationError` instead of rendering in-code default values, whatever `waitForInitialization` says: the file is missing, cannot be read, is not a JSON object or holds a value such as `null`; `file` and `values` are both given; or either is given in `streaming` or `polling` mode. A test cell with a misspelled path fails instead of passing on in-code default values. `pollingInterval`, `timeout` and `baseUrl` have no effect in `offline` mode. A statically generated site (`nuxt generate`) has no server at runtime, so there is no route to fetch the values from. Give the browser its own values with `runtimeConfig.public.configdirector.connection: { mode: "offline", values }` in `nuxt.config.ts`. If you come from LaunchDarkly, a file of values is what LaunchDarkly calls "flags from files", and `offline` with no values is its "offline mode". #### `waitForInitialization` When your Nuxt server starts, the server-side client connects to ConfigDirector and waits for its initial config payload. Any request that arrives before that payload is received (typically only the first few requests after a server start or a deploy) is **held until the payload arrives**, so SSR and server routes evaluate real config values instead of defaults. This behavior is enabled by default. The wait is bounded by the connection `timeout`. If the timeout expires before the payload is received: - The held requests are released and handled with **default config values**, and the client logs a warning. - Requests that arrive afterwards are no longer held. They also evaluate to default values until the payload arrives. - In `streaming` mode, the client keeps trying to connect in the background and starts serving real values as soon as the payload is received. In `polling` mode, the next attempt happens on the following polling interval. Requests are never held once initialization has settled, whether it succeeded, timed out, or failed with an unrecoverable error such as an invalid server SDK key. In normal operation, the payload arrives well within the timeout and the hold is barely noticeable. Holding requests during startup avoids rendering pages with default values that are then replaced by the real values once the browser client connects, which shows up as a visible flash of content and, in some cases, a hydration mismatch. If you prefer requests to be handled immediately during startup, accepting that they will evaluate configs to their default values until the payload arrives, you can opt out: ```ts [nuxt.config.ts] export default defineNuxtConfig({ //... modules: ["@configdirector/nuxt-sdk"], runtimeConfig: { configdirector: { waitForInitialization: false, }, }, }); ``` The option can also be set at runtime via the `NUXT_CONFIGDIRECTOR_WAIT_FOR_INITIALIZATION` environment variable. It has no effect in `offline` mode, where requests always wait for the values to load, which happens at once, and fail when they cannot be loaded (see [Run without a connection](#run-without-a-connection)). #### `deferInitialization` By default, the browser client connects to ConfigDirector as soon as the app is created, with whatever context it has at that point. An app that knows its context only after startup, such as one with `ssr: false` or one that fetches the signed-in user in the browser, therefore opens a first connection with no context and a second one once it calls `updateContext`. The visitor of that first connection counts as a [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) in addition to the signed-in user, and the values kept on the device for the signed-in user are not served until the second connection opens. Turn on `deferInitialization` to make the browser client wait for its first context instead: ```ts [nuxt.config.ts] export default defineNuxtConfig({ //... modules: ["@configdirector/nuxt-sdk"], runtimeConfig: { public: { configdirector: { deferInitialization: true, }, }, }, }); ``` With the option on, nothing connects and nothing is sent until the app calls `updateContext` from `useConfigDirectorContext`. Until then, `useConfigDirectorStatus` reports `loading` and configs return their in-code default values. The first `updateContext` call starts the client with that context: one connection opens and carries it, the values kept on the device for that context are served until live values arrive, and the status becomes `ready`, or `default` when the connection `timeout` passes without values, as at startup without the option. Later calls update the context as usual. A context set during server-side rendering is already known when the app is created in the browser, so the client starts with it right away, as it does without the option. A page that never sets a context never connects and stays `loading`; with SSR on, such a page renders the server's values and then shows the in-code default values once hydrated. So turn the option on only when every page that reads configs sets a context. The option has no effect in `offline` mode, where the client is ready at once with its values. The option can also be set at runtime via the `NUXT_PUBLIC_CONFIGDIRECTOR_DEFER_INITIALIZATION` environment variable. ### Hooks Hooks are events you can subscribe to in order to be notified of some key actions from the client instances. Since the Nuxt SDK utilizes both client (browser) and server instances, there are two separate composables to register hook handlers. You can learn more about *client* side hooks in the [Hooks section of the Browser Javascript SDK](https://docs.configdirector.com/sdks/browser/javascript#hooks). You can learn more about *server* side hooks in the [Hooks section of the Node.js SDK](https://docs.configdirector.com/sdks/server/node-js#hooks). #### Client-side Hooks The `useConfigDirectorClientHooks` composable can be used to register client-side hook handlers. There are two recommended ways of using the composable: 1. **Registering in a Nuxt plugin (recommended for clientReady and configsUpdated):** The browser client initializes during Nuxt's app:created lifecycle hook, before any page components mount. Registering `clientReady` and `configsUpdated` handlers in a Nuxt plugin ensures they're in place before initialization is completed and the events are fired for the first time: ```ts [plugins/configdirector-hooks.client.ts] export default defineNuxtPlugin(() => { useConfigDirectorClientHooks({ clientReady: ({ action }) => { console.log(`ConfigDirector client ready (${action})`); }, configsUpdated: ({ keys }) => { console.log("Configs updated:", keys); }, }); }); ``` The .client.ts suffix ensures this plugin runs only in the browser, not during SSR. This approach of registering hook handlers in a plugin, is also suitable for `contextUpdated` and `configEvaluated` in cases where the handlers are for global behavior (as opposed of page or component specific). 1. **Registering in a page component:** For hooks handlers for page or component specific behavior, register them inside a component's ` ``` #### Server-side Hooks The `useConfigDirectorServerHooks` composable can be used to register server-side hook handlers. Server hooks run against the long-lived Nitro singleton ConfigDirector client instance. Register them in a Nitro plugin in `server/plugins/` which runs once at server startup: ```ts [server/plugins/configdirector-hooks.ts] export default defineNitroPlugin(() => { useConfigDirectorServerHooks({ clientReady: () => { console.log("Server SDK connected to ConfigDirector"); }, configsUpdated: ({ keys }) => { console.log("Server-side configs updated:", keys); }, configEvaluated: ({ evaluation }) => { console.log("Config evaluated": evaluation); }, }); }); ``` ## Retrieve config values The `useConfigDirectorValue` composable is available to app components. On the server side, the `useConfigDirectorServerClient` composable returns the global server instance of the client (an instance of the [Node.js SDK](https://docs.configdirector.com/sdks/server/node-js)). Retrieving config values in components: ```vue [YourComponent.vue] ``` Retrieving config values in server resource handlers: ```ts [server-resource.get.ts] export default defineEventHandler(async (_event) => { const client = useConfigDirectorServerClient(); return client.getValue("my-config-key", false); }); ``` The `value` returned by the `useConfigDirectorValue` composable is a ShallowRef that will update whenever the config value is updated in the ConfigDirector dashboard, or if it evaluates to a different value due to targeting rules (for example, if the context is updated). Keep in mind that it is a ShallowRef when accessing it in scripts: ```vue [YourComponent.vue] ``` You can also determine if the client is still initializing, so rather than transition from the default value to the evaluated value, you can show a loading state until the client is ready and config values are evaluated: ```vue [YourComponent.vue] ``` Additionally, you can also use the `useConfigDirectorStatus` composable to retrieve just the status: ```vue [YourComponent.vue] ``` ### SSR config evaluation Evaluation of config values during server side rendering (SSR) utilizes the global server instance of the ConfigDirector Node.js client. That means there are no additional network requests that take place since the server SDK evaluates targeting rules locally for any given context. **For SSR to work correctly, configs need to be made available to both, the client and the server, in the ConfigDirector dashboard.** ## Update the context The context can be updated via the `useConfigDirectorContext` composable: ```vue [YourComponent.vue] ``` On server resource handlers, the context is provided at evaluation time as the third argument to `getValue`. Unlike client SDKs, the server SDKs are able to evaluate targeting rules for the given context locally without additional network calls. ```ts [server-resource.get.ts] export default defineEventHandler(async (_event) => { const client = useConfigDirectorServerClient(); return client.getValue("my-config-key", false, { id: "654321", name: "Another User", traits: { region: "Australia", }, }); }); ``` To evaluate configs for an entity other than a user, such as a tenant, set the context kind with `kind`. It works the same in `updateContext` and in `getValue`: ```ts [server-resource.get.ts] return client.getValue("my-config-key", false, { id: "acme", kind: "tenant", name: "Acme Inc.", traits: { plan: "enterprise", }, }); ``` Leaving `kind` out means `user`. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given. Context kinds are available from version 1.10.0 of the SDK. Updating the context on the client side re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior context. If your app learns its context only after startup, for example with `ssr: false` or once it has fetched the signed-in user, turn on [`deferInitialization`](#deferinitialization) so that the browser client waits for the first `updateContext` call and its first connection already carries the context. In the browser, the SDK keeps an instance ID in `localStorage` until the site's data is cleared, and ConfigDirector derives the identifier of a context without an `id` from it. A visitor who has not signed in keeps that identifier, and their percentage rollout bucket, from one visit to the next, and counts as one [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) however often they visit. ## `useConfigDirectorClient` composable In the event that you need to have access to the underlying Javascript ConfigDirectorClient instance in app components for more complex behaviors, you can access the instance via the `useConfigDirectorClient` composable: ```vue [YourComponent.vue] ``` Proceed with caution when using the `useConfigDirectorClient` composable. The `useConfigDirectorValue` and `useConfigDirectorContext` composables manage listeners and cleanup automatically. However, if you make use of the `useConfigDirectorClient` composable, you must manage cleaning up any listeners yourself.
Additionally, any calls to `dispose`, `unwatch`, or `unwatchAll` on the client instance can have unintended side effects and may result in subtle bugs.
### `evaluate` Both clients can return the evaluation behind a value instead of the value alone: the value, whether it is the in-code default value, the `reason`, and the `valueId`. In app components, the client from `useConfigDirectorClient` has `evaluate(key, defaultValue)`, as described for the [JavaScript SDK](https://docs.configdirector.com/sdks/browser/javascript#evaluate). In server code, the client from `useConfigDirectorServerClient` has `evaluate(key, defaultValue, context)`, with the context optional as in `getValue`, as described for the [Node.js SDK](https://docs.configdirector.com/sdks/server/node-js#evaluate). Use it where the reason matters at the point of the read, such as when reporting an evaluation to another system. `@configdirector/nuxt-sdk` exports the `ConfigEvaluation` and `EvaluationReason` types, and `evaluate` is available from version 1.13.0 of the SDK. ```ts [server-resource.get.ts] export default defineEventHandler(async (_event) => { const evaluation = useConfigDirectorServerClient().evaluate("new-checkout", false, { id: "user-id" }); return { newCheckout: evaluation.value, reason: evaluation.reason }; }); ``` ## Test your components Use the SDK's testing entry point, `@configdirector/nuxt-sdk/testing`, to test components and composables that read your configs and flags. It creates a **test client**: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, including the plugin and the composables, so your components behave exactly as they do against ConfigDirector, and no network connection is opened and no telemetry is sent. Tests run in the `nuxt` environment of [`@nuxt/test-utils`](https://nuxt.com/docs/getting-started/testing#unit-testing) version 4, which creates your Nuxt app inside the test process. `installTestClient` makes the ConfigDirector plugin provide the test client's client to the app instead of building one: ```ts [Checkout.nuxt.spec.ts] import { nextTick } from "vue"; import { mountSuspended } from "@nuxt/test-utils/runtime"; import { createTestClient, installTestClient } from "@configdirector/nuxt-sdk/testing"; import Checkout from "~/components/Checkout.vue"; const testClient = createTestClient({ values: { "new-checkout": true } }); installTestClient(testClient); beforeEach(() => testClient.replaceValues({ "new-checkout": true })); test("shows the new checkout when the flag is on", async () => { const component = await mountSuspended(Checkout); expect(component.text()).toContain("New checkout"); testClient.setValue("new-checkout", false); await nextTick(); expect(component.text()).toContain("Classic checkout"); }); ``` `@nuxt/test-utils` 4 creates the app once per test file, in a `beforeAll` hook that runs before your own hooks, so call `installTestClient` at the top level of the test file, or of a vitest `setupFiles` entry, and not inside `beforeAll` or `beforeEach`. `@nuxt/test-utils` 3 creates the app before your test file loads, so the plugin cannot see a client installed there: the testing entry needs version 4, which in turn needs vitest 4. Because the app and its client live for the whole file, reset the values between tests with `replaceValues`. Await `nextTick()` after `setValue`, `removeValue`, and `replaceValues`, since they update the composables' refs synchronously and Vue renders on the next tick. ### What the plugin does with an installed client - It provides the installed client to the app and does not build one, so the app under test needs no `clientSdkKey`. Of the public `configdirector` runtime config, only `logLevel` and `deferInitialization` apply. - It does not call `initialize` on a client that is ready or initializing. If the client is ready and the app's context differs from the client's, it calls `updateContext` instead. The status from `useConfigDirectorStatus` starts as `ready` for a ready client. - With `deferInitialization` on, it waits for the app's first context as in production: a client that is neither ready nor initializing is left untouched while the app has no context, and the first `updateContext` call starts it with that context. - It keeps its `clientReady` handler registered for the app's lifetime, as in production, and never disposes the client. Server code is not covered: the `nuxt` environment does not run Nitro, so the server plugin, the server `useConfigDirectorServerClient` composable, and your server handlers run only in end-to-end tests. Those start the built app in a separate process, where the SDK connects to the `baseUrl` from the runtime config as it does in production. ### Test client controls `values`, `setValue`, `removeValue`, and `replaceValues` take native values, and the config type follows from each value: a boolean, an integral number (`integer`), any other number (`float`), a string, or a plain object or array (`json`). `holdInitialization`, `completeInitialization`, and `failInitialization` drive `initialize`, so the `loading` and `default` statuses can be tested, and `holdContextUpdate`, `completeContextUpdate`, and `failContextUpdate` do the same for `updateContext`. `contextUpdates` records the context of every `initialize` and `updateContext` call. The [JavaScript SDK's testing section](https://docs.configdirector.com/sdks/browser/javascript#test-your-code) describes every control and what to expect from the client. # Android SDK > Install and configure the Android SDK, evaluate configs and feature flags, and keep your UI current as values change ## Introduction The Android SDK is intended to be used by Android applications. It evaluates your configs against a context, keeps them current as they change in the dashboard, and hands your UI values it can re-render from. It is written in Kotlin and is meant to be used from Kotlin and Java alike. Every API has a form that works without coroutines, and the Kotlin conveniences — `suspend` functions, `Flow`, and typed extensions — sit on top of it. It ships as three artifacts:
Artifact Contents Minimum SDK Java bytecode
com.configdirector:android-sdk The SDK 21 8
com.configdirector:android-sdk-compose Optional Jetpack Compose bindings 21 11
com.configdirector:android-sdk-testing Tools for testing the code that reads your configs 21 8
The Compose artifact depends only on the Compose runtime, so it does not pull `compose-ui` or Material into an application that does not already have them. Config evaluation is synchronous and reads config state the client already holds in memory, which is what makes it safe to call from a composable or from a view binding. Telemetry about those evaluations is aggregated and reported on its own coroutine, so evaluating a config never waits on the network. ## Installation The SDK is published to the [ConfigDirector Maven repository](https://docs.configdirector.com/sdks/maven-repository). Add the repository to `settings.gradle.kts` or `settings.gradle`, next to `google()` and `mavenCentral()`: ```kotlin [settings.gradle.kts] dependencyResolutionManagement { repositories { google() mavenCentral() exclusiveContent { forRepository { maven { name = "ConfigDirector" url = uri("https://maven.configdirector.com") } } filter { includeGroup("com.configdirector") } } } } ``` ```groovy [settings.gradle] dependencyResolutionManagement { repositories { google() mavenCentral() exclusiveContent { forRepository { maven { name = 'ConfigDirector' url = 'https://maven.configdirector.com' } } filter { includeGroup 'com.configdirector' } } } } ``` Then add the dependencies to your app module: ```kotlin [build.gradle.kts] dependencies { implementation("com.configdirector:android-sdk:1.9.3") // Optional, for Jetpack Compose applications implementation("com.configdirector:android-sdk-compose:1.9.3") } ``` ```groovy [build.gradle] dependencies { implementation 'com.configdirector:android-sdk:1.9.3' // Optional, for Jetpack Compose applications implementation 'com.configdirector:android-sdk-compose:1.9.3' } ``` ```toml [libs.versions.toml] [versions] configdirector = "1.9.3" [libraries] configdirector-android-sdk = { module = "com.configdirector:android-sdk", version.ref = "configdirector" } # Optional, for Jetpack Compose applications configdirector-android-sdk-compose = { module = "com.configdirector:android-sdk-compose", version.ref = "configdirector" } ``` The SDK declares the `INTERNET` permission, which is merged into your application's manifest, so there is nothing to add there. ## Configure and initialize the client 1. Create an instance of the client providing an Android context and your client SDK key. You can retrieve a client SDK key for each environment under `SDK keys` in the dashboard's navigation panel. 2. Initialize the client to initiate its connection lifecycle. ```kotlin [ConfigDirectorSetup.kt] import com.configdirector.ConfigDirectorClient // Creating the client makes no network calls. It throws only when the SDK key is // blank or an option holds an unusable value. val client = ConfigDirectorClient(applicationContext, "YOUR-CLIENT-SDK-KEY") // Connects and waits until config values are received, or until it times out. // After a timeout the client keeps trying to connect in the background. client.initialize() ``` ```java [ConfigDirectorSetup.java] import com.configdirector.ConfigDirectorClient; // Creating the client makes no network calls. It throws only when the SDK key is // blank or an option holds an unusable value. ConfigDirectorClient client = new ConfigDirectorClient(getApplicationContext(), "YOUR-CLIENT-SDK-KEY"); // Connects, and calls back on the main thread once config values are received or // the attempt times out. After a timeout the client keeps trying in the background. client.initialize(null, () -> Log.i("MyApp", "ConfigDirector ready: " + client.isReady())); ``` The client is thread safe. Create one and share it for the lifetime of the app. `initialize` is a `suspend` function in Kotlin, so it is called from a coroutine. The overload taking a `CompletionCallback` is the one to use from Java, and from anywhere in Kotlin that has no coroutine to run in. Before the client is initialized, config values will evaluate to the default value provided in code. The constructor throws a `ConfigDirectorValidationException` in only two cases, both of which are programming errors rather than runtime conditions: a blank client SDK key outside [offline mode](#run-without-a-connection), and an option that cannot be used, such as a negative timeout or a base URL that is not absolute. Connection failures are not thrown — `initialize` returns normally whether or not config state arrived, and `isReady` tells you which happened. Most applications create a single client instance and initialize it during startup. That usually means owning it in the `Application`, so every screen shares one client: ```kotlin [MyApplication.kt] import android.app.Application import com.configdirector.ConfigDirectorClient import com.configdirector.ConfigDirectorContext import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch class MyApplication : Application() { lateinit var client: ConfigDirectorClient private set override fun onCreate() { super.onCreate() client = ConfigDirectorClient(this, "YOUR-CLIENT-SDK-KEY") MainScope().launch { client.initialize(ConfigDirectorContext.build { id("user-123") }) } } } ``` ```java [MyApplication.java] import android.app.Application; import com.configdirector.ConfigDirectorClient; import com.configdirector.ConfigDirectorContext; public class MyApplication extends Application { private ConfigDirectorClient client; @Override public void onCreate() { super.onCreate(); client = new ConfigDirectorClient(this, "YOUR-CLIENT-SDK-KEY"); client.initialize( ConfigDirectorContext.builder().id("user-123").build(), () -> Log.i("MyApp", "ConfigDirector ready: " + client.isReady())); } public ConfigDirectorClient getClient() { return client; } } ``` The client watches the application behind the context it was given so it can release its network connection while the app is backgrounded. Pass it any context — the application behind it is what matters. If the context does not belong to an `Application`, the SDK logs a warning and leaves the connection running for you to manage with [`pauseNetwork()` and `resumeNetwork()`](#pausenetwork-and-resumenetwork). Nothing needs to close the client: it lives as long as the process, and Android reclaims everything when the process ends. Call [`close()`](#close) when an application wants the client gone before that, on sign-out for instance. ### Additional configuration options These options can be passed in to `ClientOptions`, which is built with `ClientOptions.build { }` from Kotlin and `ClientOptions.builder()` from Java. They are read once when the client is created; changing them afterwards has no effect. #### `metadata` The `metadata` option allows you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application. Whichever of the two you leave unset is read from the application when the client is created — the name from the application label and the version from `versionName` — so most applications do not need to set either one. When one cannot be read, the client says so at the `INFO` log level. ```kotlin [ConfigDirectorSetup.kt] val client = ConfigDirectorClient( applicationContext, "YOUR-CLIENT-SDK-KEY", ClientOptions.build { metadata("YOUR-APP-NAME", "1.0.2") }, ) ``` ```java [ConfigDirectorSetup.java] ConfigDirectorClient client = new ConfigDirectorClient( getApplicationContext(), "YOUR-CLIENT-SDK-KEY", ClientOptions.builder() .metadata(new Metadata("YOUR-APP-NAME", "1.0.2")) .build()); ``` #### `logger` By default, the SDK logs to logcat under the `ConfigDirector` tag, and it is set to log warnings and errors only. You can configure a `logger` by either creating an `AndroidLogger` with a different log level, or by implementing the `ConfigDirectorLogger` interface to provide your own logger. The interface can be used to create an adapter to another logging library. Configure the default logger to a different level: ```kotlin [ConfigDirectorSetup.kt] val client = ConfigDirectorClient( applicationContext, "YOUR-CLIENT-SDK-KEY", ClientOptions.build { logger(AndroidLogger(LogLevel.DEBUG)) }, ) ``` ```java [ConfigDirectorSetup.java] ConfigDirectorClient client = new ConfigDirectorClient( getApplicationContext(), "YOUR-CLIENT-SDK-KEY", ClientOptions.builder().logger(new AndroidLogger(LogLevel.DEBUG)).build()); ``` The available levels are `OFF`, `ERROR`, `WARN` (the default), `INFO` and `DEBUG`. Implement your own logger adapter: ```kotlin [MyLogger.kt] import com.configdirector.ConfigDirectorLogger import com.configdirector.LogLevel class MyLogger : ConfigDirectorLogger { // Messages more verbose than this level are never passed to `log`. override val level: LogLevel = LogLevel.INFO override fun log(level: LogLevel, message: String, error: Throwable?) { // your specific logging library implementation here } } val client = ConfigDirectorClient( applicationContext, "YOUR-CLIENT-SDK-KEY", ClientOptions.build { logger(MyLogger()) }, ) ``` ```java [MyLogger.java] import com.configdirector.ConfigDirectorLogger; import com.configdirector.LogLevel; public final class MyLogger implements ConfigDirectorLogger { // Messages more verbose than this level are never passed to `log`. @Override public LogLevel getLevel() { return LogLevel.INFO; } @Override public void log(LogLevel level, String message, Throwable error) { // your specific logging library implementation here } } ``` #### `connection` `ConnectionOptions` accepts six optional values: - `mode` - The connection mode, which can be `ConnectionMode.STREAMING`, `ConnectionMode.POLLING` or `ConnectionMode.OFFLINE`. It is recommended to use the default of `STREAMING` unless you have a specific need to use `POLLING` instead. In `OFFLINE` mode the client never connects to ConfigDirector and serves the values you give it; see [Run without a connection](#run-without-a-connection). - `STREAMING` holds a connection open and receives changes as they happen, reconnecting on its own with a backoff. The server sends a keepalive at least every 15 seconds, so a connection that stays silent for 45 seconds is dropped and reopened the same way. - Defaults to `ConnectionMode.STREAMING` - `values` - The values to serve in `OFFLINE` mode, as a `Map` keyed by config key. They can be given only in `OFFLINE` mode. See [Run without a connection](#run-without-a-connection). - `pollingIntervalMillis` - How often to re-fetch config state, in milliseconds, when `mode` is `ConnectionMode.POLLING`. It has no effect in any other mode. A value below the minimum of `30_000` (30 seconds) is raised to `30_000` and a warning is logged. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). - Defaults to `60_000` (60 seconds) - `timeoutMillis` - The timeout to be used in initialization and when updating the context. This is how long `initialize` will wait for data from ConfigDirector services before returning. If the timeout is reached, `initialize` will return but the client will still be in an unready status and returning default values. The client will continue to attempt to connect and retrieve config values in the background. - If your application is used in environments where the users frequently have poor or no connection, you may want to use a lower timeout, or simply don't await the `initialize` call. - It has no effect in `OFFLINE` mode. - Defaults to `3_000` (3 seconds) - `pausesWhileBackgrounded` - Whether to pause the connection while the app is in the background and resume it when the app returns to the foreground. Android stops background connections on its own, so this is enabled by default. Set it to `false` to manage the connection yourself with `pauseNetwork()` and `resumeNetwork()`. It has no effect in `OFFLINE` mode. - Defaults to `true` - `baseUrl` - The base URL used to connect to ConfigDirector services. - This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. It has no effect in `OFFLINE` mode. ```kotlin [ConfigDirectorSetup.kt] val client = ConfigDirectorClient( applicationContext, "YOUR-CLIENT-SDK-KEY", ClientOptions.build { connection { mode(ConnectionMode.POLLING) pollingIntervalMillis(600_000L) timeoutMillis(2_000L) } }, ) ``` ```java [ConfigDirectorSetup.java] ConfigDirectorClient client = new ConfigDirectorClient( getApplicationContext(), "YOUR-CLIENT-SDK-KEY", ClientOptions.builder() .connection( ConnectionOptions.builder() .mode(ConnectionMode.POLLING) .pollingIntervalMillis(600_000L) .timeoutMillis(2_000L) .build()) .build()); ``` Settings are checked as they are built, so an unusable one throws a `ConfigDirectorValidationException` where it was written rather than becoming a client that quietly never updates. In `OFFLINE` mode the timeout and the base URL are not used, so they are not checked. #### `caching` `CachingOptions` decides whether the client keeps the last values it received on the device and serves them on the next launch until live values arrive, and where. It is on by default; see [Values kept on the device](#values-kept-on-the-device) for its two fields. ### Events Events notify you of some key actions from the client. `addEventListener` registers a listener for the client's lifecycle changes, and `addEvaluationListener` registers one for every individual config evaluation. Both are called on the main thread, and both return a `Subscription` you close to stop listening. Kotlin callers have the same two as flows, `client.events` and `client.evaluations`; each collection is independent, so several parts of your app can observe them at once, and every flow completes when the client is closed. ```kotlin [ConfigDirectorSetup.kt] import com.configdirector.ClientEvent import com.configdirector.events lifecycleScope.launch { client.events.collect { event -> when (event) { is ClientEvent.Ready -> Log.i("MyApp", "Client is ready after ${event.reason}") is ClientEvent.ConfigsUpdated -> Log.i("MyApp", "Configs updated: ${event.keys}") is ClientEvent.ContextUpdated -> Log.i("MyApp", "Context updated: ${event.context?.id}") is ClientEvent.CachedValuesLoaded -> Log.i("MyApp", "Serving the values kept on the device until live values arrive") } } } ``` ```java [ConfigDirectorSetup.java] import com.configdirector.ClientEvent; import com.configdirector.Subscription; Subscription subscription = client.addEventListener(event -> { if (event instanceof ClientEvent.Ready) { Log.i("MyApp", "Client is ready after " + ((ClientEvent.Ready) event).getReason()); } else if (event instanceof ClientEvent.ConfigsUpdated) { Log.i("MyApp", "Configs updated: " + ((ClientEvent.ConfigsUpdated) event).getKeys()); } }); // Close the subscription to stop listening subscription.close(); ``` The following events are published to an event listener: - `ClientEvent.Ready` - Emitted when the client is initialized, or after reconnection due to a context update or a resumed network connection. When this event is emitted it means the client has received a payload from the ConfigDirector servers and is ready to evaluate configs. - `reason` - A `ConnectReason`, the action that triggered the client to connect/reconnect, which can be `ConnectReason.INITIALIZATION`, `ConnectReason.CONTEXT_UPDATE`, or `ConnectReason.NETWORK_RESUME`. - `ClientEvent.ConfigsUpdated` - Emitted when a payload is received from the ConfigDirector servers with config data. It is emitted during initialization or context update when an entire config payload is received. After initialization or context update, it is emitted when updates are pushed from the server (or discovered via polling if that connection mode is used). - `keys` - A `List` listing the config keys that were included in the payload from the server. On a delta update, these are only the configs that changed. - `removedKeys` - A `List` listing the keys of configs the payload removed. A config is removed when a full payload no longer includes it. Empty when nothing was removed, and always empty on a delta update. - `ClientEvent.ContextUpdated` - Emitted whenever the context takes effect, including during initialization. It can come before or after `ClientEvent.Ready`, so it should not be relied upon as a lifecycle event but rather as an informational event to track updates to the context. - `context` - The `ConfigDirectorContext` the client is now evaluating against. It can be `null`. - `ClientEvent.CachedValuesLoaded` - Emitted when the values kept on the device from the last payload are loaded and served, before live values arrive. It is followed by `ClientEvent.ConfigsUpdated` listing their keys. See [Values kept on the device](#values-kept-on-the-device). The event carries nothing. An evaluation listener is handed a `ConfigEvaluation` every time a config is evaluated, which can be useful for debugging what a screen actually read, and for capturing additional context for analytics events. This includes every read through a getter and every re-evaluation a watch makes. A `ConfigEvaluation` carries: - `key: String` - The config key that was evaluated. - `value: Any` - The value the config evaluated to. It can be the default value provided by the caller, for example if the config was evaluated before the client was initialized. It is a `Boolean`, `String`, `Integer` or `Double`, matching the accessor the config was read with. - `valueId: String?` - The value ID, which is a stable hash of the value that can be used for analytics or other third parties rather than sending the `value`. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves to third parties. It is `null` if the evaluation had to fall back to the default value. - `isDefaultValue: Boolean` - Whether or not the evaluation fell back to the default value provided in code. - `context: ConfigDirectorContext?` - The context the config was evaluated against, if one was set. - `reason: EvaluationReason` - The reason for the evaluation resolution. In the case the config successfully evaluated based on server-provided targeting rules, it will be `FOUND_MATCH`. A value served from the values kept on the device, before live values arrive, reports `CACHED`; see [Values kept on the device](#values-kept-on-the-device). If the evaluation had to fall back to the default value, the `reason` will encode why the fallback was required: - `CLIENT_NOT_READY` - The evaluation happened before the client finished initialization. - `CONFIG_STATE_MISSING` - The requested config key was not present in the payload received from the server. This could be due to an incorrect config key, or a config that was not enabled to be available to client SDKs. - `VALUE_MISSING` - The config had no value for the current context. - `INVALID_NUMBER` - The config was requested as a number but the value received from the server could not be converted to a number. - `INVALID_BOOLEAN` - The config was requested as a boolean but the value received from the server could not be converted to a boolean. - `INVALID_JSON` - The config was requested as JSON but the value received from the server could not be read as a JSON document. - `TYPE_MISMATCH` - The config was requested with a data type that did not match the type of the config and no reasonable type conversion was possible. ```kotlin [ConfigDirectorSetup.kt] import com.configdirector.evaluations lifecycleScope.launch { client.evaluations .filter { it.isDefaultValue } .collect { Log.w("MyApp", "'${it.key}' fell back to its default: ${it.reason.wireName}") } } ``` Do not read a config from an evaluation listener, and do not drive state from `evaluations` that a composable reads a config from. The read publishes another evaluation, which recomposes, which reads the config again. ### Run without a connection Set the connection `mode` to `ConnectionMode.OFFLINE` to run your application without connecting to ConfigDirector, for example in a suite of UI tests where each run pins every flag to a known value. An offline client opens no connection and sends nothing, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no config's lifecycle state changes. Every context receives the same value and targeting rules are not applied. An offline client also leaves the device alone: it reads nothing about your app and stores nothing in `SharedPreferences`. To test the code that reads your configs rather than run the whole application, use a test client instead; see [Test your code](#test-your-code). An offline client needs no client SDK key. Create it with the constructor that takes only a context and the options, as below, or pass a blank key, such as a `BuildConfig` field left empty in a test build. Give the values inline, with one value per config key. Your application decides where they come from, for example a map in a test build or a JSON asset it bundles: ```kotlin [ConfigDirectorSetup.kt] import com.configdirector.ClientOptions import com.configdirector.ConfigDirectorClient import com.configdirector.ConnectionMode import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch val client = ConfigDirectorClient( applicationContext, ClientOptions.build { connection { mode(ConnectionMode.OFFLINE) values(mapOf("new-checkout" to true, "max-items" to 20)) } }, ) MainScope().launch { client.initialize() } ``` ```java [ConfigDirectorSetup.java] import com.configdirector.ClientOptions; import com.configdirector.ConfigDirectorClient; import com.configdirector.ConnectionMode; import com.configdirector.ConnectionOptions; import java.util.Map; ConfigDirectorClient client = new ConfigDirectorClient( getApplicationContext(), ClientOptions.builder() .connection( ConnectionOptions.builder() .mode(ConnectionMode.OFFLINE) .values(Map.of("new-checkout", true, "max-items", 20)) .build()) .build()); client.initialize(null, () -> Log.i("MyApp", "ConfigDirector ready: " + client.isReady())); ``` Each value's type decides its config's type: a `Boolean` is a `boolean` config, an `Int` or a `Long` an `integer` config, a `Float` or a `Double` a `float` config, a `String` a `string` config, and a `Map` with `String` keys or a `List` a `json` config. A JSON library that reads an object into a `Map`, such as Gson, Moshi or Jackson, gives values of these types; Gson and Moshi read every number as a `Double`, so `20` in a JSON asset is a float config, which `getInt` still reads as `20`. A value is read by the same rules as a value served by ConfigDirector, so a boolean read with `getString`, for instance, returns the in-code default value with the `TYPE_MISMATCH` reason. `initialize` returns once the values are loaded, with the client ready and `ClientEvent.Ready` delivered once. A watch registered before `initialize` is handed the in-code default value and then the offline value once `initialize` loads it; one registered after is handed the offline value. Neither is called again, since offline values do not change. Calling `initialize` a second time hands the same values over again and delivers `Ready` again. `updateContext` takes the new context at once and changes no value, and `pauseNetwork` and `resumeNetwork` do nothing. `pollingIntervalMillis`, `timeoutMillis`, `baseUrl` and `pausesWhileBackgrounded` have no effect. With `ConnectionMode.OFFLINE` and no `values`, the client is ready at once and every config returns its in-code default value with the `CONFIG_STATE_MISSING` reason. Building the connection options throws a `ConfigDirectorValidationException` when the values cannot be used: a value that is `null`, which a Java map can hold, a number that is not finite, a value of a type not listed above, a blank key, or `values` given in `STREAMING` or `POLLING` mode. A test run with a wrong value fails where the options are built instead of passing on in-code default values. If you come from LaunchDarkly, a map of values is what LaunchDarkly calls "flags from files", and `OFFLINE` with no values is its "offline mode". ### Values kept on the device By default, the client keeps the last values it received from ConfigDirector on the device, in the app's `SharedPreferences`, and serves them on the next launch of the same context until live values arrive. An app opened without a network, after at least one successful connection, shows the values ConfigDirector last sent instead of the in-code default values. Every evaluation served this way reports the reason `CACHED`, with `isDefaultValue` set to `false`; a key the kept values do not carry reports `CLIENT_NOT_READY`, as before any values arrive. The client is not ready while it serves values kept on the device: `isReady` stays `false`, `isInitializing` stays `true`, `initialize` still waits for live values or its `timeoutMillis`, and `ClientEvent.Ready` is delivered once live values arrive and replace them. `ClientEvent.CachedValuesLoaded` is delivered when the kept values are loaded, followed by `ClientEvent.ConfigsUpdated` listing their keys, and watches are handed the kept values. When the first live payload replaces them, its `ConfigsUpdated` lists the kept keys it no longer carries in `removedKeys`. The values are kept for the context they were evaluated for, and served on the next launch only when the launch context has the same `kind` and `id`. A launch with another context serves the in-code default values until live values arrive, which then replace the kept values. A context without an `id` matches on the same install, as long as the app has not been uninstalled, because ConfigDirector derives its identifier from the instance ID the SDK stores; a context without an `id` keeps its percentage rollout bucket for as long as that instance ID lasts. Name, traits, app name and app version take no part in the match: live values correct them within a second when there is a network. Once the client is running, updating the context keeps serving the current values until the new context's values arrive, as it does with live values. Turn it off, or supply your own store, with the `caching` option: - `enabled` - Whether to keep the last values received. With `false`, nothing is read or written, even when a `provider` is given. - Defaults to `true` - `provider` - A store of your own, for example an encrypted one, implementing `CachingProvider`: `read()` returns the stored entry or `null`, `write(value)` replaces it, `remove()` deletes it, and `sizeLimit()` returns the largest entry, in bytes, the store accepts. `read`, `write` and `remove` are `suspend` functions, so a provider is written in Kotlin; a Java app keeps the default store or turns caching off. One provider instance serves one client. - Defaults to the app's `SharedPreferences`, under a key derived from the client SDK key, with a size limit of 256 KB ```kotlin [ConfigDirectorSetup.kt] val client = ConfigDirectorClient( applicationContext, "YOUR-CLIENT-SDK-KEY", ClientOptions.build { caching { enabled(true) provider(MyEncryptedStore()) } }, ) ``` ```java [ConfigDirectorSetup.java] ConfigDirectorClient client = new ConfigDirectorClient( getApplicationContext(), "YOUR-CLIENT-SDK-KEY", ClientOptions.builder() .caching(CachingOptions.builder().enabled(false).build()) .build()); ``` The entry is one string whose format belongs to the SDK and may change between SDK versions; an entry a newer SDK cannot read is removed. Before every write the SDK measures the entry against `sizeLimit()`; above it, the entry is removed and a warning is logged once, so the device never serves values that no longer reflect the set. A provider that fails to read, write or remove is logged once and never stops the client: the worst outcome is the in-code default values. Keeping values on the device is ignored in `OFFLINE` mode, and a test client from `android-sdk-testing` keeps nothing. If you come from Firebase Remote Config, this is what activated values persisting across restarts gives you; if you come from LaunchDarkly, it is what its mobile SDKs do with the flags they cache on the device. ## Retrieve config values To synchronously retrieve config values, use the client's typed getters: `getBoolean`, `getString`, `getInt`, `getDouble`, `getJsonObject` and `getJsonArray`. Each requires two arguments. The first argument is the config key, and the second is the default value to be returned if the client has not yet received config values from ConfigDirector services. Kotlin callers also have `client.value(key, default)`, which takes the type from the default value. You can also subscribe to config value changes via the matching watches: `watchBoolean`, `watchString`, `watchInt`, `watchDouble`, `watchJsonObject` and `watchJsonArray`. Each takes the config key, the default value, and a listener that is handed the config's current value straight away and then every time the evaluated value changes, whether because it was edited in the ConfigDirector dashboard or because the context was updated. Consecutive identical values are not delivered again. When a full payload no longer includes the watched config, the listener is handed the default value, as a getter would now return it. Close the returned `Subscription` to stop watching. In Kotlin, `client.values(key, default)` is the same thing as a `Flow`, and cancelling the collector stops watching. ```kotlin [MainActivity.kt] import com.configdirector.value import com.configdirector.values // Retrieve the current value. The default value decides the type the config // is read as, and is returned until the client is ready. val darkMode = client.value("my-config-key", false) // Subscribe to value updates. The flow emits the current value right away // and then every time the evaluated value changes. lifecycleScope.launch { client.values("my-config-key", false).collect { darkMode -> Log.i("MyApp", "Value updated: $darkMode") } } ``` ```java [MainActivity.java] import com.configdirector.Subscription; // Retrieve the current value. There is a getter per type a config can be read // as, and each returns the default value until the client is ready. boolean darkMode = client.getBoolean("my-config-key", false); // Subscribe to value updates. The listener is called on the main thread with the // current value, and then every time the evaluated value changes. Subscription subscription = client.watchBoolean( "my-config-key", false, value -> Log.i("MyApp", "Value updated: " + value)); // Close the subscription to stop watching subscription.close(); ``` ```kotlin [SampleScreen.kt (Compose)] import com.configdirector.compose.configValue @Composable fun SampleScreen() { // Recomposes whenever the evaluated value changes. val darkMode = configValue("my-config-key", false) Text("my-config-key: $darkMode") } ``` There is one getter and one watch per type a config can be read as, so a default value of any other type is a compile error rather than a failure at runtime. If the served value does not match the type it is read as, the SDK will attempt to convert it. If the conversion fails, it will return the default value. Mismatched types at runtime are captured by the telemetry collector and will surface as warnings in the ConfigDirector dashboard. ```kotlin [MainActivity.kt] client.getString("my-string-config-key", "Default") client.getInt("my-integer-config-key", 100) client.getDouble("my-decimal-config-key", 1.5) client.getBoolean("my-boolean-config-key", false) ``` A string, enum, URL, or custom config can be read as a string, and so can a JSON config, as its raw document. A boolean, integer, or float config read as a string evaluates to the default value with the `TYPE_MISMATCH` reason. A value written as a decimal and read with `getInt` is truncated. ### JSON configs A config declared as JSON in the ConfigDirector dashboard is read with `getJsonObject` or `getJsonArray`, depending on the shape of its document. The values inside are `String`, `Number`, `Boolean`, `List`, `Map`, or null, and the document cannot be modified. Only a JSON config reads as one; read it as a string to get its raw document. ```kotlin [MainActivity.kt] val theme = client.getJsonObject("theme", mapOf("primary" to "#101010")) val primary = theme["primary"] as? String val regions = client.getJsonArray("regions", listOf("us-east")) ``` ```java [MainActivity.java] Map theme = client.getJsonObject("theme", Map.of("primary", "#101010")); String primary = (String) theme.get("primary"); List regions = client.getJsonArray("regions", List.of("us-east")); ``` ### Reading configs in Jetpack Compose The `android-sdk-compose` artifact reads configs as Compose state, so a composable recomposes on its own whenever an evaluated value changes. Wrap your content in a `ConfigDirectorProvider` and every binding below it reads from that client: ```kotlin [MainActivity.kt] import com.configdirector.compose.ConfigDirectorProvider class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val client = (application as MyApplication).client setContent { ConfigDirectorProvider(client) { MaterialTheme { HomeScreen() } } } } } ``` `configValue` then reads a config anywhere inside it, with one overload per type a config can be read as. Until the client is ready, and for a value that cannot be read as the requested type, it is the default value: ```kotlin [HomeScreen.kt] import com.configdirector.compose.configContext import com.configdirector.compose.configValue import com.configdirector.compose.isClientReady @Composable fun HomeScreen() { val darkMode = configValue("dark-mode", false) val greeting = configValue("greeting", "Hello") // Recomposes when config state arrives, which is useful to show a loading // state rather than flashing the in-code default value on a slow connection. if (!isClientReady()) { LoadingScreen() return } Text(text = greeting, color = if (darkMode) Color.White else Color.Black) } ``` `isClientReady()` and `configContext()` expose the client's readiness and its current context as state, recomposing when either changes. Each binding takes the client from `LocalConfigDirectorClient` unless one is passed to it explicitly. Reading a binding with no provider above it in the tree fails rather than serving default values that look like real ones. ### Reading configs with framework views With framework views, register a watch when the view is created and close its subscription when the view goes away: ```kotlin [MainActivity.kt] class MainActivity : AppCompatActivity() { private var subscription: Subscription? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val client = (application as MyApplication).client subscription = client.watchBoolean("dark-mode", false) { darkMode -> // Called on the main thread with the current value, and on every change. findViewById(R.id.dark_mode).isChecked = darkMode } } override fun onDestroy() { subscription?.close() super.onDestroy() } } ``` ## Update the context A context can be provided when initializing the client: ```kotlin [ConfigDirectorSetup.kt] import com.configdirector.ConfigDirectorClient import com.configdirector.ConfigDirectorContext val client = ConfigDirectorClient(applicationContext, "YOUR-CLIENT-SDK-KEY") client.initialize( ConfigDirectorContext.build { id("12345") name("Example User") // Any arbitrary traits which can be referenced in targeting rules trait("region", "North America") }, ) ``` ```java [ConfigDirectorSetup.java] import com.configdirector.ConfigDirectorClient; import com.configdirector.ConfigDirectorContext; ConfigDirectorClient client = new ConfigDirectorClient(getApplicationContext(), "YOUR-CLIENT-SDK-KEY"); ConfigDirectorContext context = ConfigDirectorContext.builder() .id("12345") .name("Example User") // Any arbitrary traits which can be referenced in targeting rules .trait("region", "North America") .build(); client.initialize(context, () -> Log.i("MyApp", "ConfigDirector ready: " + client.isReady())); ``` The context can also be updated via `updateContext`: ```kotlin [MainActivity.kt] import com.configdirector.ConfigDirectorContext client.updateContext( ConfigDirectorContext.build { id("654321") name("Another User") trait("region", "Australia") }, ) // Update it to an anonymous context when a user signs out. Leaving `id` unset // makes ConfigDirector derive an identifier that lasts until the app is uninstalled. client.updateContext(ConfigDirectorContext.build { anonymous(true) }) ``` ```java [MainActivity.java] import com.configdirector.ConfigDirectorContext; client.updateContext( ConfigDirectorContext.builder() .id("654321") .name("Another User") .trait("region", "Australia") .build(), () -> Log.i("MyApp", "Context updated")); // Update it to an anonymous context when a user signs out. Leaving `id` unset // makes ConfigDirector derive an identifier that lasts until the app is uninstalled. client.updateContext( ConfigDirectorContext.builder().anonymous(true).build(), () -> Log.i("MyApp", "Signed out")); ``` To evaluate configs for an entity other than a user, such as a tenant, set the context kind with `kind`: ```kotlin [MainActivity.kt] client.updateContext( ConfigDirectorContext.build { id("acme") kind("tenant") name("Acme Inc.") trait("plan", "enterprise") }, ) ``` ```java [MainActivity.java] client.updateContext( ConfigDirectorContext.builder() .id("acme") .kind("tenant") .name("Acme Inc.") .trait("plan", "enterprise") .build(), () -> Log.i("MyApp", "Context updated")); ``` Leaving `kind` out means `user`. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given. Context kinds are available from version 1.8.0 of the SDK. In client SDKs (browser and mobile), updating the context re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior context.
Awaiting `updateContext` will wait until the new config values are downloaded or the connection times out. In the case of a timeout, the client will continue to attempt to connect with the new context in the background.
Traits are JSON-shaped values: `String`, `Number`, `Boolean`, `List`, `Map`, or null. A trait of any other type could never match a targeting rule, so building the context throws a `ConfigDirectorValidationException` rather than sending something the server cannot evaluate. ```kotlin [MainActivity.kt] client.updateContext( ConfigDirectorContext.build { id("user-123") name("Ada Lovelace") traits( mapOf( "plan" to "pro", "seats" to 12, "beta" to true, "regions" to listOf("us-east", "eu-west"), ), ) }, ) ``` For a signed-out user, set `anonymous` to `true`. The values are still used to evaluate targeting rules, but the context is not persisted and does not appear in the dashboard. The SDK keeps an instance ID in the app's `SharedPreferences` until the app is uninstalled, and ConfigDirector derives the identifier of a context without an `id` from it. A person who uses the app without signing in keeps that identifier, and their percentage rollout bucket, from one app start to the next, and counts as one [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) however often they open the app. ## Other useful client features The client provides additional properties and methods. ### `context` Returns the current context (which may be `null`). When calling `updateContext`, the `context` is not immediately updated. The `context` is updated once the connection for the new context succeeds or, if it times out, once the values for the new context arrive. ### `isReady` Returns a `Boolean` indicating if the client has been successfully initialized and is ready to evaluate configs for the given context. It is initially `false` and becomes `true` after `initialize` succeeds. Upon calling `updateContext` it becomes `false` again and it is set to `true` once `updateContext` succeeds. It stays `false` while the client serves the [values kept on the device](#values-kept-on-the-device). ### `isInitializing` Returns a `Boolean` indicating whether the client is trying to get its very first config state from the server. It is `false` on creation, becomes `true` when `initialize` is called on a client that has never received config state, stays `true` through timeouts and retries, and becomes `false` when the first config state arrives, when an unrecoverable connection error stops the retries, or on `close`. `updateContext` and `resumeNetwork` never set it. To show a loading state on a slow connection rather than switching from the in-code default value to the evaluated value, end it when `initialize` returns (or calls its callback): it returns once the values arrive, and at the latest once `timeoutMillis` elapses. Don't end it when `isInitializing` turns `false`, because that can happen long after the timeout, for example when the app starts without a network. ### `pauseNetwork()` and `resumeNetwork()` `pauseNetwork()` releases the network connection without discarding config state, watches, or listeners, and `resumeNetwork()` re-establishes it using the last context given to `initialize` or `updateContext`. Reads keep serving the last config state the client received, and `isReady` is `false` until the connection is back. The client already does this on its own while the app is backgrounded, so these are only needed if you set `pausesWhileBackgrounded` to `false` in order to manage the connection yourself. `resumeNetwork` has the same two forms as `initialize`: a `suspend` function for Kotlin, and one taking a `CompletionCallback`. ### `close()` Closes the connection, every watch, and every listener registration, and reports whatever telemetry is left. The client cannot be used afterwards: it stops receiving config state and never becomes ready again, though reads keep serving the last config state it received. Most applications never need to call it — the client lives as long as the process, and Android reclaims everything when the process ends. Call it when an application wants the client gone before that, on sign-out for instance. ## Test your code Use the SDK's testing artifact, `com.configdirector:android-sdk-testing`, to test the code that reads your configs and flags. It creates a **test client**: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector, the telemetry, and the app lifecycle observer are replaced. Everything else is the same code that runs in production, so the code under test behaves exactly as it does against ConfigDirector, and no network connection is opened, no telemetry is sent, no values are kept on the device, and no Android `Context` is needed. It runs in a plain JVM unit test, under Robolectric, and in an instrumented test alike. The artifact is released together with the SDK and must be the same version as the `android-sdk` on your test classpath; `createTestClient` refuses any other. Add it in test scope: ```kotlin [build.gradle.kts] dependencies { testImplementation("com.configdirector:android-sdk-testing:1.9.3") // For a Main dispatcher in plain JVM tests, see below testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") } ``` ```groovy [build.gradle] dependencies { testImplementation 'com.configdirector:android-sdk-testing:1.9.3' // For a Main dispatcher in plain JVM tests, see below testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0' } ``` ```kotlin [CheckoutTest.kt] import com.configdirector.testing.createTestClient class CheckoutTest { @Test fun `enables the new checkout when the flag is on`() = runBlocking { val testClient = createTestClient(values = mapOf("new-checkout" to true, "max-items" to 20)) testClient.client.initialize() val checkout = Checkout(testClient.client) assertThat(checkout.isNewCheckoutEnabled).isTrue() testClient.setValue("new-checkout", false) assertThat(checkout.isNewCheckoutEnabled).isFalse() testClient.client.close() } } ``` `testClient.client` is a `ConfigDirectorClient`, so it goes anywhere your code accepts one. It starts uninitialized, like a production client, because the code under test usually owns the call to `initialize`; with nothing held or failed, `initialize` completes at once with the client ready. Close it when the test is done, as your production code would. From Java, the factory is `ConfigDirectorTesting.createTestClient(values)`, with overloads that also take the timeout and the logger. The callback overloads of the client complete on the Main dispatcher, so a plain JVM test sets one first (see below): ```java [CheckoutTest.java] import com.configdirector.testing.ConfigDirectorTesting; import com.configdirector.testing.TestClient; TestClient testClient = ConfigDirectorTesting.createTestClient(Collections.singletonMap("new-checkout", true)); CountDownLatch initialized = new CountDownLatch(1); testClient.getClient().initialize(null, initialized::countDown); initialized.await(5, TimeUnit.SECONDS); Checkout checkout = new Checkout(testClient.getClient()); assertThat(checkout.isNewCheckoutEnabled()).isTrue(); testClient.setValue("new-checkout", false); assertThat(checkout.isNewCheckoutEnabled()).isFalse(); testClient.getClient().close(); ``` ### The Main dispatcher The client hands listeners, watches, the `values` and `events` flows, the Compose bindings, and the Java callback overloads back on the Main dispatcher. Robolectric and instrumented tests have one. A plain JVM test does not, so it sets one from `kotlinx-coroutines-test`. With `UnconfinedTestDispatcher`, watches and listeners run inline, so a watch has seen a `setValue` by the time it returns: ```kotlin @Before fun setUpMainDispatcher() { Dispatchers.setMain(UnconfinedTestDispatcher()) } @After fun tearDown() { testClient.client.close() Dispatchers.resetMain() } ``` From Java, call `TestDispatchers.setMain(Dispatchers.INSTANCE, Dispatchers.getDefault())` before the test and `TestDispatchers.resetMain(Dispatchers.INSTANCE)` after it. Reads through `getBoolean` and the other getters need no Main dispatcher. Without one, the first client call that needs it, such as registering a watch or `initialize`, fails with an `IllegalStateException` saying that `Dispatchers.Main` is missing. ### In Jetpack Compose A composable needs a composition to run in, which on the JVM means Robolectric with `createComposeRule`, or an instrumented test. Wrap the content in the production `ConfigDirectorProvider` with the test client's client, as the app does. The client hands values to the bindings on the main looper, which Robolectric only runs when the test lets it, so idle it after `initialize` and after every change: ```kotlin [HomeScreenTest.kt] import com.configdirector.compose.ConfigDirectorProvider import com.configdirector.testing.createTestClient @RunWith(RobolectricTestRunner::class) class HomeScreenTest { @get:Rule val compose = createComposeRule() private val testClient = createTestClient(values = mapOf("greeting" to "hello")) @After fun tearDown() = testClient.client.close() private fun settle() { shadowOf(Looper.getMainLooper()).idle() compose.waitForIdle() } @Test fun `shows the greeting from the config`() { compose.setContent { ConfigDirectorProvider(testClient.client) { HomeScreen() } } runBlocking { testClient.client.initialize() } settle() compose.onNodeWithText("hello").assertIsDisplayed() testClient.setValue("greeting", "hi") settle() compose.onNodeWithText("hi").assertIsDisplayed() } } ``` ### Values `values`, `setValue`, and `replaceValues` take native values, and the config type follows from each value's type: a `Boolean`; an `Int` or `Long` (an integer config); a `Float` or `Double` (a float config); a `String`; or a `Map` with `String` keys or a `List` (a JSON config). A `String` is always a string config, so JSON text meant as a JSON config is given as a `Map` or a `List`. Every value is served to every context; a test that needs different values per context is written as one test per value. A blank key, a non-finite number, a value of any other type, a `Map` with a non-`String` key, or JSON contents of any other type throw a `ConfigDirectorValidationException`, and nothing changes. From Java, a `null` value is a `NullPointerException`. Reads behave as they do in production: `setValue("k", true)` read as a `String` returns the in-code default value with the `TYPE_MISMATCH` reason, `setValue("k", 2.5)` read with `getInt` returns `2`, a `Long` beyond the `Int` range reads through `getDouble` and gives `getInt` the `INVALID_NUMBER` reason, and `setValue("k", "")` serves the in-code default value with the `VALUE_MISSING` reason. A JSON object is encoded in the insertion order of a `LinkedHashMap`, which is what `mapOf` builds, and with sorted keys for any other map; `getJsonObject` returns it in `org.json`'s order, as in production. JSON configs parse through `org.json`, which the artifact brings for plain JVM tests, where Android's own is a stub; on a device Android's own is used. ### Controls
Control Effect
setValue(key, value) Stores the value and, once the client is connected, delivers an update carrying only key . Reads see it when setValue returns; watches of key and ConfigsUpdated listeners see it once the Main dispatcher runs them.
removeValue(key) Removes the value and delivers a full update without it. Reads return the in-code default value with the CONFIG_STATE_MISSING reason, watches of key are handed the default, and ConfigsUpdated lists key in removedKeys .
replaceValues(values) Replaces every stored value, disarms any armed hold or failure, and delivers a full update. Use it to reset a test client shared across tests.
holdInitialization() The next initialize waits until completeInitialization() or failInitialization() , or until the client's timeoutMillis elapses.
completeInitialization() Delivers the stored values to the held initialize , so the client is ready when it returns, and lets initialize complete. Called while a hold is armed but not picked up, it disarms the hold.
failInitialization() Fails the held initialize the way an invalid SDK key does: it completes promptly, the client is not ready, and an error is logged. Called with no initialize held, it arms the next one to fail.
holdContextUpdate() , completeContextUpdate() , failContextUpdate() The same for the next updateContext .
contextUpdates The context of every initialize and updateContext call, in order. initialize without a context records an empty context, and resumeNetwork is not recorded.
The initialization controls act only on `initialize` and the context update controls only on `updateContext`; `resumeNetwork` is never held or failed. After a failed attempt, the next attempt succeeds and delivers the values stored in the meantime. A held `initialize` suspends its caller, so a test that acts while it is held starts it on another dispatcher, or uses the callback overload: ```kotlin [LoadingTest.kt] val testClient = createTestClient(values = mapOf("new-checkout" to true), timeoutMillis = 500) testClient.holdInitialization() val initialization = launch(Dispatchers.Default) { testClient.client.initialize() } assertThat(testClient.client.isReady).isFalse() testClient.completeInitialization() initialization.join() assertThat(testClient.client.isReady).isTrue() ``` ### Options `createTestClient` takes `values`, `timeoutMillis` (the client's connection timeout, which bounds how long a held attempt waits; defaults to the SDK's production timeout of 3 seconds), and `logger`. The logger defaults to a `StandardErrorLogger`, which writes warnings and errors to standard error, because the SDK's default `AndroidLogger` writes to logcat, which a plain JVM test does not have; pass `StandardErrorLogger(LogLevel.DEBUG)` to see everything, or any `ConfigDirectorLogger` of your own. ### What to expect - Under the test client, `initialize` publishes `Ready`, then `ConfigsUpdated`, then `ContextUpdated`. - A held attempt that times out, or that is interrupted by `pauseNetwork`, `close`, or a new attempt, leaves the client not ready until the next attempt and logs "An error occurred during initialization" (or "during context update") at error level, as production does after a connection timeout. `isInitializing` stays `true` until values arrive, as in production, unless `close` ended the attempt. - Watches are called on registration and then only when the evaluated value changes, as in production, so a `setValue` to the value already served calls no watch, while `ConfigsUpdated` still fires. - `pausesWhileBackgrounded` has no effect: the test client does not follow the app lifecycle, so backgrounding the app under Robolectric does not pause the connection. Call `pauseNetwork` and `resumeNetwork` to test that path; values set while paused are delivered on resume. - After `close`, the test client's controls are silent no-ops, and no thread or coroutine from the SDK is left running. # Flutter SDK > Install and configure the Flutter SDK, and evaluate configs and feature flags on every platform Flutter targets ## Introduction The Flutter SDK is intended to be used by Flutter applications on any of the platforms Flutter targets: iOS, Android, web, macOS, Windows, and Linux. The minimum supported version of Flutter is `3.22`, and the minimum supported version of Dart is `3.4`. Telemetry collection runs on a background isolate, so preparing and sending telemetry stays off your application's main isolate. The web has no isolates and a package cannot ship a web worker of its own, so on web the reports are prepared on the main thread instead; they are still built one batch per flush interval rather than while a config is being evaluated. ## Installation The SDK can be installed from pub.dev: [https://pub.dev/packages/configdirector_flutter_client_sdk](https://pub.dev/packages/configdirector_flutter_client_sdk) ```bash [flutter] flutter pub add configdirector_flutter_client_sdk ``` ```yaml [pubspec.yaml] dependencies: configdirector_flutter_client_sdk: ^1.7.2 ``` ## Configure and initialize the client 1. Create an instance of the client providing your client SDK key. You can retrieve a client SDK key for each environment under `SDK keys` in the dashboard's navigation panel. 2. Initialize the client to initiate its connection lifecycle. ```dart [config_director_setup.dart (Initialize the client)] import 'package:configdirector_flutter_client_sdk/configdirector_flutter_client_sdk.dart'; final client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY'); await client.initialize(); ``` Before the client is initialized, config values will evaluate to the default value provided in code. Most applications create a single client instance, initialize it during startup, and dispose of it on shutdown. In a Flutter app that usually means owning it in a `State`: ```dart [app.dart] class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { late final ConfigDirectorClient _client; @override void initState() { super.initState(); _client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY'); unawaited(_client.initialize()); } @override void dispose() { _client.dispose(); super.dispose(); } @override Widget build(BuildContext context) => MaterialApp(home: const HomePage()); } ``` The SDK follows the app lifecycle through `WidgetsBinding` so it can release its network connection while the app is backgrounded. If you create the client before `runApp`, call `WidgetsFlutterBinding.ensureInitialized()` first. Without it the client still works, but the connection is not paused automatically and the SDK logs a warning. ### Additional configuration options These options can be passed in to `ConfigDirectorClientOptions`. #### `metadata` The `metadata` option allows you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application. Whichever of the two you leave unset is read from the platform — the application label and `versionName` on Android, the bundle display name and short version string on iOS and macOS, and `version.json` on web — so most applications do not need to set either one. ```dart [config_director_setup.dart] final client = ConfigDirectorClient( clientSdkKey: 'YOUR-CLIENT-SDK-KEY', options: const ConfigDirectorClientOptions( metadata: ConfigDirectorMetaContext( appName: 'YOUR-APP-NAME', appVersion: '1.0.2', ), ), ); ``` #### `logger` By default, the SDK logs to the console and it is set to log warnings and errors only. You can configure a `logger` by either creating a ConfigDirector console logger with a different log level, or by implementing the `ConfigDirectorLogger` interface to provide your own logger. The interface can be used to create an adapter to another logging library. Configure the ConfigDirector console logger to a different level: ```dart [config_director_setup.dart] final client = ConfigDirectorClient( clientSdkKey: 'YOUR-CLIENT-SDK-KEY', options: ConfigDirectorClientOptions( logger: ConsoleLogger(level: ConfigDirectorLogLevel.debug), ), ); ``` Implement your own logger adapter: ```dart [config_director_setup.dart] class MyLogger implements ConfigDirectorLogger { @override void debug(String message, [Object? error, StackTrace? stackTrace]) { // your specific logging library implementation here } @override void info(String message, [Object? error, StackTrace? stackTrace]) { // your specific logging library implementation here } @override void warn(String message, [Object? error, StackTrace? stackTrace]) { // your specific logging library implementation here } @override void error(String message, [Object? error, StackTrace? stackTrace]) { // your specific logging library implementation here } } final client = ConfigDirectorClient( clientSdkKey: 'YOUR-CLIENT-SDK-KEY', options: ConfigDirectorClientOptions(logger: MyLogger()), ); ``` #### `connection` `ConnectionOptions` accepts six optional values: - `mode` - The connection mode, which can be `streaming`, `polling` or `offline`. It is recommended to use the default of `streaming` unless you have a specific need to use `polling` instead. In `offline` mode the client never connects to ConfigDirector and serves the values you give it; see [Run without a connection](#run-without-a-connection). - Defaults to `ConnectionMode.streaming` - `values` - The values to serve in `offline` mode, keyed by config key. It can be given only in `offline` mode. See [Run without a connection](#run-without-a-connection). - `pollingInterval` - How often to re-fetch config state when `mode` is `ConnectionMode.polling`. It has no effect in any other mode. A value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). - Defaults to `Duration(seconds: 60)` - `timeout` - The timeout to be used in initialization and when updating the context. This is how long `initialize` will wait for data from ConfigDirector services before completing its `Future`. If the timeout is reached, `initialize` will return but the client will still be in an unready status and returning in-code default values. The client will continue to attempt to connect and retrieve config values in the background. It has no effect in `offline` mode. - If your application is used in environments where the users frequently have poor or no connection, you may want to use a lower timeout, or simply don't await the `initialize` method call - Defaults to `Duration(seconds: 3)` - `pauseWhileBackgrounded` - Whether to pause the connection while the app is in the background and resume it when the app returns to the foreground. Mobile operating systems terminate background connections on their own, so this is enabled by default. Set it to `false` to manage the connection yourself with `pauseNetwork` and `resumeNetwork`. It has no effect in `offline` mode. - Defaults to `true` - `baseUrl` - The base URL used to connect to ConfigDirector services - This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. It has no effect in `offline` mode. ```dart [config_director_setup.dart] final client = ConfigDirectorClient( clientSdkKey: 'YOUR-CLIENT-SDK-KEY', options: const ConfigDirectorClientOptions( connection: ConnectionOptions( mode: ConnectionMode.streaming, timeout: Duration(seconds: 2), ), ), ); ``` #### `caching` `CachingOptions` decides whether the client keeps the last values it received on the device and serves them on the next launch until live values arrive, and where. It is on by default; see [Values kept on the device](#values-kept-on-the-device) for its two fields. ### Events Events notify you of some key actions from the client. Each one is exposed as a `Stream` you can listen to, and every stream is closed when the client is disposed: ```dart [config_director_setup.dart] client.onConfigEvaluated.listen((event) { debugPrint('Received onConfigEvaluated: ${event.evaluation}'); }); ``` The following events are available: - `onClientReady: StreamClientReadyEvent` - Emitted when the client is initialized, or after reconnection due to a context update or a resumed network connection. When this event is emitted it means the client has received a payload from the ConfigDirector servers and is ready to evaluate configs. - `action` - The action that triggered the client to connect/reconnect, which can be `ClientConnectAction.initialization`, `ClientConnectAction.contextUpdate`, or `ClientConnectAction.networkResume`. - `onContextUpdated: StreamContextUpdatedEvent` - Emitted whenever the context is updated, including during initialization. It is emitted before `onClientReady`, therefore should not be relied upon as a lifecycle event but rather as an informational event to track updates to the context. - `context` - The context the client is being updated to. It can be `null`. - `onConfigsUpdated: StreamConfigsUpdatedEvent` - Emitted when a payload is received from the ConfigDirector servers with config data. It is emitted during initialization or context update when an entire config payload is received. After initialization or context update, it is emitted when updates are pushed from the server (or discovered via polling if that connection mode is used). - `keys` - A `List` listing the config keys that were included in the payload from the server. On a delta update, these are only the configs that changed. - `removedKeys` - A `List` listing the keys of configs the payload removed. A config is removed when a full payload no longer includes it. Empty when nothing was removed, and always empty on a delta update. - `onCachedValuesLoaded: StreamCachedValuesLoadedEvent` - Emitted when the values kept on the device from the last payload are loaded and served, before live values arrive. See [Values kept on the device](#values-kept-on-the-device). The event carries nothing. - `onConfigEvaluated: StreamConfigEvaluatedEvent` - Emitted whenever a config is evaluated. This includes calls to `getValue` and evaluations delivered to listeners via `watch`. - `evaluation` - A `ConfigEvaluation` object containing the details of the evaluation: - `key: String` - The config key that was evaluated - `value: Object` - The value the config evaluated to. It can be the default value provided to `getValue` or `watch`. For example, if the config was evaluated before the client was initialized. - `valueId: String?` - The value ID, which is a stable hash of the value that can be used for analytics or other third parties rather than sending the `value`. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves to third parties. It may be `null` if the evaluation had to fall back to the default value. - `isDefaultValue: bool` - Whether or not the evaluation fell back to the default value provided in code. - `context: ConfigDirectorContext?` - The context the config was evaluated against, if one was set. - `reason: EvaluationReason` - The reason for the evaluation resolution. In the case the config successfully evaluated based on server-provided targeting rules, it will be `foundMatch`. A value served from the values kept on the device, before live values arrive, reports `cached`; see [Values kept on the device](#values-kept-on-the-device). If the evaluation had to fall back to the default value, the `reason` will encode why the fallback was required: - `clientNotReady` - The evaluation happened before the client finished initialization - `configStateMissing` - The requested config key was not present in the payload received from the server. This could be due to an incorrect config key, or a config that was not enabled to be available to client SDKs. - `valueMissing` - The config had no value for the current context. - `invalidNumber` - The config was requested as a number but the value received from the server could not be converted to a number. - `invalidBoolean` - The config was requested as a boolean but the value received from the server could not be converted to a boolean. - `invalidJson` - The config was requested as JSON but the value received from the server could not be converted to a JSON document. - `typeMismatch` - The config was requested with a data type that did not match the type of the config and no reasonable type conversion was possible. ### Run without a connection Set the connection `mode` to `ConnectionMode.offline` to run your application without connecting to ConfigDirector, for example in a suite of UI tests where each run pins every flag to a known value. An offline client opens no connection and sends nothing, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no config's lifecycle state changes. Every context receives the same value and targeting rules are not applied. An offline client reads nothing from the platform either, so it also runs under `flutter test`. An offline client needs no client SDK key. Leave `clientSdkKey` out, as below, or pass an empty one, such as the value `String.fromEnvironment` returns for a key that was not given with `--dart-define`. Give the values inline, with one value per config key. Your application decides where they come from, for example a map in the test build or a JSON asset it bundles: ```dart [config_director_setup.dart] final client = ConfigDirectorClient( options: const ConfigDirectorClientOptions( connection: ConnectionOptions( mode: ConnectionMode.offline, values: {'new-checkout': true, 'max-items': 20}, ), ), ); await client.initialize(); ``` ```dart [config_director_setup.dart] import 'dart:convert'; import 'package:flutter/services.dart'; final values = jsonDecode( await rootBundle.loadString('assets/flags/new-checkout-on.json'), ) as Map; final client = ConfigDirectorClient( options: ConfigDirectorClientOptions( connection: ConnectionOptions(mode: ConnectionMode.offline, values: values), ), ); await client.initialize(); ``` ```json [assets/flags/new-checkout-on.json] { "new-checkout": true, "max-items": 20, "greeting": "hello", "theme": { "color": "blue" } } ``` The config type of each value follows from its Dart type: a `bool` (`boolean`), an `int` (`integer`), a `double` (`float`), a `String` (`string`), or a `Map` with `String` keys or a `List` (`json`). The result of `jsonDecode` can be passed as it is, so `20` in a JSON asset is an integer and `0.15` a float. On the web, where Dart integers are JavaScript numbers, `2.0` is an `int` and an integer beyond 2 53 − 1 in either direction is refused. The values are read when the client is created. A value read with an in-code default value of another type returns the in-code default value with the `typeMismatch` reason, as it does against ConfigDirector. `initialize` completes once the values are loaded, with the client ready and `onClientReady` emitted once. A `watch` stream subscribed before `initialize` emits the in-code default value and then the offline value once `initialize` loads it; one subscribed after emits the offline value. Neither emits again, since offline values do not change. `updateContext` takes the new context at once and changes no value, and `pauseNetwork` and `resumeNetwork` do nothing. `timeout`, `pollingInterval`, `baseUrl` and `pauseWhileBackgrounded` have no effect. With `ConnectionMode.offline` and no `values`, the client is ready at once and every config returns its in-code default value with the `configStateMissing` reason. `initialize` throws a `ConfigDirectorInitializationException`, which the SDK exports, when the values cannot be loaded: a value that is `null`, a number that is not finite, on the web an integer beyond 2 53 − 1 in either direction, a value of a type not listed above, a blank key, or `values` given in `streaming` or `polling` mode. The client stays not ready, so a test run with a wrong value fails instead of passing on in-code default values. If you come from LaunchDarkly, a map of values is what LaunchDarkly calls "flags from files", and `offline` with no values is its "offline mode". ### Values kept on the device By default, the client keeps the last values it received from ConfigDirector on the device, through the `shared_preferences` plugin, and serves them on the next launch of the same context until live values arrive. An app opened without a network, after at least one successful connection, shows the values ConfigDirector last sent instead of the in-code default values. Every evaluation served this way reports the reason `cached`, with `isDefaultValue` set to `false`. The client is not ready while it serves values kept on the device: `isReady` stays `false`, `isInitializing` stays `true`, `initialize` still waits for live values or its `timeout`, and `onClientReady` emits once live values arrive and replace them. `onCachedValuesLoaded` emits when the kept values are loaded, followed by `onConfigsUpdated` listing their keys, and `watch` streams emit the kept values. The values are kept for the context they were evaluated for, and served on the next launch only when the launch context has the same `kind` and `id`. A launch with another context serves the in-code default values until live values arrive, which then replace the kept values. A context without an `id` matches on the same install, as long as the app has not been uninstalled, because ConfigDirector derives its identifier from the instance ID the SDK stores; a context without an `id` keeps its percentage rollout bucket for as long as that instance ID lasts. Name, traits, app name and app version take no part in the match: live values correct them within a second when there is a network. Once the client is running, updating the context keeps serving the current values until the new context's values arrive, as it does with live values. Turn it off, or supply your own store, with the `caching` option: - `enabled` - Whether to keep the last values received. With `false`, nothing is read or written, even when a `provider` is given. - Defaults to `true` - `provider` - A store of your own, for example an encrypted one, implementing `CachingProvider`: `read()` returns the stored entry or `null`, `write(value)` replaces it, `remove()` deletes it, and `sizeLimit()` returns the largest entry, in bytes, the store accepts. `read`, `write` and `remove` return a `Future`. One provider instance serves one client. - Defaults to `shared_preferences`, under a key derived from the client SDK key, with a size limit of 256 KB ```dart [config_director_setup.dart] final client = ConfigDirectorClient( clientSdkKey: 'YOUR-CLIENT-SDK-KEY', options: ConfigDirectorClientOptions( caching: CachingOptions(enabled: true, provider: MyEncryptedStore()), ), ); await client.initialize(); ``` The entry is one string whose format belongs to the SDK and may change between SDK versions; an entry a newer SDK cannot read is removed. Before every write the SDK measures the entry against `sizeLimit()`; above it, the entry is removed and a warning is logged once, so the device never serves values that no longer reflect the set. A provider that fails to read, write or remove is logged once and never stops the client: the worst outcome is the in-code default values. Keeping values on the device is ignored in `offline` mode, and the test client in `testing.dart` keeps nothing. If you come from Firebase Remote Config, this is what activated values persisting across restarts gives you; if you come from LaunchDarkly, it is what its mobile SDKs do with the flags they cache on the device. ## Retrieve config values To synchronously retrieve config values, use the client's `getValue` method. It requires two arguments. The first argument is the config key, and the second is the default value to be returned if the client has not yet received config values from ConfigDirector services. You can also subscribe to config value changes via the `watch` method. It requires two arguments, the config key and the default value, and returns a `Stream` that emits the config's current value on subscription and then every time the evaluated value changes. Consecutive identical values are not re-emitted. Cancel the subscription to stop watching, or call the `unwatch` method to close every stream watching a specific config key. ```dart [main.dart (Use the client)] import 'config_director_setup.dart'; // Retrieve the current value final value = client.getValue('my-config-key', false); // Subscribe to value updates final subscription = client.watch('my-config-key', false).listen((newValue) { debugPrint('Value updated: $newValue'); }); await subscription.cancel(); // Cancel the subscription to remove the listener client.unwatch('my-config-key'); // Closes every stream watching that key ``` Both `getValue` and `watch` infer their type from the default value, and accept any type supported by that specific config (`bool`, `String`, `int`, `double`, `num`, or any type a JSON config decodes into). A string config requested as a `bool` or a number is converted when its value spells one. A boolean, integer, or float config requested as a `String`, or any config requested as a type its value cannot be converted to, evaluates to the default value with the `typeMismatch` reason. Mismatched types at runtime are captured by the telemetry collector and will surface as warnings in the ConfigDirector dashboard. ```dart [main.dart] client.getValue('my-string-config-key', 'Default'); client.getValue('my-integer-config-key', 100); client.getValue('my-boolean-config-key', false); client.getValue>('my-json-config-key', const {}); ``` In widgets, `watch` pairs with a `StreamBuilder` so the widget rebuilds whenever the config changes, whether because it was edited in the ConfigDirector dashboard or because the context was updated. When a full payload no longer includes the watched config, the stream emits the default value, as `getValue` would now return it: ```dart [home_page.dart] StreamBuilder( stream: client.watch('dark-mode', false), initialData: false, builder: (context, snapshot) => MyHomePage(darkMode: snapshot.data ?? false), ) ``` `watch` hands out a new stream on every call, so call it when its inputs change rather than on every build, from `initState` or `didChangeDependencies`, holding onto the stream in between. Calling it directly in `build` resubscribes on each rebuild. ## Update the context A context can be provided when initializing the client: ```dart [config_director_setup.dart] import 'package:configdirector_flutter_client_sdk/configdirector_flutter_client_sdk.dart'; final client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY'); await client.initialize( const ConfigDirectorContext( id: '12345', name: 'Example User', traits: { // Any arbitrary traits which can be referenced in targeting rules 'region': 'North America', }, ), ); ``` The context can also be updated via `updateContext`: ```dart [main.dart] import 'package:configdirector_flutter_client_sdk/configdirector_flutter_client_sdk.dart'; import 'config_director_setup.dart'; await client.updateContext( const ConfigDirectorContext( id: '654321', name: 'Another User', traits: {'region': 'Australia'}, ), ); // Update it to a context with no `id` (anonymous user context) when a user // signs out. ConfigDirector derives an identifier that lasts until the app is // uninstalled, or on the web until the site's data is cleared. await client.updateContext(const ConfigDirectorContext()); ``` To evaluate configs for an entity other than a user, such as a tenant, set the context kind with `kind`: ```dart [main.dart] await client.updateContext( const ConfigDirectorContext( id: 'acme', kind: 'tenant', name: 'Acme Inc.', traits: {'plan': 'enterprise'}, ), ); ``` Leaving `kind` out means `user`. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given. Context kinds are available from version 1.6.0 of the SDK. In client SDKs (browser and mobile), updating the context re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior context.
Awaiting `updateContext` will wait until the new config values are downloaded or the connection times out. In the case of a timeout, the client will continue to attempt to connect with the new context in the background.
The SDK keeps an instance ID through the `shared_preferences` plugin until the app is uninstalled, or on the web until the site's data is cleared, and ConfigDirector derives the identifier of a context without an `id` from it. A person who uses the app without signing in keeps that identifier, and their percentage rollout bucket, from one app start to the next, and counts as one [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics) however often they open the app. ## Other useful client features The client provides additional properties and methods. ### `evaluate` Evaluates a config the way `getValue` does and returns the `ConfigEvaluation` behind it — the value, whether it is the default, the `reason`, and the `valueId` — instead of the value alone. It is the same object `onConfigEvaluated` emits, so use it where the reason matters at the point of the read, such as when reporting an evaluation to another system; `getValue` is enough everywhere else. Like every read, it is emitted on `onConfigEvaluated` and counted in telemetry. ```dart [main.dart] final evaluation = client.evaluate('my-config-key', false); if (evaluation.reason == EvaluationReason.clientNotReady) { // Config state has not arrived yet. } ``` ### `context` Returns the current context (which may be `null`). When calling `updateContext`, the `context` is not immediately updated. The `context` is updated after the new connection for the new context succeeds (or times out and goes on retry). ### `isReady` Returns a `bool` indicating if the client has been successfully initialized and is ready to evaluate configs for the given context. It is initially `false` and becomes `true` after `initialize` succeeds. Upon calling `updateContext` it becomes `false` again and it is set to `true` once `updateContext` succeeds. ### `isInitializing` Returns a `bool` indicating whether the client is trying to get its very first config state from the server. It is `false` on creation, becomes `true` when `initialize` is called on a client that has never received config state, stays `true` through timeouts and retries, and becomes `false` when the first config state arrives, when an unrecoverable connection error stops the retries, or on `dispose`. `updateContext` and `resumeNetwork` never set it. To show a loading state on a slow connection rather than switching from the in-code default value to the evaluated value, end it when `initialize` completes: it completes once the values arrive, and at the latest once the `timeout` elapses. Don't end it when `isInitializing` turns `false`, because that can happen long after the timeout, for example when the app starts without a network. ### `unwatchAll` Closes every stream that was previously created via `watch`, for all config keys. ### `pauseNetwork` and `resumeNetwork` `pauseNetwork` releases the network connection without discarding config state, event listeners, or watch streams, and `resumeNetwork` re-establishes it using the last context given to `initialize` or `updateContext`. The client already does this on its own while the app is backgrounded, so these are only needed if you set `pauseWhileBackgrounded` to `false` in order to manage the connection yourself. ### `dispose` Closes the connection, every watch stream, and every event stream, and reports whatever telemetry is left. Only call `dispose` when your application shuts down and it will no longer make use of the client instance. The client cannot be used afterwards. ## Test your code Use the SDK's testing library, `package:configdirector_flutter_client_sdk/testing.dart`, to test the code that reads your configs and flags. It creates a **test client**: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector, the telemetry, and the app lifecycle and platform lookups are replaced. Everything else is the same code that runs in production, so the code under test behaves exactly as it does against ConfigDirector, and no network connection is opened, no telemetry is sent, and no platform channel is used. The library ships in the package, so there is nothing else to install, and it runs under `flutter test` on the VM and in a browser alike. ```dart [checkout_test.dart] import 'package:configdirector_flutter_client_sdk/testing.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { test('enables the new checkout when the flag is on', () async { final testClient = createTestClient( values: {'new-checkout': true, 'max-items': 20}, ); addTearDown(testClient.client.dispose); await testClient.client.initialize(); final checkout = Checkout(testClient.client); expect(checkout.isNewCheckoutEnabled, isTrue); testClient.setValue('new-checkout', false); await pumpEventQueue(); expect(checkout.isNewCheckoutEnabled, isFalse); }); } ``` `testClient.client` is a `ConfigDirectorClient`, so it goes anywhere your code accepts one. It starts uninitialized, like a production client, because the code under test usually owns the call to `initialize`; with nothing held or failed, `initialize` completes at once with the client ready. Dispose it as your production code would, for example with `addTearDown`. ### Widget tests Hand the client to your widgets the way your app does, through an `InheritedWidget`, a constructor parameter, or your dependency injection of choice. A `StreamBuilder` over `watch` then rebuilds whenever a value changes. Updates are delivered asynchronously, so pump after every change with `await tester.pumpAndSettle()`: it runs the delivery and then the frame the rebuild schedules, whereas a single `tester.pump()` only draws a frame that was already scheduled. ```dart [home_page_test.dart] testWidgets('shows the greeting from the config', (tester) async { final testClient = createTestClient(values: {'greeting': 'hello'}); addTearDown(testClient.client.dispose); await tester.pumpWidget(MyApp(client: testClient.client)); unawaited(testClient.client.initialize()); await tester.pumpAndSettle(); expect(find.text('hello'), findsOneWidget); testClient.setValue('greeting', 'hi'); await tester.pumpAndSettle(); expect(find.text('hi'), findsOneWidget); }); ``` In `testWidgets`, time is fake and does not advance on its own, and the check for pending timers runs right after the test body. A held `initialize` keeps a timer pending, so end it inside the test body by completing it, failing it, or disposing the client, and never await a held `initialize` directly, which hangs the test body. The client measures elapsed time with a real `Stopwatch`, which fake time does not advance, so a hold that is meant to time out needs about twice the `timeout` of fake time. ### Values `values`, `setValue`, and `replaceValues` take native values, and the config type follows from each value's type: a `bool`; an `int` (an integer config); a `double` (a float config); a `String`; or a `Map` with `String` keys or a `List` (a JSON config). A `String` is always a string config, so JSON text meant as a JSON config is given as `jsonDecode(text)`. Every value is served to every context; a test that needs different values per context is written as one test per value. `null`, a blank key, a non-finite number, a value of any other type, a `Map` with a non-`String` key, or JSON contents of any other type throw a `ConfigDirectorValidationException`, and nothing changes. On the web, where Dart integers are JavaScript numbers, `2.0` is an `int` and an integer beyond 2 53 − 1 in either direction is rejected. Reads behave as they do in production: `setValue('k', true)` read as a `String` returns the in-code default value with the `typeMismatch` reason, `setValue('k', 2.5)` read with an `int` default returns `2`, and `setValue('k', '')` serves the in-code default value with the `valueMissing` reason. JSON objects keep their insertion order. ### Controls
Control Effect
setValue(key, value) Stores the value and, once the client is connected, delivers an update carrying only key . watch streams of key , onConfigsUpdated listeners, and reads see it after the next pumpEventQueue() .
removeValue(key) Removes the value and delivers a full update without it. Reads return the in-code default value with the configStateMissing reason, watch streams of key emit the default, and onConfigsUpdated lists key in removedKeys .
replaceValues(values) Replaces every stored value, disarms any armed hold or failure, and delivers a full update. Use it to reset a test client shared across tests.
holdInitialization() The next initialize waits until completeInitialization() or failInitialization() , or until the client's timeout elapses.
completeInitialization() Delivers the stored values to the held initialize , which completes with the client ready. Called while a hold is armed but not picked up, it disarms the hold.
failInitialization() Fails the held initialize the way an invalid SDK key does: it completes promptly, the client is not ready, and an error is logged. Called with no initialize held, it arms the next one to fail.
holdContextUpdate() , completeContextUpdate() , failContextUpdate() The same for the next updateContext .
contextUpdates The context of every initialize and updateContext call, in order. initialize without a context records an empty context, and resumeNetwork is not recorded.
The initialization controls act only on `initialize` and the context update controls only on `updateContext`; `resumeNetwork` is never held or failed. After a failed attempt, the next attempt succeeds and delivers the values stored in the meantime. ```dart [loading_test.dart] final testClient = createTestClient( values: {'new-checkout': true}, timeout: const Duration(milliseconds: 500), ); addTearDown(testClient.client.dispose); testClient.holdInitialization(); final initialization = testClient.client.initialize(); await pumpEventQueue(); expect(testClient.client.isInitializing, isTrue); testClient.completeInitialization(); await initialization; expect(testClient.client.isReady, isTrue); ``` ### Options `createTestClient` takes `values`, `timeout` (the client's connection timeout, which bounds how long a held attempt waits; defaults to the SDK's production timeout of 3 seconds), and `logger` (defaults to the SDK's `ConsoleLogger`). ### What to expect - Under the test client, `initialize` emits `onContextUpdated`, then `onClientReady`, then `onConfigsUpdated`. The first update arrives in a microtask after the connection is established, so `await client.initialize()` returns with the client ready, and listeners run after the next `pumpEventQueue()`. - A held attempt that times out, or that is interrupted by `pauseNetwork`, `dispose`, or a new attempt, leaves the client not ready until the next attempt and logs the SDK's timeout warning, which says the client will keep retrying, as production streaming would. A held `initialize` ended by `dispose` logs it too. - `watch` streams emit on subscription and then only when the evaluated value changes, as in production, so a `setValue` to the value already served emits nothing, while `onConfigsUpdated` still fires. `removeValue` and `replaceValues` deliver a full update, and only the streams whose value changed emit. - A `watch` listener that throws surfaces as an uncaught error in the test's zone and fails the test. - `pauseWhileBackgrounded` has no effect: the test client does not follow the app lifecycle, so backgrounding the app in a widget test does not pause the connection. Call `pauseNetwork` and `resumeNetwork` to test that path. - After `dispose`, the test client's controls are silent no-ops, and no timer or other resource from the SDK is left running. # React Native SDK > Install and configure the React Native SDK provider, and evaluate configs and feature flags in React Native and Expo apps ## Introduction The React Native SDK is intended to be used by React Native mobile applications. The minimum supported version of React Native is `0.75`, and the minimum supported version of Expo is `52`. ## Installation The SDK can be installed from NPM: [https://www.npmjs.com/package/@configdirector/react-native-sdk](https://www.npmjs.com/package/@configdirector/react-native-sdk) ```bash [npm] npm install --save @configdirector/react-native-sdk ``` ```bash [yarn] yarn add @configdirector/react-native-sdk ``` ```bash [pnpm]Javascript pnpm add @configdirector/react-native-sdk ``` ```bash [bun] bun add @configdirector/react-native-sdk ``` Also install `@react-native-async-storage/async-storage`, which works in Expo Go. Without it, the SDK cannot tell one app start from a new install: every start counts as one more [monthly active user](https://docs.configdirector.com/getting-started/pricing-and-billing#billing-metrics), nothing is [kept on the device](https://docs.configdirector.com/sdks/mobile/react-native#values-kept-on-the-device) for the next start, and the SDK logs a warning. ```bash [npm] npm install --save @react-native-async-storage/async-storage ``` ```bash [yarn] yarn add @react-native-async-storage/async-storage ``` ```bash [pnpm] pnpm add @react-native-async-storage/async-storage ``` ```bash [bun] bun add @react-native-async-storage/async-storage ``` ```bash [expo] npx expo install @react-native-async-storage/async-storage ``` ## Configure and initialize the provider Initialize the provider with your client SDK key: ```tsx [main.tsx] import { View } from "react-native"; import AwesomeApp from "./AwesomeApp"; import { ConfigDirectorProvider } from "@configdirector/react-native-sdk"; export default function App() { return ( ); }; ``` ### Additional configuration options These configuration options can be passed in to the `ConfigDirectorProvider`. #### `appName` and `appVersion` The `appName` and `appVersion` props allow you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application. React Native itself does not expose the app name or version, so whichever of the two you leave unset is read from [`expo-application`](https://docs.expo.dev/versions/latest/sdk/application/) or [`react-native-device-info`](https://github.com/react-native-device-info/react-native-device-info) when your app has either installed. Without one of those modules, set both props if you intend to target on them; the SDK says which one it could not find at the `info` log level. ```tsx [main.tsx] import { View } from "react-native"; import AwesomeApp from "./AwesomeApp"; import { ConfigDirectorProvider } from "@configdirector/react-native-sdk"; export default function App() { return ( ); }; ``` #### `context` The context to be used during targeting rules evaluation: ```tsx [main.tsx] import { View } from "react-native"; import AwesomeApp from "./AwesomeApp"; import { ConfigDirectorProvider } from "@configdirector/react-native-sdk"; export default function App() { return ( ); } ``` #### `logger` By default, the SDK logs to the console and it is set to log warnings and errors only. You can configure a `logger` by either creating a ConfigDirector console logger with a different log level, or by implementing the `ConfigDirectorLogger` interface to provide your own logger. The interface can be used to create an adapter to another logging library. Configure the ConfigDirector console logger to a different level: ```tsx [main.tsx] import { View } from "react-native"; import AwesomeApp from "./AwesomeApp"; import { ConfigDirectorProvider, createConsoleLogger } from "@configdirector/react-native-sdk"; export default function App() { return ( ); } ``` Implement your own logger adapter: ```tsx [main.tsx] import { View } from "react-native"; import AwesomeApp from "./AwesomeApp"; import { ConfigDirectorProvider, ConfigDirectorLogger } from "@configdirector/react-native-sdk"; export default function App() { return ( ); } ``` #### `netInfoSubscribe` Allows providing a subscription function with the same signature as `NetInfo.addEventListener` from `@react-native-community/netinfo`. This options can be used to enable immediate reconnection when the device regains network connectivity instead of waiting for the next exponential-backoff retry. For example, if a user lost network connectivity for several minutes, once they regain network connectivity there may still be a delay of minutes for the exponential backoff retry to catch up. In most cases, this is not needed and the standard retry with exponential backoff is sufficient. However, if your use case needs immediate reconnection, this can be a useful option. ```tsx [main.tsx] import { View } from "react-native"; import AwesomeApp from "./AwesomeApp"; import { ConfigDirectorProvider, ConfigDirectorLogger } from "@configdirector/react-native-sdk"; import NetInfo from '@react-native-community/netinfo'; export default function App() { return ( ); }; ``` #### `mode` The connection mode, which can be `streaming`, `polling` or `offline`. It is recommended to use the default of `streaming` unless you have a specific need to use `polling` instead. Defaults to `streaming`. Set `mode` to `offline` to run your application without connecting to ConfigDirector, for example in a suite of UI tests where each run pins every flag to a known value. An offline provider needs no `sdkKey`, opens no connection and sends nothing, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no lifecycle state changes. Every context receives the same value and targeting rules are not applied. Give the values in the `values` prop, with one value per config key. Your application decides where they come from, for example a module bundled with the test build: ```tsx [App.tsx] import { ConfigDirectorProvider } from "@configdirector/react-native-sdk"; export default function App() { return ( ); } ``` The config type of each value follows from the value itself: a boolean, an integral number (`integer`), any other number (`float`), a string, or an object or array (`json`). A value read with an in-code default value of another type returns the in-code default value, as it does against ConfigDirector. The status turns `ready` right after the provider mounts, and a new `context` keeps the same values. Returning to the foreground or regaining the network opens no connection. `values` can be given only in `offline` mode, and `pollingInterval`, `timeout` and `url` have no effect in it. With `mode="offline"` and no `values`, the provider is ready at once and every config returns its in-code default value. When the values cannot be loaded, because of a value such as `null` or because `values` is given in `streaming` or `polling` mode, the client never becomes ready: every config returns its in-code default value, the status stays `loading`, and the provider reports a `ConfigDirectorInitializationError` as an unhandled promise rejection. If you come from LaunchDarkly, a map of values is what LaunchDarkly calls "flags from files", and `offline` with no values is its "offline mode". #### `values` The values to serve in `offline` mode, one value per config key. It can be given only in `offline` mode; see [`mode`](#mode). #### `pollingInterval` The interval, in seconds, between requests for config updates when `mode` is `polling`. It has no effect in `streaming` mode. A value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. Defaults to `60` seconds. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals). ```tsx [App.tsx] import { ConfigDirectorProvider } from "@configdirector/react-native-sdk"; export default function App() { return ( ); } ``` #### `url` The base URL used to connect to ConfigDirector services. This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. ### Values kept on the device By default, the client keeps the last values it received from ConfigDirector in AsyncStorage, and serves them on the next launch of the same context until live values arrive. An application opened without a network, after at least one successful connection, shows the values ConfigDirector last sent instead of the default values provided in code. Every evaluation served this way reports the reason `cached`, with `isDefaultValue` set to `false`. The client is not ready while it serves values kept on the device: the status stays `loading` until the provider's initialization completes with live values or its `timeout`, then `default`, and `ready` once live values arrive and replace them. The `cachedValuesLoaded` hook fires when the kept values are loaded, together with `configsUpdated` listing their keys, so watchers and hooks pick them up. The values are kept for the context they were evaluated for, and served on the next launch only when the launch context has the same `kind` and `id`. A launch with another context serves the in-code default values until live values arrive, which then replace the kept values. A context without an `id` matches on the same install, as long as the app has not been uninstalled, because ConfigDirector derives its identifier from the instance ID the SDK stores; a context without an `id` keeps its percentage rollout bucket for as long as that instance ID lasts. Name, traits, app name and app version take no part in the match: live values correct them within a second when there is a network. Once the client is running, updating the context keeps serving the current values until the new context's values arrive, as it does with live values. Turn it off, or supply your own store, with the `caching` prop: - `enabled` - Whether to keep the last values received. With `false`, nothing is read or written, even when a `provider` is given. - Defaults to `true` - `provider` - A store of your own, for example an encrypted one, implementing `CachingProvider`: `read()` returns the stored entry or `undefined`, `write(value)` replaces it, `remove()` deletes it, and `sizeLimit()` returns the largest entry, in bytes, the store accepts. Each of `read`, `write` and `remove` may return a promise. One provider instance serves one client. - Defaults to AsyncStorage, under a key derived from the client SDK key, with a size limit of 256 KB ```tsx [App.tsx] import { ConfigDirectorProvider } from "@configdirector/react-native-sdk"; export default function App() { return ( ); } ``` The entry is one string whose format belongs to the SDK and may change between SDK versions; an entry a newer SDK cannot read is removed. Before every write the SDK measures the entry against `sizeLimit()`; above it, the entry is removed and a warning is logged once, so the device never serves values that no longer reflect the set. A provider that fails to read, write or remove is logged once and never stops the client: the worst outcome is the in-code default values. Keeping values on the device is ignored in `offline` mode. Without `@react-native-async-storage/async-storage` installed, the SDK logs one warning and keeps nothing on the device, as it keeps no instance ID. ## Retrieve config values Retrieve a config value with the `useConfigValue` hook: ```tsx [YourComponent.tsx] import { useConfigValue } from "@configdirector/react-native-sdk"; function YourComponent() { const { value } = useConfigValue("my-config-key", false); return

my-config-key: {value}

; } export default YourComponent; ``` You can also determine if the client is still initializing. This can be useful in the event of a slow connection where retrieving the initial config values may be slow, so rather than transition from the in-code default value to the evaluated value, you can show a loading state until the client is ready and config values are evaluated: ```tsx [YourComponent.tsx] import { useConfigValue } from "@configdirector/react-native-sdk"; function YourComponent() { const { value, loading } = useConfigValue("my-config", "default value"); if (loading) { return

Loading...

; } else { return

My config value: {value}

; } } export default YourComponent; ``` ## Update the context The app connects with the context given to the provider's `context` prop. Give it there when you already know the context as the app starts, so the first connection carries it: ```tsx [main.tsx] import { View } from "react-native"; import AwesomeApp from "./AwesomeApp"; import { ConfigDirectorProvider } from "@configdirector/react-native-sdk"; export default function App() { return ( ); }; ``` To change the context afterwards, for example when a user signs in, call `updateContext` from the `useContext` hook where the change happens. Each `updateContext` call opens a new connection with the new context: ```tsx [YourComponent.tsx] import { Button, Text, View } from "react-native"; import { useConfigValue, useContext } from "@configdirector/react-native-sdk"; function YourComponent() { const { value } = useConfigValue("my-config-key", false); const { updateContext } = useContext(); const onUpdate = async () => { await updateContext({ id: "654321", name: "Another User", traits: { region: "Australia", }, }); }; return ( my-config-key: {String(value)}