Dynamic values
Typed config values — strings, numbers, booleans, JSON — that change instantly without a redeploy. Schema-validated, versioned.
A dynamic value is a typed key/value pair stored on your project. Use one when the answer to "what should this be right now?" is not boolean — pricing, copy, limits, feature options, API endpoints, model names, anything you'd otherwise hard-code as a constant or stash in an environment variable.
Unlike gates, dynamic values are global per environment: every user in prod sees the same value. To vary by user, gate the code that reads the value, or use a structured config with one key per cohort.
Types
"v2", "USD", "claude-haiku-4-5".9.99, 100, 30000.Reading a value
import { configureShipeasy, flags } from "@shipeasy/sdk/server";
configureShipeasy({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
await flags.init();
const limit = flags.value<number>("rate-limit") ?? 100;
const theme = flags.value<string>("default-theme") ?? "light";
const cfg = flags.value<{ tier: string; limit: number }>("plan-defaults");The optional generic types the return value. Always provide a fallback — your code should never crash because the SDK hasn't initialised yet, or because someone deleted the value in the dashboard.
Browser:
import { configureShipeasy } from "@shipeasy/sdk/client";
const client = configureShipeasy({ apiKey });
const cfg = client.getValue<{ tier: string; limit: number }>("plan-defaults");Same plumbing as
flags.gate() — the bundle is in memory after init(), so reads are a hash table lookup. There is no
async, no Promise, no fetch.
Updating a value
Dashboard: Configs → Dynamic values → click → edit value → Save. The change is in KV in <100 ms and visible to your SDK on the next poll (default 30s; faster on Pro).
CLI:
shipeasy values set rate-limit 200
shipeasy values set default-theme "dark"
shipeasy values set plan-defaults --json '{"tier":"pro","limit":50}'A new value creates a revision; you can roll back from the dashboard at any time. Every revision records the author, source (dashboard / CLI / API), and a diff against the previous value.
# View revision history
shipeasy values history rate-limit
# Roll back to a specific revision
shipeasy values rollback rate-limit --revision 7Structured configs
A single JSON value can encode an entire decision tree. This is great for things like plan limits, where the answer is "a struct" rather than "a value":
{
"free": { "max_seats": 3, "history_days": 7 },
"pro": { "max_seats": 25, "history_days": 90 },
"ent": { "max_seats": null, "history_days": 365 }
}type PlanCfg = Record<string, { max_seats: number | null; history_days: number }>;
const cfg = flags.value<PlanCfg>("plan-defaults");
const seats = cfg?.[user.plan]?.max_seats ?? 3;This collapses N flags into one JSON config that's atomically updated — no race conditions where the user sees half a new config and half the old one. The KV write is a single object, and the SDK either has the old version or the new one.
Other classic shapes:
{
"default": "claude-haiku-4-5",
"premium": "claude-opus-4-7",
"fallback": "claude-haiku-4-5",
"timeout_ms": 30000
}{
"headline": "Ship features 10× faster",
"sub": "Flags, configs, and experiments — one SDK.",
"cta_label": "Start free"
}Validation
When you create a value you can attach a schema:
- Type: one of
string/number/boolean/json. - For
json: optionally a Zod schema, stored as a JSON Schema document on the project.
If a write would violate the schema, the dashboard, the CLI, and the API all reject it before it reaches KV. Bad config never goes live.
The SDK still applies a runtime decoder you supply, as a defence-in-depth:
import { z } from "zod";
const Schema = z.object({ tier: z.string(), limit: z.number() });
const cfg = flags.value("plan-defaults", {
decode: (raw) => Schema.parse(raw),
});The SDK calls decode once per fetched blob — invalid payloads are dropped (with a console warning) and the previous good value is kept. Your code never sees a malformed config.
Three properties together that JSON-on-disk doesn't give you. A bad value never reaches production. A good value is in production in seconds. A great value can be reverted in one click.
When to use a value vs. a gate
| Question | Use |
|---|---|
| Should I show feature X? | Gate. |
| What should the value of N be? | Dynamic value. |
| Different value per user / cohort? | Gate around the read, or structured value keyed by cohort. |
| Need an audit trail? | Both. Every write is versioned. |
| Need a kill-switch? | Killswitch. |
A useful rule: if you'd say "show feature X", gate it. If you'd say "set feature X to value Y", use a dynamic value.
CI snippet
Validate every value reference in your code resolves on the project:
- name: Validate dynamic values
run: shipeasy values validate ./srcThis catches typos like rate_limit vs rate-limit and stale references after a value has been deleted.
Limits
| Plan | Values per project | JSON size per value |
|---|---|---|
| Free | 25 | 4 KB |
| Pro | 250 | 64 KB |
| Enterprise | Unlimited | 256 KB |
If your JSON is approaching the limit, you're probably storing data that belongs in a database, not a config. Configs are designed for "a few KB of decisions per project", not "the user's shopping cart".
Add targeting rules to a gate.
Now that you've seen typed values, see how gates layer rules and rollout to vary behaviour per user — deterministically, with no flicker.
Quickstart
From zero to a flag-gated feature in production — install the SDK, create a gate, roll it out, kill-switch it, and watch it in the dashboard. Five minutes, end to end.
Targeting & rollouts
Express "show this to the right users" with attribute rules, deterministic percentage rollouts, and per-gate salts. The rules engine, in detail.