flagdash / sdk
Official server-side FlagDash SDK for PHP
Requires
- php: ^8.1
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Feature flags, remote config, AI configs, translations and experiments for PHP 8.1+.
Dependency-free: the default transport uses PHP streams, so there is nothing to install beyond the package itself.
Installation
composer require flagdash/sdk
To install straight from GitHub, add https://github.com/flagdash/flagdash-php
as a Composer VCS repository and require the immutable tag v0.1.0.
Quick start
use FlagDash\Client; $client = new Client($_ENV['FLAGDASH_SDK_KEY']); if ($client->flag('checkout-v2', false, ['user_id' => 'alice'])) { // new checkout } $client->close();
API key tiers
The key decides which project and environment you read, and what you may reach.
There is no environment parameter anywhere in this SDK — the key carries it.
| Key | Prefix | Reaches |
|---|---|---|
| Client | pk_ |
Flag values and configs. Safe in a browser or mobile app. |
| Server | sk_ |
Everything the client key reaches, plus targeting rules, translations and experiments. Keep it on your server. |
Anything below marked server key returns nothing useful with a client key.
Configuration
$client = new Client( sdkKey: $_ENV['FLAGDASH_SDK_KEY'], baseUrl: 'https://flagdash.io', // self-hosted? point it here timeout: 5.0, // seconds per request cacheTtl: 60.0, // seconds; 0 disables caching region: 'eu-west-1', // omit to auto-detect transport: new StreamTransport(), );
region is detected from FLY_REGION, AWS_REGION and friends when you leave
it out, so region-scoped targeting rules work with no wiring. transport takes
any FlagDash\Transport, which is where a PSR-18 client or a test double goes.
Feature flags
// A single flag, with a fallback used whenever FlagDash cannot be reached. $enabled = $client->flag('checkout-v2', false, ['user_id' => 'alice']); // Every flag for this context, in one request. $flags = $client->allFlags(['user_id' => 'alice', 'country' => 'GB']); // Why did it resolve that way? (server key) $detail = $client->flagDetail('checkout-v2', false, ['user_id' => 'alice']); // ['value' => true, 'reason' => 'rule_match', 'variation' => 'treatment', ...] // Flag metadata without evaluating anything (server key). $all = $client->listFlags();
Pass a user_id (or key) in the context whenever you want a stable
answer. Percentage rollouts and A/B variations hash that identifier, so an
anonymous context re-rolls on every call by design.
Context is a plain array. Any attribute you send can be targeted on:
$client->flag('beta-banner', false, [ 'user_id' => 'alice', 'country' => 'GB', 'plan' => 'premium', 'email' => 'alice@example.com', ]);
Remote config
$limit = $client->config('rate_limit', 100); $all = $client->listConfigs();
AI configs
Prompts, agents, skills and rules, versioned per environment and editable without a deploy.
$prompt = $client->aiConfig('support-agent.md'); $files = $client->listAiConfigs();
Translations (server key)
$greeting = $client->translation('checkout.greeting', 'fr', 'Hello', [ 'name' => 'Alice', ]);
The key is namespace.message. {placeholders} are filled from the variables
array, and the default is returned whenever the catalogue, the namespace or the
message is missing — a translation lookup never throws.
Experiments (server key)
$assignment = $client->experiment('checkout-redesign', ['user_id' => 'alice']); if ($assignment !== null && $assignment['variant'] === 'treatment') { // ... } $client->trackExperimentMetric( experimentKey: 'checkout-redesign', eventName: 'purchase', userId: 'alice', value: 42.50, properties: ['currency' => 'GBP'], );
experiment() returns null for a context with no identifier — an assignment
that cannot be stable is worse than no assignment.
Metrics are buffered in memory (up to 1000) and sent by flush(), which
close() calls for you:
$client->flush(); // send now $client->close(); // flush and release the transport
In a long-running worker, call flush() periodically. In a request/response
process, close() at the end of the request is enough.
Caching
Responses are cached in memory for cacheTtl seconds (60 by default), so a
burst of flag() calls costs one request. clearCache() empties it.
$client->clearCache();
Failure behaviour
Every read returns the default you passed rather than throwing, so an outage
degrades to your fallback values instead of an exception in a request path.
FlagDash\FlagDashException surfaces only for programming errors such as an
empty SDK key.
Security
Keep the server key on the server. A key granted only the scopes it needs limits the blast radius if it leaks, and a client key never returns targeting rules — the browser cannot see who else you are targeting.
License
MIT
Backend session replay
$replay = new FlagDash\BackendReplay($_ENV['FLAGDASH_REPLAY_KEY'], release: '2026.08'); if ($replay->start()) { $replay->event('checkout_started', attributes: ['items' => 2]); $replay->breadcrumb('payment requested'); $replay->stop(); }
The recorder captures only events you add. It redacts sensitive attribute keys, buffers bounded batches, and exposes contextHeaders() for cross-service correlation. Use a replays:write key.
AI releases
Create a release in Manage → AI Releases, select the environment, then set
a baseline, candidate and rollout. With your initialized client, evaluate it using
an environment-bound key with ai_configs:read:
$release = $client->aiConfigRelease("support-agent", "usr_123");
The result includes key, version, config, reason, variation_key, and
rollout_percentage (idiomatic field names for typed SDKs). Pass a stable user
identity. Decisions are fetched afresh; verify baseline, rollout and paused
behavior in development before ramping production. Your backend calls the AI
provider. Ordinary evaluation leaves secret references unresolved; never put
provider credentials in a configuration delivered to browsers or mobile apps.
See the release guide for lifecycle and cleanup.
Remote config value format
Remote config storage and server metadata use exactly one {"value": ...}
envelope. Convenience config reads return the inner application value, without
recursively unwrapping application fields named value. For example, stored
{"value": {"value": 7, "enabled": false}} reads as
{"value": 7, "enabled": false}. Metadata methods retain the envelope.
MCP and management writes accept the envelope and automatically wrap bare objects.
To store an application object whose only key is value, send
{"value": {"value": 7}}. Arrays and scalar values must be inside the envelope.
Existing bare stored objects are migrated, preserving secret-reference bindings.
Server SDK callers previously receiving the envelope should remove their extra
.value access when upgrading. Verify in a development environment first.