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.
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.
Everything lives under client.config (inside /try and the Mac preview the client is already injected as lingcode):
config.ready → Promise<ConfigApi>. Resolves once this client's remote config has loaded. await it before reading on first paint.config.get(key, def?) → value. Returns the configured value, or def if the key is unset. Experiment variants are delivered through get() too.config.all() → Record<string, any>. All resolved config values.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.
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 telemetry — logEvent 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.
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, ... }
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.
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.