How wagle works, in 30 seconds
Everything below assumes this loop. Your app holds a whole, versioned config snapshot and evaluates against it in-process; wagle's job is to get the next version to you cheaply.
Mint a token
In the console, mint an SDK token: a durable, revocable credential, read-only by design. Store it in WAGLE_API_TOKEN.
Seed once
The SDK pulls one whole (project, environment) snapshot with its version - flags, segments, rules, all of it.
Evaluate locally
Every evaluation is an in-process read of that snapshot: hash the entity into a bucket, walk the rules, serve a variation. No network call, ever.
Poll for changes
The SDK re-checks on your interval (default 30s). An unchanged poll gets a tiny “nothing new” answer, so short intervals stay cheap.
Mint an SDK token, exchange it for access
Programmatic access starts with an SDK token: a logged-in console user mints it once (Settings → SDK Tokens, admin-gated), you store it in WAGLE_API_TOKEN, and the SDK exchanges it for short-lived access tokens automatically. It is read-only - eval and sync only.
# Mint an SDK token: a durable, revocable REFRESH credential returned ONCE.
# Put it in WAGLE_API_TOKEN; the SDK exchanges it for short access tokens.
# User token, admin-gated. Omit "class" for backend trust; pass
# "TRUST_CLASS_FRONTEND" for a browser bundle that must never see a server_only flag.
curl -sS $WAGLE_URL/api.sdk.v1.SdkTokenService/CreateSdkToken \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","name":"web-app"}'
# -> {"token":"<refresh-credential, shown once>","models":{"sdkTokens":[...]},"graphs":[...]}# Programmatic access uses an SDK token: a durable, revocable REFRESH
# credential a logged-in console user mints once (CreateSdkToken — see below),
# stored in WAGLE_API_TOKEN. It is READ-ONLY: eval + sync only (SPEC D46).
# The SDK exchanges it for short 24h access tokens automatically; here is that
# exchange by hand — a PUBLIC call, the credential in the body is the auth.
curl -sS $WAGLE_URL/api.sdk.v1.SdkTokenService/RefreshSdkToken \
-H 'Content-Type: application/json' \
-d '{"refreshToken":"$WAGLE_API_TOKEN"}'
# -> {"accessToken":"<jwt, 24h>"}
# Pass that access token as Authorization: Bearer on eval / sync.- The SDK token is a durable, revocable refresh credential, returned once. It can never call an admin RPC - admin writes take a user session or an agent token.
- Trust is a package choice, not a per-call flag: @wagleflags/sdk and the Go SDK are backend-trust; @wagleflags/sdk/frontend is the browser twin and never receives a server-only flag - pair it with a frontend-class token.
Go SDK: seed once, evaluate locally
wagle.New blocks until the first seed lands and fails fast on a seed error, so the returned client is ready. Each evaluate is a pure, lock-free read of the held snapshot - zero network hop.
// go get github.com/unirakun/wagle-sdks/go
import wagle "github.com/unirakun/wagle-sdks/go"
// The SDK owns its transport: it dials https://api.wagle.sh over TLS and reads
// its bearer from WAGLE_API_TOKEN — the once-shown token from the console's
// "SDK Tokens" settings card. project is the opaque project UUID (D44), not its
// display name. New blocks until the first seed lands (fails fast on error),
// so the returned client is ready to Evaluate.
client, err := wagle.New("baseline", "production")
if err != nil {
// initial seed failed
}
defer client.Close()
// Point at another server / pass the token explicitly / dial local h2c:
// client, err = wagle.New("baseline", "production",
// wagle.WithURL("$WAGLE_URL"), // default https://api.wagle.sh
// wagle.WithToken(os.Getenv("MY_TOKEN")), // default WAGLE_API_TOKEN
// wagle.WithInsecure()) // cleartext h2c for local dev// A pure, local, lock-free read of the held snapshot — zero network hop.
// attrs may be nil (empty context).
res, err := client.Evaluate("new-checkout", "user-42", map[string]string{"plan": "pro"})
if err != nil {
// a pinned eval error, e.g. flag not found
}
// res.VariationKey / res.Value / res.Reason / res.ValueTypeTypeScript SDK: backend, browser, React
createWagleClient is backend-trust. For an untrusted bundle use @wagleflags/sdk/frontend: the same seed, poll and local-eval loop, but a server-only flag never enters its store. Hooks take the client as an argument and re-render when a poll swaps the snapshot.
// pnpm add @wagleflags/sdk
import { createWagleClient } from "@wagleflags/sdk";
const client = createWagleClient({
url: "$WAGLE_URL",
project: "baseline",
environment: "production",
token: process.env.WAGLE_API_TOKEN, // the SDK refresh credential (SPEC D46)
});
await client.ready(); // resolves after the first seed
client.close(); // stop the pump when done// A pure, local read of the held snapshot — zero per-eval network hop.
const res = client.evaluate("new-checkout", {
entityId: "user-42",
attributes: { plan: "pro" },
});
// res is a Trace ({ variationKey, value, reason, ... })
// or an EvaluationError — narrow with isEvalError.import { useFlag } from "@wagleflags/sdk/react";
function Checkout({ client }: { client: WagleClient }) {
// Re-renders live when a poll swaps the snapshot; evaluates locally.
const value = useFlag(client, "new-checkout", {
entityId: "user-42",
attributes: { plan: "pro" },
});
return value === "true" ? <NewCheckout /> : <OldCheckout />;
}
// useFlag -> the served value string (undefined when unresolved);
// useEvaluate -> the full Trace | EvaluationError (reason, bucket, ...).// Untrusted client bundle (web/mobile)? Use the BROWSER-trust twin: the
// same seed -> poll -> local-eval loop, but a server_only flag never
// enters its store. Pair it with a frontend-class SDK token.
import { createWagleFrontendClient } from "@wagleflags/sdk/frontend";
const client = createWagleFrontendClient({
url: "$WAGLE_URL",
project: "baseline",
environment: "production",
token: "<frontend-sdk-token>",
});The versioned poll: your freshness-vs-cost dial
After the seed the SDK re-checks sync.Get on an interval (default 30s), sending the version it holds: an unchanged poll is answered with a tiny response, so short intervals stay cheap. The interval is per client - 5–30s for interactive apps, 60–300s for batch.
// New seeds once from SyncService.Get, then re-checks on an interval.
// Each poll sends the held version; when nothing changed the server answers
// with a tiny "unchanged" response, so short intervals stay cheap. The
// interval is your freshness-vs-cost dial: 5-30s interactive, 60-300s batch.
client, err := wagle.New("baseline", "production",
wagle.WithPollInterval(15*time.Second), // default 30s
)
if err != nil {
// initial seed failed
}
defer client.Close()
v := client.Version() // the held (project, environment) config version// createWagleClient seeds once via sync.Get, then re-checks on an
// interval. Each poll sends the held version; when nothing changed the server
// answers with a tiny "unchanged" response, so short intervals stay cheap.
const client = createWagleClient({
url: "$WAGLE_URL",
project: "baseline",
environment: "production",
token: process.env.WAGLE_API_TOKEN,
pollIntervalMs: 15_000, // the freshness-vs-cost dial (default 30_000)
});
await client.ready();
client.getSnapshot().version; // the held config versionSegments: reusable targeting, resolved locally
A segment is a named predicate over context attributes (plan in [pro, enterprise]), reusable across flags. Segments ride down in the same snapshot as the flags, so a rule that targets one still evaluates in-process - no extra call.
# A segment is a reusable named predicate over context attributes. Conditions
# are ANDed; operators are OPERATOR_EQ / OPERATOR_IN here (no nesting).
curl -sS $WAGLE_URL/api.admin.v1.AdminService/CreateSegment \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{
"project": "baseline",
"environment": "production",
"segment": {
"key": "beta-testers",
"conditions": [
{"attribute": "plan", "op": "OPERATOR_IN", "values": ["pro", "enterprise"]}
]
}
}'
# -> {"models":{"segments":[...]},"graphs":[...],"version":"..."}
# Reference it from a flag rule — operator IN_SEGMENT, values = [segment key]:
# "rules": [
# {"conditions": [{"op": "OPERATOR_IN_SEGMENT", "values": ["beta-testers"]}],
# "variationKey": "on"}
# ]// Segments arrive in the seed snapshot alongside the flags, so a rule that
// targets a segment (operator IN_SEGMENT) is resolved LOCALLY — zero extra
// network hop, same as any other rule. You just evaluate.
res, err := client.Evaluate("new-checkout", "user-42", map[string]string{"plan": "pro"})
if err != nil {
// a pinned eval error
}
// When the entity's attributes place it in a targeted segment, res.Reason is
// the matched rule's Reason enum: TARGET_MATCH (fixed variation) or
// ROLLOUT (a percentage split).- Segments are one level deep on purpose - a segment can't reference another segment. Compose by ANDing segments in a rule.
The HTTP API: every RPC is plain JSON over POST
The backend speaks Connect over plain HTTP, so curl is a real client: every RPC is a POST of camelCase JSON to /api.<svc>.v1.<Service>/<Method> with Authorization: Bearer. Create a flag (admin - user token), evaluate, and seed, straight from a terminal.
curl -sS $WAGLE_URL/api.admin.v1.AdminService/CreateFlag \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{
"project": "baseline",
"environment": "production",
"flag": {
"key": "new-checkout",
"valueType": "VALUE_TYPE_BOOL",
"variations": [
{"key": "on", "value": "true"},
{"key": "off", "value": "false"}
],
"environments": {
"production": {"enabled": true, "defaultVariationKey": "on", "offVariationKey": "off"}
}
}
}'
# -> {"models":{"flags":[...]},"graphs":[...],"version":"1"}curl -sS $WAGLE_URL/api.eval.v1.EvaluationService/Evaluate \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production","flagKey":"new-checkout","context":{"entityId":"user-42","attributes":{"plan":"pro"}}}'
# -> {"variationKey":"on","value":"true","reason":"REASON_DEFAULT","valueType":"VALUE_TYPE_BOOL"}# Initial seed: the whole (project, environment) snapshot + its version.
curl -sS $WAGLE_URL/api.sync.v1.SyncService/Get \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production"}'
# -> {"models":{"flags":[...],"segments":[...]},"graphs":[...],"version":"1"}
# Efficient poll: send the version you hold — an unchanged poll returns a
# tiny no-change response instead of the whole snapshot.
curl -sS $WAGLE_URL/api.sync.v1.SyncService/Get \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production","sinceVersion":"1"}'
# -> {"version":"1","unchanged":true}
# The SDKs poll this same Get on an interval (default 30s, tunable):
# the freshness-vs-cost dial. An unchanged poll costs almost nothing.- The example project is the placeholder baseline; your real project is an opaque UUID - discover it via ListProjects.
- A uint64 (like a config version) serializes as a JSON string - that's protojson, not a quirk to work around.
Audit history: who changed what, exactly
Every write is atomic, bumps one per-project version and appends a history entry attributed to the authenticated actor. Read it back per project or per flag - each entry carries the full snapshot at that version, so consecutive entries diff cleanly.
# Project audit view: the latest version per entity, grouped by type.
curl -sS $WAGLE_URL/api.admin.v1.AdminService/GetHistory \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production"}'
# -> {"history":[{"type":"HISTORY_TYPE_FLAG_VERSION","entries":{...}}, ...],"version":"..."}
# One flag generation's full write timeline (oldest first), fetched by the
# flag's immutable UUID (flagId — SPEC D54; ids come from ListFlags). Each entry
# carries the protojson snapshot at that version for consecutive-version diffs.
# ("key":"new-checkout" is the compat request instead: it resolves the NEWEST
# generation only — a recreated key never concatenates its predecessor's rows.)
curl -sS $WAGLE_URL/api.admin.v1.AdminService/GetFlagHistory \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $WAGLE_TOKEN" \
-d '{"project":"baseline","environment":"production","flagId":"<flag-uuid>"}'
# -> {"entries":[{"sourceId":"baseline/new-checkout","entityId":"<flag-uuid>",
# "entityKey":"new-checkout","createdBy":"...","snapshot":"{...}"}, ...]}
# (protojson omits zero values: "deleted":true appears only on delete rows.)Errors are pinned bytes, not prose
Errors are Connect JSON - a snake_case code, a message, a mapped HTTP status. Evaluate codes and messages are pinned, stable across releases: clients may depend on the exact bytes, and both SDKs surface the same code and message the wire does.
# Errors are Connect JSON — {"code":"<snake_case>","message":"..."} with a
# mapped HTTP status. Eval/sync/admin are authenticated, so the gate answers
# first: with NO bearer token you get 401 unauthenticated. This needs no
# credential — Run it:
curl -sS $WAGLE_URL/api.eval.v1.EvaluationService/Evaluate \
-H 'Content-Type: application/json' \
-d '{"project":"baseline","environment":"production","flagKey":"missing"}'
# -> HTTP 401 {"code":"unauthenticated","message":"missing bearer token"}
# Once authenticated (against your OWN project) the pinned Evaluate errors show —
# these messages are stable across releases, clients may depend on the bytes:
# missing flag: HTTP 404 {"code":"not_found","message":"flag not found: baseline/production/missing"}
# no environment: HTTP 400 {"code":"invalid_argument","message":"environment is required"}MCP: drive your flags from an AI agent
The admin surface is exposed to agents over a Streamable-HTTP MCP endpoint at /mcp - 15 tools covering flag and segment CRUD, rollouts, history and evaluation. Don't script tool calls: connect a host and ask in plain language.
{
"mcpServers": {
"wagle-admin": {
"type": "http",
"url": "$WAGLE_URL/mcp",
"headers": { "Authorization": "Bearer <refresh-credential>" }
}
}
}# Claude Code: register the endpoint once, then just ask.
claude mcp add --transport http wagle-admin \
$WAGLE_URL/mcp \
--header "Authorization: Bearer <refresh-credential>"
# The agent discovers the tools itself - you talk product, it picks the tool:
> Which flags are enabled in production, and which are still fully off?
> Turn new-checkout off in production.
> Roll new-checkout out to 25% in production, everyone else stays off.
> Create a bool flag "maintenance-banner" in production, default off.
> Who changed new-checkout recently, and what exactly changed?- The host's bearer is the agent's long-lived refresh credential (minted in Settings → Agents) - the server exchanges it in-process for short access tokens, and revoking the agent cuts it off.
- MCP is agent-only: a user token on the /mcp path is refused with permission_denied, so don't debug that blind.
Frequently asked questions
Is there a REST API for feature flags?
Yes - every RPC is a plain POST of JSON over HTTP (the Connect protocol), so curl is a real client. No gRPC toolchain, no SDK required: create flags, evaluate, and sync against /api.<service>.v1 endpoints with a bearer token.
Does evaluating a flag make a network call?
No. The SDK seeds an in-memory snapshot once, then every evaluation is a local, in-process read - zero network hops, no database on the hot path. Local evaluations are free and unlimited; plans price sync volume, never evaluations.
How fast do flag changes reach my app?
On the next poll: each SDK re-checks on an interval you choose (default 30 seconds, tunable per client). Changes are eventually consistent - fast, but not an atomic fleet-wide flip in the same millisecond.
What happens if wagle is unreachable?
Your app keeps serving the last snapshot it saw - stale-but-available, reads never fail. Freshness resumes on the next successful poll. The cost: “turn this off now” is bounded by your poll interval.
Which SDKs does wagle have?
Go and TypeScript for backends, a browser-trust twin (@wagleflags/sdk/frontend) that never receives server-only flags, and React hooks. All evaluate byte-identically - one normative algorithm, pinned by a shared vector file in CI. No mobile SDKs today.
Can an AI agent manage my feature flags?
Yes. The admin surface is exposed over MCP (Streamable HTTP) at /mcp: 15 tools covering flag and segment CRUD, percentage rollouts, history and evaluation. Access is agent-only, authenticated by a revocable credential.