Docs / Search / LingCode Cloud / Remote config
📘 Reference ● Guide Updated 2026-06-11

Remote config & experiments

TL;DR: Remote config lets you change how your app behaves without shipping a new build — feature flags, tunable values, and A/B experiment variants delivered from the backend at runtime. You read values in your app with lingcode.config; you flip them server-side. Always pass a default to get() so the app works before config loads.

A build is a slow, all-or-nothing way to make a decision. Hard-code new_checkout = true and you've committed every user, and the only way back is another release through the store. Remote config inverts that: the decision lives on the server, the app reads it at startup, and you can roll a flag forward, dial it back, or split traffic into experiment variants — all without touching the binary your users already have.

Why remote config

Three jobs, one mechanism. A feature flag turns a code path on or off (ship dark, enable later, kill a regression instantly). A tunable value exposes a number or a piece of copy you want to adjust without a release — a page size, a price-display string, a retry count. An experiment hands different users different variants so you can measure which one wins. All three are just key/value pairs your app reads at runtime.

The split to hold in your head: you read values in the client, you set them in the backend. The client never decides what a flag should be — it asks, gets an answer, and branches. Changing the answer is a server-side action that takes effect on the next config load, no rebuild involved.

The SDK surface

Everything lives under client.config (inside /try and the Mac preview the client is already injected as lingcode):

Reading a flag

Wait for config to load, read the flag with a sensible default, and branch:

await lingcode.config.ready;

const showNewCheckout = lingcode.config.get('new_checkout', false); // default false

if (showNewCheckout) {
  renderNewCheckout();
} else {
  renderOldCheckout();
}

The default isn't decoration — it's the value the app uses if config hasn't loaded yet or the fetch fails. Pick a default that's correct on its own (here, the existing checkout) and your app degrades gracefully no matter what the network does.

Running an experiment

An experiment is a flag with more than two outcomes: the backend assigns a variant per client, and you read it with get() and branch on it. Pair it with telemetrylogEvent on the outcome you care about — so you can measure which variant actually wins:

await lingcode.config.ready;

const variant = lingcode.config.get('checkout_button', 'control'); // 'control' | 'green' | 'urgent'

renderCheckoutButton(variant);

// later, when the user converts — attribute the outcome to the variant
async function onPurchase(amount) {
  await lingcode.telemetry.logEvent('purchase', { variant, amount });
}

Same client assignment in, outcome event out — that's the whole loop. Over time the telemetry tells you whether green or urgent beat control, and you promote the winner by flipping the config server-side.

Reading everything at once

When you want the full resolved set — to hydrate a settings screen, or to log which config a session ran under — use all():

await lingcode.config.ready;
const cfg = lingcode.config.all(); // { new_checkout: true, checkout_button: 'green', page_size: 50, ... }

What config is for — and what it isn't

Treat remote config as the home for feature flags, rollouts, copy tweaks, and tunable numbers. It is not a place for secrets. Private keys (Stripe secret, third-party API keys) belong in the Secrets vault and are read from functions, which run server-side. And lean on your defaults: the app should behave correctly with every flag at its default value, so a slow or failed config load is never a broken app.

Config values are delivered to the client. They're readable by anyone using your app — inspect the network tab and they're right there. Never put anything sensitive in a config value. Use it for behavior, not for secrets.

Under the hood (REST)

The SDK fetches config from a single endpoint, scoped to the client so each one can receive its own experiment assignments:

GET https://lingcode.dev/api/cloud/be/<backend-id>/config?client_id=<id>

The response is a flat object of key/value pairs:

{
  "new_checkout": true,
  "checkout_button": "green",
  "page_size": 50
}

If the fetch fails the SDK surfaces a config_failed error — which is exactly why every get() call should carry a default. The app keeps running on its defaults; you don't have to handle the error inline.