Stitchd Feature Flag
Stitchd Feature Flag is a self-hosted platform for feature flagging and experimentation.
This documentation covers:
- REST API — Admin API reference (auto-generated from OpenAPI annotations)
- gRPC / Protobuf — SDK protocol reference (auto-generated from
.protofiles) - Rust SDK — Server-side SDK usage guide and API reference
- Deployment — Self-hosting guide for PostgreSQL + ClickHouse
- Architecture — System design and data flow diagrams
Gateway Overview
The stitchd-gateway is the single public entry point for all stitchd traffic. It is an Axum-based HTTP/gRPC server that authenticates requests, then proxies them to the appropriate internal gRPC service.
Ports
| Protocol | Default Port | Purpose |
|---|---|---|
| HTTP/REST | 8080 | SDK evaluation, event ingestion, admin management |
| gRPC | 50050 | Definition-sync streaming (FlagSyncService) for SDK clients |
Routing Rules
Incoming requests are dispatched by path prefix:
| Path Prefix | Auth | Upstream Service |
|---|---|---|
POST /v1/auth/login | none | stitchd-auth-service |
/v1/admin/** | Bearer JWT (system-org only) | stitchd-auth-service |
/v1/management/** | Bearer JWT | stitchd-auth-service / stitchd-flag-service |
/v1/environments/{id}/evaluate | x-sdk-key | stitchd-flag-service |
/v1/environments/{id}/events | x-sdk-key or Bearer JWT | stitchd-event-service |
/v1/environments/{id}/segments/** | x-sdk-key or Bearer JWT | stitchd-segmentation-service |
/v1/environments/{id}/flags/** | Bearer JWT | stitchd-flag-service |
/v1/environments/{id}/experiments/** | Bearer JWT | stitchd-experimentation-service |
/v1/environments/{id}/event-definitions/** | Bearer JWT | stitchd-event-service |
Auth Header Matrix
| Route group | Header required | Value |
|---|---|---|
| SDK routes (evaluate, ingest events, list-check) | x-sdk-key | Environment SDK key |
| Admin & management routes | Authorization | Bearer <jwt> |
| Flag / segment / event / experiment CRUD | Authorization | Bearer <jwt> |
| Login | — | none |
The gateway validates JWT tokens by calling stitchd-auth-service.ValidateToken. SDK keys are verified against the environment record in stitchd-flag-service.
Error Envelope
All REST error responses use a JSON envelope:
{
"error": "human-readable message",
"code": "GRPC_STATUS_NAME"
}
HTTP status codes map from gRPC status codes:
| gRPC Status | HTTP Status |
|---|---|
NOT_FOUND | 404 |
UNAUTHENTICATED | 401 |
PERMISSION_DENIED | 403 |
INVALID_ARGUMENT | 400 |
UNAVAILABLE | 502 |
INTERNAL | 500 |
What’s New Since the Monolith
The gateway adds the following endpoints that did not exist in stitchd-server:
| Endpoint | Description |
|---|---|
POST /v1/environments/{id}/events/batch | Bulk event ingestion in a single request |
POST /v1/environments/{id}/segments/batch-list-check | Bulk segment membership check |
GET/POST/PUT/DELETE /v1/environments/{id}/experiments/** | Full experimentation CRUD |
GET/POST/PUT/DELETE /v1/environments/{id}/event-definitions/** | Event definition management |
PUT /v1/environments/{id}/flags/{key}/hashing | Per-flag hashing configuration |
SDK APIs
REST endpoints consumed by the stitchd SDK. All SDK routes authenticate via the x-sdk-key header — no JWT required.
Auth Model
Include the environment’s SDK key in every request:
x-sdk-key: sdk_live_abc123...
SDK keys are scoped to a single environment. A request with an invalid or missing key returns 401 Unauthorized.
Endpoints
Evaluate Flag
POST /v1/environments/{env_id}/evaluate
Evaluate a feature flag for a context.
Request body:
{
"flag_key": "my-flag",
"context_type": "user",
"context_key": "user-123",
"attributes": {
"plan": "pro",
"country": "US"
}
}
Response:
{
"flag_key": "my-flag",
"variant_key": "treatment",
"is_enabled": true
}
Ingest Event
POST /v1/environments/{env_id}/events
Record a single metric event.
Request body:
{
"metric_key": "button_click",
"context_type": "user",
"context_key": "user-123",
"value": true,
"timestamp_ms": 1714000000000
}
value is optional and can be a boolean, integer, or float. timestamp_ms defaults to server-received time if omitted.
Response:
{
"accepted_count": 1,
"rejected_keys": []
}
Batch Ingest Events
POST /v1/environments/{env_id}/events/batch
Record multiple events in a single request.
Request body:
{
"events": [
{ "metric_key": "page_view", "context_type": "user", "context_key": "u1" },
{ "metric_key": "purchase", "context_type": "user", "context_key": "u1", "value": 49.99 }
]
}
List-Check Segment Membership
POST /v1/environments/{env_id}/segments/list-check
Check whether a context is a member of a list segment.
Request body:
{
"segment_key": "beta-users",
"context_type": "user",
"context_key": "user-123"
}
Response:
{
"is_member": true
}
Batch List-Check Segment Membership
POST /v1/environments/{env_id}/segments/batch-list-check
Check membership for multiple (segment, context) pairs in one call.
Error Envelope
Errors follow the standard gateway envelope:
{ "error": "sdk key not found", "code": "UNAUTHENTICATED" }
Rate Limits
SDK routes are designed for high-throughput SDK usage. No explicit rate limits are enforced by the gateway itself; operators should place a reverse proxy (e.g., nginx, envoy) in front for production rate limiting.
Gateway gRPC
The gateway exposes a single gRPC service for SDK clients that need real-time flag definition streaming: FlagSyncService.
Service
| Service | Proto package | Gateway port |
|---|---|---|
FlagSyncService | flags.v1 | 50050 |
This is a passthrough — the gateway forwards the streaming RPC directly to stitchd-flag-service.
RPCs
SyncDefinitions
rpc SyncDefinitions(SyncRequest) returns (stream SyncResponse);
Opens a server-streaming connection. The gateway pushes the full flag/segment definition set on connect, then streams incremental updates as definitions change.
Auth: Pass the SDK key as gRPC metadata:
x-sdk-key: sdk_live_abc123...
SyncRequest fields:
| Field | Type | Description |
|---|---|---|
environment_id | string | Environment to subscribe to |
SyncResponse fields:
| Field | Type | Description |
|---|---|---|
flags | repeated FlagDefinition | Current set of flag definitions |
segments | repeated SegmentDefinition | Current set of segment definitions |
sequence_number | int64 | Monotonically increasing sync counter |
Connecting from the Rust SDK
The SDK automatically opens the streaming connection on SdkClient::connect(). You do not need to manage the gRPC channel directly.
#![allow(unused)]
fn main() {
let client = SdkClient::builder()
.gateway("http://localhost:50050")
.sdk_key("sdk_live_abc123")
.build()
.await?;
}
Connecting from Other Languages
Use any gRPC client with the flags/v1/flag_sync.proto definition. Set the x-sdk-key metadata key on the channel.
import grpc
from flags.v1 import flag_sync_pb2_grpc, flag_sync_pb2
channel = grpc.insecure_channel("localhost:50050")
stub = flag_sync_pb2_grpc.FlagSyncServiceStub(channel)
metadata = [("x-sdk-key", "sdk_live_abc123")]
for response in stub.SyncDefinitions(
flag_sync_pb2.SyncRequest(environment_id="env-1"),
metadata=metadata
):
print(response)
See the gRPC Protobuf Reference for the full proto schema.
Human JWT APIs
REST endpoints consumed by the Admin UI and operator tooling. All routes require a valid Bearer JWT in the Authorization header.
Auth Model
- Obtain a JWT by posting credentials to
POST /v1/auth/login. - Include the token in subsequent requests:
Authorization: Bearer eyJhbGci...
Tokens expire after the configured TTL (default: 24 hours). A 401 Unauthorized response means the token is missing, invalid, or expired.
Login
POST /v1/auth/login
Request body:
{
"email": "admin@example.com",
"password": "hunter2"
}
Response:
{
"token": "eyJhbGci...",
"expires_at": "2026-04-23T10:00:00Z"
}
Admin Endpoints (system-org only)
These routes are only accessible to users belonging to the system organisation.
| Method | Path | Description |
|---|---|---|
POST | /v1/admin/orgs | Create a new organisation |
POST | /v1/admin/seed-user | Bootstrap the first admin user |
Management Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v1/management/projects | Create a project |
POST | /v1/management/projects/{id}/environments | Create an environment within a project |
POST | /v1/management/environments/{id}/sdk-keys | Issue a new SDK key |
POST | /v1/management/users | Create a user account |
Flag Management
| Method | Path | Description |
|---|---|---|
GET | /v1/environments/{env_id}/flags | List all flags |
POST | /v1/environments/{env_id}/flags | Create a flag |
GET | /v1/environments/{env_id}/flags/{key} | Get a flag |
PUT | /v1/environments/{env_id}/flags/{key} | Update a flag |
DELETE | /v1/environments/{env_id}/flags/{key} | Delete a flag |
POST | /v1/environments/{env_id}/flags/{key}/variants | Add a variant |
PUT | /v1/environments/{env_id}/flags/{key}/rules | Replace targeting rules |
PUT | /v1/environments/{env_id}/flags/{key}/hashing | Update hashing config |
Segment Management
| Method | Path | Description |
|---|---|---|
GET | /v1/environments/{env_id}/segments | List all segments |
POST | /v1/environments/{env_id}/segments | Create a segment |
GET | /v1/environments/{env_id}/segments/{key} | Get a segment |
PUT | /v1/environments/{env_id}/segments/{key} | Update a segment |
DELETE | /v1/environments/{env_id}/segments/{key} | Delete a segment |
Event Definition Management
| Method | Path | Description |
|---|---|---|
GET | /v1/environments/{env_id}/event-definitions | List event definitions |
POST | /v1/environments/{env_id}/event-definitions | Create an event definition |
GET | /v1/environments/{env_id}/event-definitions/{key} | Get a definition |
PUT | /v1/environments/{env_id}/event-definitions/{key} | Update a definition |
DELETE | /v1/environments/{env_id}/event-definitions/{key} | Delete a definition |
Experiment Management
| Method | Path | Description |
|---|---|---|
GET | /v1/environments/{env_id}/experiments | List experiments |
POST | /v1/environments/{env_id}/experiments | Create an experiment |
GET | /v1/environments/{env_id}/experiments/{id} | Get an experiment |
PUT | /v1/environments/{env_id}/experiments/{id} | Update an experiment |
DELETE | /v1/environments/{env_id}/experiments/{id} | Delete an experiment |
POST | /v1/environments/{env_id}/experiments/{id}/transition | Transition experiment state |
GET | /v1/environments/{env_id}/experiments/{id}/iterations | List iterations |
GET | /v1/environments/{env_id}/experiments/{id}/results | Get statistical results |
OpenAPI / Swagger UI
The full machine-readable spec is available at:
- Raw JSON:
/api/openapi.json(served by mdBook or the docs build) - Interactive UI: See OpenAPI Spec for how to run a local Swagger UI
For complete request/response schemas, consult the OpenAPI Spec.
OpenAPI Spec
The gateway publishes an OpenAPI 3.0 specification that describes every REST endpoint, request schema, response schema, and security requirement.
Viewing the Spec
The generated spec is committed to the repository at docs/src/api/openapi.json. It is regenerated automatically by cargo xtask docs.
To browse it interactively, run a local Swagger UI:
# Using Docker
docker run -p 8888:8080 \
-e SWAGGER_JSON_URL=http://localhost:3000/api/openapi.json \
swaggerapi/swagger-ui
# Then open: http://localhost:8888
Or use the Swagger Editor and paste the JSON.
Regenerating the Spec
cargo xtask docs
This command:
- Compiles
stitchd-gatewayin debug mode - Runs
stitchd-gateway --export-openapi docs/src/api/openapi.json - The binary serialises the
utoipa-derivedApiDocstruct to JSON and exits
The spec is derived from #[utoipa::path] annotations on every route handler in crates/stitchd-gateway/src/routes/. To add a new endpoint to the spec:
- Annotate the handler with
#[utoipa::path(...)] - Add the handler path to
ApiDocincrates/stitchd-gateway/src/openapi.rs - Register any new request/response types in the
components(schemas(...))block - Run
cargo xtask docsto regenerate
Contract Checking
The repository includes a contract-check script that verifies the spec covers all registered gateway routes:
python3 scripts/check_openapi_contract.py
This script:
- Parses
crates/stitchd-gateway/src/routes/for registered Axum routes - Reads
docs/src/api/openapi.json - Reports any route without a matching OpenAPI path entry
Run this in CI to catch annotation gaps before they reach main.
Security Schemes
The spec defines two security schemes:
| Scheme | Type | Header |
|---|---|---|
sdk_key | API Key | x-sdk-key |
bearer_jwt | HTTP Bearer | Authorization: Bearer <jwt> |
Every endpoint declares which scheme it uses via the security field. Endpoints that require no auth (e.g., login) declare an empty security array.
Internal gRPC Services
Auto-generated from .proto files in the proto/ directory.
Run cargo xtask docs to regenerate.
Auth & Identity
Flag & Segmentation
Events & Experimentation
Auth Service
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/auth/v1/auth_service.proto
Package: stitchd.auth.v1
Message: CredentialRequest
A raw credential presented by a caller for validation. Exactly one field must be set.
| Field | Type | Description |
|---|---|---|
bearer_token | string | A signed JWT issued by the human-auth flow. |
sdk_key | string | A raw SDK key presented via x-sdk-key header. |
Message: RbacContext
Access-control information returned for a validated credential. Downstream services inject this into request context without re-validating.
| Field | Type | Description |
|---|---|---|
tenant_id | string | |
environment_id | string | |
roles | repeated string | |
permissions | repeated string | |
subject | string | The resolved actor identity (user_id for JWT, sdk_key_id for SDK keys). |
is_system | bool | True when the credential belongs to a user in the platform System org. Used by the gateway to enforce admin-only vs. management-only route separation: admin routes require is_system=true; management routes require is_system=false. |
Message: LoginRequest
| Field | Type | Description |
|---|---|---|
email | string | |
password | string | |
org_id | string | Optional org scope; if empty the first org the user belongs to is used. |
Message: LoginResponse
| Field | Type | Description |
|---|---|---|
access_token | string | |
refresh_token | string | |
expires_in | int64 | Seconds until the access token expires. |
user_id | string | |
org_id | string |
Message: SwitchOrgRequest
| Field | Type | Description |
|---|---|---|
current_token | string | |
target_org_id | string |
Message: SwitchOrgResponse
| Field | Type | Description |
|---|---|---|
access_token | string | |
refresh_token | string | |
expires_in | int64 | |
org_id | string |
Message: UserOrgEntry
| Field | Type | Description |
|---|---|---|
org_id | string | |
org_name | string | |
role | string |
Message: ListUserOrgsRequest
| Field | Type | Description |
|---|---|---|
current_token | string |
Message: ListUserOrgsResponse
| Field | Type | Description |
|---|---|---|
orgs | repeated UserOrgEntry |
Service: AuthService
ValidateCredential
Validates a credential and returns the RBAC context for the caller. Returns UNAUTHENTICATED if the credential is invalid or expired.
- Request:
CredentialRequest - Response:
RbacContext
LoginWithPassword
Authenticates an email + password credential and issues a JWT.
- Request:
LoginRequest - Response:
LoginResponse
SwitchOrg
Switches the current user to a different org, issuing a new JWT.
- Request:
SwitchOrgRequest - Response:
SwitchOrgResponse
ListUserOrgs
Lists all orgs the current user is a member of (excluding System orgs).
- Request:
ListUserOrgsRequest - Response:
ListUserOrgsResponse
Management
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/auth/v1/management.proto
Package: stitchd.auth.v1
Message: OidcConfig
| Field | Type | Description |
|---|---|---|
issuer_url | string | |
client_id | string | |
client_secret | string | Plaintext on create/update; redacted (empty) on read. |
scopes | repeated string |
Message: SamlConfig
| Field | Type | Description |
|---|---|---|
idp_metadata_url | string | Exactly one of the following must be set. |
idp_metadata_xml | string | |
name_id_format | string | |
sp_entity_id | string |
Message: AuthProviderResponse
A redacted provider record (secrets never returned in read responses).
| Field | Type | Description |
|---|---|---|
id | string | |
org_id | string | |
provider_type | string | “oidc” |
display_name | string | |
enabled | bool | |
created_at | string | RFC3339 |
updated_at | string | RFC3339 |
acs_url | string | ACS URL is returned on create for SAML providers. |
oidc | OidcConfig | |
saml | SamlConfig |
Message: CreateAuthProviderRequest
| Field | Type | Description |
|---|---|---|
org_id | string | |
display_name | string | |
enabled | bool | |
oidc | OidcConfig | |
saml | SamlConfig |
Message: CreateAuthProviderResponse
| Field | Type | Description |
|---|---|---|
provider | AuthProviderResponse |
Message: ListAuthProvidersRequest
| Field | Type | Description |
|---|---|---|
org_id | string |
Message: ListAuthProvidersResponse
| Field | Type | Description |
|---|---|---|
providers | repeated AuthProviderResponse |
Message: GetAuthProviderRequest
| Field | Type | Description |
|---|---|---|
provider_id | string |
Message: GetAuthProviderResponse
| Field | Type | Description |
|---|---|---|
provider | AuthProviderResponse |
Message: UpdateAuthProviderRequest
| Field | Type | Description |
|---|---|---|
provider_id | string | |
display_name | string | |
enabled | bool | |
oidc | OidcConfig | |
saml | SamlConfig |
Message: UpdateAuthProviderResponse
| Field | Type | Description |
|---|---|---|
provider | AuthProviderResponse |
Message: DeleteAuthProviderRequest
| Field | Type | Description |
|---|---|---|
provider_id | string |
Message: DeleteAuthProviderResponse
| Field | Type | Description |
|---|---|---|
provider_id | string | |
xml | string | Well-formed SAML SP EntityDescriptor XML. |
Oidc Login
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/auth/v1/oidc_login.proto
Package: stitchd.auth.v1
Message: OidcAuthorizeRequest
| Field | Type | Description |
|---|---|---|
provider_id | string | |
org_id | string | |
redirect_uri | string |
Message: OidcAuthorizeResponse
| Field | Type | Description |
|---|---|---|
redirect_url | string | The URL to redirect the user-agent to. |
Message: OidcCallbackRequest
| Field | Type | Description |
|---|---|---|
provider_id | string | |
code | string | |
state | string | CSRF state value returned by the IdP — must match a stored pending state. |
redirect_uri | string |
Message: OidcCallbackResponse
| Field | Type | Description |
|---|---|---|
access_token | string | |
refresh_token | string | |
expires_in | int64 | |
user_id | string | |
org_id | string |
Service: OidcLoginService
OidcAuthorize
- Request:
OidcAuthorizeRequest - Response:
OidcAuthorizeResponse
OidcCallback
- Request:
OidcCallbackRequest - Response:
OidcCallbackResponse
Saml Login
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/auth/v1/saml_login.proto
Package: stitchd.auth.v1
Message: SamlSsoRequest
| Field | Type | Description |
|---|---|---|
provider_id | string | |
org_id | string | |
acs_url | string | ACS URL that the IdP should POST the assertion back to. |
Message: SamlSsoResponse
| Field | Type | Description |
|---|---|---|
redirect_url | string | IdP redirect URL with deflated+base64+URL-encoded SAMLRequest. |
relay_state | string | RelayState value that will be returned by the IdP in the ACS callback. |
Message: SamlAcsRequest
| Field | Type | Description |
|---|---|---|
provider_id | string | |
saml_response_b64 | string | Base64-encoded SAMLResponse from the IdP. |
relay_state | string | RelayState returned by the IdP — must match a stored pending state. |
Message: SamlAcsResponse
| Field | Type | Description |
|---|---|---|
access_token | string | |
refresh_token | string | |
expires_in | int64 | |
user_id | string | |
org_id | string |
Service: SamlLoginService
SamlSsoInitiate
- Request:
SamlSsoRequest - Response:
SamlSsoResponse
SamlAcsCallback
- Request:
SamlAcsRequest - Response:
SamlAcsResponse
Management Service
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/management/v1/management_service.proto
Package: stitchd.management.v1
Message: CreateOrgRequest
| Field | Type | Description |
|---|---|---|
name | string |
Message: CreateOrgResponse
| Field | Type | Description |
|---|---|---|
org_id | string | |
org_name | string |
Message: CreateProjectRequest
| Field | Type | Description |
|---|---|---|
org_id | string | |
name | string |
Message: CreateProjectResponse
| Field | Type | Description |
|---|---|---|
project_id | string | |
project_name | string |
Message: CreateEnvironmentRequest
| Field | Type | Description |
|---|---|---|
project_id | string | |
name | string |
Message: CreateEnvironmentResponse
| Field | Type | Description |
|---|---|---|
environment_id | string | |
environment_name | string |
Message: CreateSdkKeyRequest
| Field | Type | Description |
|---|---|---|
environment_id | string |
Message: CreateSdkKeyResponse
| Field | Type | Description |
|---|---|---|
sdk_key_id | string | |
raw_key | string | The raw key — only returned on creation, never stored in plaintext. |
Message: CreateUserRequest
| Field | Type | Description |
|---|---|---|
org_id | string | |
email | string | |
display_name | string | |
password | string | |
org_role | string | “org_admin” or “org_member” |
Message: CreateUserResponse
| Field | Type | Description |
|---|---|---|
user_id | string | |
email | string | |
display_name | string |
Service: ManagementService
CreateOrg
- Request:
CreateOrgRequest - Response:
CreateOrgResponse
CreateProject
- Request:
CreateProjectRequest - Response:
CreateProjectResponse
CreateEnvironment
- Request:
CreateEnvironmentRequest - Response:
CreateEnvironmentResponse
CreateSdkKey
- Request:
CreateSdkKeyRequest - Response:
CreateSdkKeyResponse
CreateUser
- Request:
CreateUserRequest - Response:
CreateUserResponse
Context
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/common/v1/context.proto
Package: stitchd.common.v1
Message: ParameterValue
A typed parameter value. Covers all supported context parameter types.
| Field | Type | Description |
|---|---|---|
int_value | int64 | |
double_value | double | |
string_value | string | |
bool_value | bool | |
semver_value | string | SemVer stored as string; parsed by the receiver. |
Message: Context
A single evaluation context supplied by the client SDK.
context_type: discriminator (e.g. “user”, “organisation”, “device”)key: stable identifier for this context instanceparameters: typed attributes used in rule evaluationprivate_parameters: names of parameters that must NOT appear in any log or telemetry output
| Field | Type | Description |
|---|---|---|
context_type | string | |
key | string | |
parameters | map<string, ParameterValue> | |
private_parameters | repeated string |
Flag Service
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/flags/v1/flag_service.proto
Package: stitchd.flags.v1
Message: GetFlagRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
flag_key | string |
Message: ListFlagsRequest
| Field | Type | Description |
|---|---|---|
environment_id | string |
Message: ListFlagsResponse
| Field | Type | Description |
|---|---|---|
flags | repeated FeatureFlag |
Enum: MutationKind
| Value | Description |
|---|---|
MUTATION_KIND_UNSPECIFIED | |
MUTATION_KIND_CREATE | |
MUTATION_KIND_UPDATE | |
MUTATION_KIND_DELETE | |
MUTATION_KIND_ARCHIVE |
Message: MutateFlagRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
kind | MutationKind | |
flag | FeatureFlag | |
version | uint64 | Optimistic-locking version — required for UPDATE / DELETE / ARCHIVE. Server rejects with ABORTED if stored version differs. |
Message: MutateFlagResponse
| Field | Type | Description |
|---|---|---|
flag | FeatureFlag | |
version | uint64 |
Message: GetFlagDefinitionsRequest
| Field | Type | Description |
|---|---|---|
environment_id | string |
Message: FlagHashingConfig
Controls which context parameters are used for percentage-rollout hashing.
| Field | Type | Description |
|---|---|---|
parameter_key | string | |
parameter_type | string | |
order | int32 |
Message: UpdateFlagHashingRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
flag_key | string | |
configs | repeated FlagHashingConfig |
Message: UpdateFlagHashingResponse
| Field | Type | Description |
|---|---|---|
flag | FeatureFlag | |
configs | repeated FlagHashingConfig |
Service: FlagService
GetFlag
Fetch a single flag definition by key.
- Request:
GetFlagRequest - Response:
FeatureFlag
ListFlags
List all flag definitions for an environment.
- Request:
ListFlagsRequest - Response:
ListFlagsResponse
MutateFlag
Create, update, delete, or archive a flag.
- Request:
MutateFlagRequest - Response:
MutateFlagResponse
GetFlagDefinitions
Server-streaming endpoint for SDK definition sync.
- Request:
GetFlagDefinitionsRequest - Response:
stream FeatureFlag
UpdateFlagHashing
Replace the hashing config for a flag (which context params drive rollout %).
- Request:
UpdateFlagHashingRequest - Response:
UpdateFlagHashingResponse
Flag Sync
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/flags/v1/flag_sync.proto
Package: stitchd.flags.v1
Message: VariantValue
| Field | Type | Description |
|---|---|---|
bool_value | bool | |
int_value | int64 | |
double_value | double | |
string_value | string | |
json_value | string | JSON serialised as string |
Message: Variant
| Field | Type | Description |
|---|---|---|
key | string | |
value | VariantValue |
Message: AllocationBucket
A single bucket in a percentage rollout.
| Field | Type | Description |
|---|---|---|
variant_key | string | Variant key this bucket maps to. |
weight_milli | uint32 | Weight in units of 0.1% (e.g. 1000 = 100.0%, 500 = 50.0%). |
Message: PercentageAllocation
Percentage allocation: deterministic hash over context keys/params. hash(targeted_keys, flag_key, project_id, environment_id) mod 100_000
| Field | Type | Description |
|---|---|---|
context_hash_specs | map<string, ContextHashSpec> | Context types and their parameter names to include in the hash. An empty parameter list means use the context key only. |
buckets | repeated AllocationBucket |
Message: ContextHashSpec
| Field | Type | Description |
|---|---|---|
parameter_names | repeated string | If empty, only the context key is hashed. |
Message: FlagRule
| Field | Type | Description |
|---|---|---|
rule_payload | bytes | Opaque serialised rule payload — parsed by the client rule engine. Full typed rule messages will replace this in the rules track. |
variant_key | string | |
allocation | PercentageAllocation |
Enum: FlagValueType
| Value | Description |
|---|---|
FLAG_VALUE_TYPE_UNSPECIFIED | |
FLAG_VALUE_TYPE_BOOL | |
FLAG_VALUE_TYPE_INT | |
FLAG_VALUE_TYPE_DOUBLE | |
FLAG_VALUE_TYPE_STRING | |
FLAG_VALUE_TYPE_JSON |
Message: FeatureFlag
| Field | Type | Description |
|---|---|---|
key | string | |
enabled | bool | |
value_type | FlagValueType | |
variants | repeated Variant | |
rules | repeated FlagRule | Evaluated in order; first match wins. Last rule is the default fallback. |
Message: SyncRequest
| Field | Type | Description |
|---|---|---|
contexts | repeated stitchd.common.v1.Context | Contexts the client was initialised with. |
Message: SyncResponse
| Field | Type | Description |
|---|---|---|
flags | repeated FeatureFlag | |
server_timestamp_ms | int64 | Epoch millis — client uses this to detect stale payloads. |
rule_segments | repeated stitchd.segments.v1.RuleSegment | Rule-based segment definitions for local evaluation. |
list_segments | repeated stitchd.segments.v1.ListSegmentMeta | List-based segment metadata (key + context_type only, no list entries). The SDK uses these to identify which segments require a list-check API call. |
environment_id | string | UUID of the resolved environment — SDK uses this in REST list-check URLs. |
Service: FlagSyncService
Sync
Returns all feature flags and associated data for the calling environment.
SDK key is supplied via gRPC metadata header x-sdk-key.
- Request:
SyncRequest - Response:
SyncResponse
Segment
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/segments/v1/segment.proto
Package: stitchd.segments.v1
Message: ListSegment
Targets a specific context type by explicit include/exclude key lists.
| Field | Type | Description |
|---|---|---|
key | string | |
context_type | string | |
included_keys | repeated string | |
excluded_keys | repeated string |
Message: RuleSegment
A rule-based segment with its full rule payload for client-side evaluation. (Full typed rule messages added in the rules track.)
| Field | Type | Description |
|---|---|---|
key | string | |
context_type | string | |
rule_payload | bytes | Opaque serialised rule payload — parsed by the client rule engine. |
id | string | UUID of the segment — used by the SDK to correlate ConditionExpr references. |
Message: ListSegmentMeta
Lightweight metadata for a list-based segment. The SDK receives this during definition sync so it knows which segments require a server lookup for membership resolution. The list entries themselves are NOT included here — they are fetched on demand via the list-check REST API.
| Field | Type | Description |
|---|---|---|
key | string | Segment key, used in flag rules and as the segment_key in list-check requests. |
context_type | string | The context type this segment applies to (e.g. “user”, “org”). |
id | string | UUID of the segment — used by the SDK to correlate ConditionExpr references. |
Message: SegmentBundle
Segment data returned as part of the flag sync response (embedded via FlagSyncService).
| Field | Type | Description |
|---|---|---|
list_segments | repeated ListSegment | |
rule_segments | repeated RuleSegment |
Segmentation Service
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/segments/v1/segmentation_service.proto
Package: stitchd.segments.v1
Message: GetSegmentRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
segment_key | string |
Message: ListSegmentsRequest
| Field | Type | Description |
|---|---|---|
environment_id | string |
Message: ListSegmentsResponse
| Field | Type | Description |
|---|---|---|
rule_segments | repeated RuleSegment | |
list_segments | repeated ListSegmentMeta |
Message: EvaluateMembershipRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
segment_key | string | |
context_key | string | The context key to test for membership. |
context_type | string |
Message: EvaluateMembershipResponse
| Field | Type | Description |
|---|---|---|
is_member | bool |
Enum: SegmentMutationKind
| Value | Description |
|---|---|
SEGMENT_MUTATION_KIND_UNSPECIFIED | |
SEGMENT_MUTATION_KIND_CREATE | |
SEGMENT_MUTATION_KIND_UPDATE | |
SEGMENT_MUTATION_KIND_DELETE |
Message: MutateSegmentRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
kind | SegmentMutationKind | |
rule_segment | RuleSegment | |
list_segment | ListSegment | |
version | uint64 |
Message: MutateSegmentResponse
| Field | Type | Description |
|---|---|---|
rule_segment | RuleSegment | |
list_segment | ListSegment | |
version | uint64 |
Service: SegmentationService
GetSegment
Fetch a single segment definition by key.
- Request:
GetSegmentRequest - Response:
SegmentBundle
ListSegments
List all segments for an environment.
- Request:
ListSegmentsRequest - Response:
ListSegmentsResponse
EvaluateMembership
Evaluate whether a context key is a member of a segment. Handles both rule-based and list-based segments.
- Request:
EvaluateMembershipRequest - Response:
EvaluateMembershipResponse
MutateSegment
Create, update, or delete a segment.
- Request:
MutateSegmentRequest - Response:
MutateSegmentResponse
Event
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/events/v1/event.proto
Package: stitchd.events.v1
Message: MetricValue
| Field | Type | Description |
|---|---|---|
bool_value | bool | |
int_value | int64 | |
double_value | double |
Message: Event
A single metric event emitted by the client SDK. Only pre-registered event keys with known types are accepted.
| Field | Type | Description |
|---|---|---|
metric_key | string | The registered event key. |
context_type | string | Minimal context: type + key only (no parameters — not needed for events). |
context_key | string | |
value | MetricValue | The metric value — must match the registered type for this metric_key. |
timestamp_ms | int64 | Client-side timestamp in epoch milliseconds. |
Message: IngestRequest
| Field | Type | Description |
|---|---|---|
events | repeated Event |
Message: IngestResponse
| Field | Type | Description |
|---|---|---|
accepted_count | uint32 | Number of events accepted (unknown keys are rejected and not counted). |
rejected_keys | repeated string | Keys that were rejected (not pre-registered or type mismatch). |
Service: EventService
Ingest
Ingests a batch of metric events.
SDK key is supplied via gRPC metadata header x-sdk-key.
- Request:
IngestRequest - Response:
IngestResponse
Event Service
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/events/v1/event_service.proto
Package: stitchd.events.v1
Message: EventDefinition
A pre-registered event definition — only registered events are accepted.
| Field | Type | Description |
|---|---|---|
key | string | |
description | string | |
value_type | MetricValueType | Expected value type for this metric. |
environment_id | string |
Enum: MetricValueType
| Value | Description |
|---|---|
METRIC_VALUE_TYPE_UNSPECIFIED | |
METRIC_VALUE_TYPE_BOOL | |
METRIC_VALUE_TYPE_INT | |
METRIC_VALUE_TYPE_DOUBLE |
Service: EventIngestionService
EventIngestionService provides the ingestion endpoint for the Event Service crate. The existing EventService (in event.proto) is the legacy SDK-facing service. This service is used for inter-service communication.
IngestEvent
Ingest a batch of metric events.
Unknown keys (not pre-registered) are rejected and reported in rejected_keys.
SDK key is supplied via gRPC metadata header x-sdk-key.
- Request:
IngestRequest - Response:
IngestResponse
Experimentation Service
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/experiments/v1/experimentation_service.proto
Package: stitchd.experiments.v1
Enum: ExperimentStatus
| Value | Description |
|---|---|
EXPERIMENT_STATUS_UNSPECIFIED | |
EXPERIMENT_STATUS_DRAFT | |
EXPERIMENT_STATUS_ACTIVE | |
EXPERIMENT_STATUS_PAUSED | |
EXPERIMENT_STATUS_CONCLUDED |
Message: Experiment
| Field | Type | Description |
|---|---|---|
id | string | |
environment_id | string | |
name | string | |
description | string | |
flag_key | string | |
status | ExperimentStatus | |
variant_keys | repeated string | |
created_at_ms | int64 | |
updated_at_ms | int64 | |
version | uint64 |
Message: ExperimentIteration
| Field | Type | Description |
|---|---|---|
id | string | |
experiment_id | string | |
iteration_number | int32 | |
started_at_ms | int64 | |
ended_at_ms | int64 | Zero when the iteration is still running. |
metric_keys | repeated string | |
traffic_allocation | double |
Message: VariantResult
| Field | Type | Description |
|---|---|---|
variant_key | string | |
participant_count | uint64 | |
metric_values | map<string, double> | |
p_value | double | |
p_value_present | bool |
Message: ExperimentResults
| Field | Type | Description |
|---|---|---|
experiment_id | string | |
variant_results | repeated VariantResult | |
computed_at_ms | int64 | |
is_stale | bool | |
next_run_at_ms | int64 | |
computation_status | string |
Message: CreateExperimentRequest
| Field | Type | Description |
|---|---|---|
experiment | Experiment |
Message: UpdateExperimentRequest
| Field | Type | Description |
|---|---|---|
experiment | Experiment |
Message: DeleteExperimentRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
experiment_id | string |
Message: GetExperimentRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
experiment_id | string |
Message: ListExperimentsRequest
| Field | Type | Description |
|---|---|---|
environment_id | string |
Message: ListExperimentsResponse
| Field | Type | Description |
|---|---|---|
experiments | repeated Experiment |
Message: GetResultsRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
experiment_id | string |
Message: TransitionExperimentRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
experiment_id | string | |
new_status | ExperimentStatus | |
reason | string | Human-readable reason for the transition (optional). |
Message: ListIterationsRequest
| Field | Type | Description |
|---|---|---|
environment_id | string | |
experiment_id | string |
Message: ListIterationsResponse
| Field | Type | Description |
|---|---|---|
iterations | repeated ExperimentIteration |
Service: ExperimentationService
CreateExperiment
- Request:
CreateExperimentRequest - Response:
Experiment
GetExperiment
- Request:
GetExperimentRequest - Response:
Experiment
ListExperiments
- Request:
ListExperimentsRequest - Response:
ListExperimentsResponse
UpdateExperiment
- Request:
UpdateExperimentRequest - Response:
Experiment
DeleteExperiment
- Request:
DeleteExperimentRequest - Response:
Experiment
TransitionExperiment
- Request:
TransitionExperimentRequest - Response:
Experiment
ListIterations
- Request:
ListIterationsRequest - Response:
ListIterationsResponse
GetResults
- Request:
GetResultsRequest - Response:
ExperimentResults
Stats Service
Auto-generated from
/home/runner/work/feature-flag/feature-flag/proto/stats/v1/stats_service.proto
Package: stitchd.stats.v1
Message: TriggerRecomputeRequest
| Field | Type | Description |
|---|---|---|
experiment_id | string | UUID of the experiment to recompute stats for. |
Message: TriggerRecomputeResponse
| Field | Type | Description |
|---|---|---|
job_id | string | UUID of the created stats job. |
status | string | Initial status — always “pending”. |
created_at_ms | int64 | Milliseconds since Unix epoch when the job was created. |
Message: GetJobStatusRequest
| Field | Type | Description |
|---|---|---|
job_id | string | UUID of the stats job to query. |
Message: GetJobStatusResponse
| Field | Type | Description |
|---|---|---|
job_id | string | |
status | string | |
started_at_ms | int64 | Zero when the job has not yet started. |
completed_at_ms | int64 | Zero when the job has not yet completed. |
error | string | Empty unless the job status is “failed”. |
Service: StatsService
TriggerRecompute
Trigger an out-of-band stats recompute for an experiment. Returns immediately with a job_id; poll GetJobStatus to track progress.
- Request:
TriggerRecomputeRequest - Response:
TriggerRecomputeResponse
GetJobStatus
Query the status of a previously triggered recompute job.
- Request:
GetJobStatusRequest - Response:
GetJobStatusResponse
Service Coordination Flows
Key request flows across the microservice mesh. All inter-service calls use gRPC.
Flag Evaluation
An SDK client evaluates a feature flag. The gateway authenticates the SDK key, then asks the flag service to evaluate the flag for the given context. The flag service may call the segmentation service to resolve rule-based targeting.
sequenceDiagram
participant SDK as SDK Client
participant GW as stitchd-gateway<br/>(REST :8080)
participant FS as stitchd-flag-service<br/>(gRPC :50051)
participant SS as stitchd-segmentation-service<br/>(gRPC :50053)
SDK->>GW: POST /v1/environments/{env}/evaluate<br/>x-sdk-key: sdk_live_...
GW->>FS: ValidateSdkKey(environment_id, sdk_key)
FS-->>GW: Ok(environment)
GW->>FS: EvaluateFlag(flag_key, context_type, context_key, attributes)
alt flag has segment rule
FS->>SS: CheckMembership(segment_key, context_type, context_key)
SS-->>FS: MembershipResponse(is_member)
end
FS-->>GW: EvaluateResponse(variant_key, is_enabled)
GW-->>SDK: 200 { variant_key, is_enabled }
Event Ingestion
An SDK client records a metric event. The gateway authenticates the SDK key, then forwards the event to the event service for storage and downstream processing.
sequenceDiagram
participant SDK as SDK Client
participant GW as stitchd-gateway<br/>(REST :8080)
participant FS as stitchd-flag-service<br/>(gRPC :50051)
participant ES as stitchd-event-service<br/>(gRPC :50054)
SDK->>GW: POST /v1/environments/{env}/events<br/>x-sdk-key: sdk_live_...
GW->>FS: ValidateSdkKey(environment_id, sdk_key)
FS-->>GW: Ok(environment)
GW->>ES: IngestEvent(events: [{ metric_key, context_type, context_key, value }])
ES-->>GW: IngestResponse(accepted_count, rejected_keys)
GW-->>SDK: 200 { accepted_count, rejected_keys }
Definition Sync
An SDK client opens a long-lived gRPC streaming connection to receive the full flag/segment definition set and incremental updates. The gateway passes the stream through to the flag service.
sequenceDiagram
participant SDK as SDK Client
participant GW as stitchd-gateway<br/>(gRPC :50050)
participant FS as stitchd-flag-service<br/>(gRPC :50051)
SDK->>GW: FlagSyncService.SyncDefinitions(SyncRequest)<br/>metadata: x-sdk-key: sdk_live_...
GW->>FS: ValidateSdkKey(environment_id, sdk_key)
FS-->>GW: Ok(environment)
GW->>FS: FlagSyncService.SyncDefinitions(SyncRequest)
Note over FS: Stream open — full snapshot first
FS-->>GW: SyncResponse(flags[], segments[], sequence_number=1)
GW-->>SDK: SyncResponse(flags[], segments[], sequence_number=1)
Note over FS: Incremental update on mutation
FS-->>GW: SyncResponse(flags[updated], segments[], sequence_number=2)
GW-->>SDK: SyncResponse(flags[updated], segments[], sequence_number=2)
Note over SDK: Connection held open indefinitely
Human Auth
An admin user or the Admin UI logs in and obtains a JWT. Subsequent management requests carry the JWT which the gateway validates before proxying.
sequenceDiagram
participant UI as Admin UI / Operator
participant GW as stitchd-gateway<br/>(REST :8080)
participant AS as stitchd-auth-service<br/>(gRPC :50052)
UI->>GW: POST /v1/auth/login<br/>{ email, password }
GW->>AS: Login(email, password)
AS-->>GW: LoginResponse(token, expires_at)
GW-->>UI: 200 { token, expires_at }
Note over UI: Subsequent management request
UI->>GW: GET /v1/environments/{env}/flags<br/>Authorization: Bearer eyJhbGci...
GW->>AS: ValidateToken(token)
AS-->>GW: TokenClaims(user_id, org_id, roles)
GW->>GW: Authorise: roles include required permission?
alt authorised
GW->>GW: Proxy to stitchd-flag-service
GW-->>UI: 200 { flags: [...] }
else forbidden
GW-->>UI: 403 { error: "insufficient permissions" }
end
Rust SDK Overview
The Stitchd Rust SDK provides server-side, in-process flag evaluation. Rule-based segments are evaluated locally; list-based segments fall back to a REST membership check (optionally pre-warmed via an LFU cache).
Key Features
- Streaming gRPC sync — flag definitions are pushed to the SDK and cached in memory
- In-process rule evaluation — zero network hops for rule-based decisions
- LFU pre-warmed list-segment cache — hot contexts avoid REST round-trips
- Thread-safe
Arc<SdkClient>— clone freely across Tokio tasks
Getting Started
See Quickstart for a code example, or browse the
full API reference generated by cargo doc.
SDK Quickstart
Auto-extracted from
stitchd-sdk/src/lib.rsmodule docs. Runcargo xtask docsto regenerate.
Add the SDK to your Cargo.toml:
[dependencies]
stitchd-sdk = "0.1"
Initialize the client once at application startup:
use stitchd_sdk::{SdkClient, SdkConfig};
use stitchd_sdk::{Context, EvaluationContext};
use std::sync::Arc;
#[tokio::main]
async fn main() {
let config = SdkConfig::new(
"http://localhost:9090", // gRPC endpoint
"http://localhost:8080", // REST endpoint for list-checks
"sk_live_...", // SDK key
);
let client: Arc<SdkClient> = SdkClient::init(config).await.expect("SDK init failed");
// Evaluate a flag for a specific user context
let ctx = EvaluationContext {
contexts: vec![
Context::new("user", "user-123"),
],
};
let variant = client.evaluate("my-feature-flag", &ctx).await.expect("evaluation failed");
if let Some(v) = variant {
println!("Flag value: {:?}", v);
}
}
See [SdkClient], [SdkConfig], and [EvaluationContext] for full API details.
Deployment Overview
Stitchd Feature Flag is designed for self-hosted deployment. The server is a single statically-linked Rust binary with two network interfaces: a REST API on HTTP and a gRPC server for SDK sync.
Prerequisites
| Dependency | Minimum Version | Purpose |
|---|---|---|
| PostgreSQL | 16+ | Feature flag config, tenants, RBAC, audit logs |
| ClickHouse | 24+ | Experiment events and metric aggregations (upcoming) |
| Rust toolchain | stable | Building from source |
Quick Start
# 1. Start PostgreSQL
# 2. Set required environment variable
export DATABASE_URL="postgres://user:pass@localhost/stitchd"
# 3. Run migrations
sqlx migrate run --database-url "$DATABASE_URL"
# 4. Start the server
cargo run -p stitchd-server
The server starts on:
HTTP_PORT(default8080) — REST Admin APIGRPC_PORT(default9090) — SDK gRPC sync
Chapters
PostgreSQL Setup
Stitchd requires PostgreSQL 16 or later. PostgreSQL is the primary configuration store: it holds tenants, projects, environments, SDK keys, feature flag definitions, segment rules, list-segment membership, experiments, and audit logs.
System Requirements
- PostgreSQL 16+
- Sufficient disk for flag config and audit log growth (typically small — megabytes, not gigabytes)
1. Create the Database
CREATE USER stitchd WITH PASSWORD 'yourpassword';
CREATE DATABASE stitchd OWNER stitchd;
2. Run Migrations
Stitchd uses sqlx for migrations. Run them against the database before starting the server:
export DATABASE_URL="postgres://stitchd:yourpassword@localhost/stitchd"
sqlx migrate run --database-url "$DATABASE_URL"
Migrations live in crates/stitchd-db/migrations/. They are additive and safe to
re-run — sqlx tracks which have already been applied.
3. Connection String Format
postgres://[user]:[password]@[host]:[port]/[database]
Example:
postgres://stitchd:secret@db.internal:5432/stitchd
Pass this as the DATABASE_URL environment variable to stitchd-server.
Connection Pool
The server uses a sqlx::PgPool with default pool sizing. For production, tune
PostgreSQL max_connections to accommodate the pool. The default pool size is 10
connections per server instance.
Docker Example
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: stitchd
POSTGRES_USER: stitchd
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
pg_data:
ClickHouse Setup
Status: Upcoming — ClickHouse integration is planned but not yet implemented. The
stitchd-eventscrate scaffolding exists; the client and schema are in progress.
Stitchd will use ClickHouse 24+ for high-volume event storage:
- Experiment evaluation events (per-context metric values)
- Experiment results and metric aggregations
- Flag evaluation telemetry (optional)
Why ClickHouse
PostgreSQL is optimized for transactional config reads/writes. Experiment events are append-only, high-throughput, and require analytical queries (aggregations, time-series). ClickHouse handles this workload efficiently without impacting the main config store.
Planned Setup
# Create the events database
clickhouse-client --query "CREATE DATABASE stitchd_events"
A future CLICKHOUSE_URL environment variable will point the server at the HTTP
interface (default port 8123):
http://clickhouse.internal:8123
Docker Example (for future use)
services:
clickhouse:
image: clickhouse/clickhouse-server:24
ports:
- "8123:8123" # HTTP interface
- "9000:9000" # Native protocol
volumes:
- ch_data:/var/lib/clickhouse
volumes:
ch_data:
Check back when the stitchd-events crate reaches its first release milestone.
Environment Variables
All configuration is passed via environment variables. There are no config files.
Required
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string, e.g. postgres://user:pass@host/stitchd |
Optional
| Variable | Default | Description |
|---|---|---|
HTTP_PORT | 8080 | Port for the REST Admin API |
GRPC_PORT | 9090 | Port for the SDK gRPC sync server |
APP_ENV | (unset) | Set to production to enable JSON structured logging |
RUST_LOG | (unset) | Log level filter, e.g. info or info,stitchd_server=debug |
Example .env
DATABASE_URL=postgres://stitchd:secret@localhost/stitchd
HTTP_PORT=8080
GRPC_PORT=9090
APP_ENV=production
RUST_LOG=info
Log Levels
RUST_LOG follows the tracing-subscriber directive format:
# All info, verbose for stitchd crates
RUST_LOG=info,stitchd_server=debug,stitchd_core=debug
# Quiet mode
RUST_LOG=warn
In production (APP_ENV=production) logs are emitted as JSON. In development they are
printed in a human-readable format with colors.
SDK Keys
SDK keys authenticate the Rust SDK against stitchd-server. Each key is scoped to a
single project + environment pair — an SDK key cannot access data from another environment.
Key Format
Keys are prefixed with sk_live_ followed by a random token, e.g.:
sk_live_a3f8b2c9d1e4...
Creating an SDK Key
Use the Admin REST API to create a key for a specific environment:
curl -X POST "http://localhost:8080/api/v1/environments/{env_id}/sdk-keys" \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{"name": "production-server"}'
The response includes the full key value — store it immediately; the server does not return the plaintext again after creation.
Using the Key in the SDK
Pass the key in SdkConfig:
#![allow(unused)]
fn main() {
let config = SdkConfig::new(
"http://localhost:9090", // gRPC endpoint
"http://localhost:8080", // REST endpoint
"sk_live_...", // SDK key
);
}
Key Rotation
At least one active SDK key per environment is enforced at the API level — you cannot delete the last key. The safe rotation procedure is:
- Create a new key via the API
- Deploy the new key to your application (update
SdkConfig) - Verify the new key is active and receiving traffic
- Revoke the old key via
DELETE /api/v1/sdk-keys/{key_id}
This zero-downtime rotation ensures no evaluation gaps during key rollover.
Listing Keys
curl "http://localhost:8080/api/v1/environments/{env_id}/sdk-keys" \
-H "Authorization: Bearer <admin_token>"
Returns key IDs, names, creation timestamps, and active status — never the plaintext secret after initial creation.
System Architecture
Stitchd Feature Flag is a self-hosted feature flagging and experimentation platform built on a small set of Rust crates with two external data stores.
High-Level Diagram
graph TB
subgraph Clients
AdminUI[Admin UI / curl]
App[Your Application]
SDK[stitchd-sdk]
end
subgraph Gateway["stitchd-gateway"]
REST[REST API\n:8080]
GRPC_GW[gRPC FlagSync\n:50050]
end
subgraph Services
AS[stitchd-auth-service\n:50051]
FS[stitchd-flag-service\n:50052]
SS[stitchd-segmentation-service\n:50053]
ES[stitchd-event-service\n:50054]
XS[stitchd-experimentation-service\n:50055]
end
subgraph Stores
PG[(PostgreSQL\nconfig store)]
CH[(ClickHouse\nevents store)]
end
AdminUI -->|HTTP REST| REST
App -->|SdkClient::init| SDK
SDK -->|gRPC SyncDefinitions| GRPC_GW
SDK -->|REST list-segment check| REST
REST -->|gRPC| AS
REST -->|gRPC| FS
REST -->|gRPC| SS
REST -->|gRPC| ES
REST -->|gRPC| XS
GRPC_GW -->|gRPC proxy| FS
AS -->|sqlx| PG
FS -->|sqlx| PG
SS -->|sqlx| PG
XS -->|sqlx| PG
ES -->|sqlx| PG
ES -->|ClickHouse client| CH
Crate Map
| Crate | Role | Type |
|---|---|---|
stitchd-gateway | REST + gRPC gateway — single entry point for all external traffic | Binary |
stitchd-auth-service | Authentication (login, JWT) and organisation/project management | Binary |
stitchd-flag-service | Flag definitions, variant management, SDK flag-sync streaming | Binary |
stitchd-segmentation-service | Segment membership evaluation and list-segment checks | Binary |
stitchd-event-service | Experiment event ingestion, forwarded to ClickHouse | Binary |
stitchd-experimentation-service | Experiment CRUD and result aggregation | Binary |
stitchd-stats-service | Scheduled statistics computation (60-min loop), on-demand recompute jobs, stats_jobs + stats_schedule management | Binary |
stitchd-sdk | Server-side Rust SDK — in-process flag evaluation | Library |
stitchd-core | Domain model, rule engine, segmentation logic, hashing, ID types | Library |
stitchd-db | Database access layer (sqlx repositories + ClickHouse) | Library |
stitchd-proto | Protobuf definitions and generated tonic stubs for all services | Library |
stitchd-events | ClickHouse event ingestion and migration helpers | Library |
xtask | Build tool: mdBook docs generation, tool installation | Binary |
Design Principles
Gateway-fronted microservices — All external traffic (admin API, SDK flag sync, event
ingestion) enters through stitchd-gateway. Backend services are never exposed directly,
making it straightforward to add auth, rate limiting, or TLS termination in one place.
In-process evaluation — The SDK syncs flag definitions via gRPC on startup and keeps them in memory. Rule evaluation happens locally with zero network hops per request.
Dual data store — PostgreSQL handles transactional config; ClickHouse handles append-only, analytical event data. The two stores are intentionally separate so event load cannot affect flag evaluation latency.
Multi-tenancy at the project level — A single deployment hosts multiple tenants. Isolation is enforced at the database layer; every query is scoped to a tenant/project/env.
Further Reading
Flag Evaluation Flow
The SDK evaluates flags in-process after syncing definitions from the server via gRPC. The only network calls after startup are for list-based segment membership checks.
Startup Sync
sequenceDiagram
participant App
participant SDK as stitchd-sdk
participant GW as stitchd-gateway<br/>gRPC :50050
participant FS as stitchd-flag-service<br/>gRPC :50052
participant PG as PostgreSQL
App->>SDK: SdkClient::init(config)
SDK->>GW: gRPC SyncDefinitions (stream)<br/>metadata: x-sdk-key: sdk_live_...
GW->>FS: ValidateSdkKey + proxy SyncDefinitions
FS->>PG: load flag + segment definitions
PG-->>FS: definitions
FS-->>GW: FlagDefinitions snapshot (stream)
GW-->>SDK: FlagDefinitions snapshot
SDK->>SDK: cache in DefinitionCache (Arc<RwLock>)
SDK-->>App: Arc<SdkClient> ready
After init, the SDK holds a complete copy of all flag definitions in memory. The gRPC
stream remains open and flag-service pushes incremental updates whenever a flag or
segment changes — no polling interval.
Per-Request Evaluation
sequenceDiagram
participant App
participant SDK as stitchd-sdk
participant GW as stitchd-gateway<br/>REST :8080
participant SS as stitchd-segmentation-service
App->>SDK: evaluate("flag-key", &ctx)
SDK->>SDK: read DefinitionCache (lock-free read)
SDK->>SDK: find flag by key
alt Flag disabled or no rules match
SDK-->>App: default variant
else Rule-based segment match
SDK->>SDK: evaluate ConditionExpr in-process
SDK-->>App: matched variant
else List-based segment
SDK->>SDK: check LFU cache
alt Cache hit
SDK-->>App: variant (cache)
else Cache miss
SDK->>GW: REST GET /v1/environments/{env}/list-segment-check
GW->>SS: gRPC CheckMembership
SS-->>GW: MembershipResponse
GW-->>SDK: membership boolean
SDK->>SDK: populate LFU cache
SDK-->>App: variant
end
end
Rule Engine
Rules are evaluated as a ConditionExpr tree:
And(Vec<ConditionExpr>)— all children must matchOr(Vec<ConditionExpr>)— any child must matchNot(Box<ConditionExpr>)— negationLeaf { field, operator, value }— compare a context attribute
Context attributes are looked up from the EvaluationContext passed to evaluate().
The context contains typed ParameterValue entries (string, int, float, bool, list)
keyed by (context_type, attribute_name).
LFU Cache
List-segment membership results are cached using an LFU (Least Frequently Used) eviction
policy. Pre-warming a set of high-traffic contexts at startup avoids REST round-trips for
known users. Cache size is configured in LfuConfig.
Multi-Tenancy Model
Stitchd uses a four-level hierarchy: Tenant → Project → Environment → SDK Key.
Hierarchy Diagram
graph TD
T[Tenant] -->|owns many| P[Project]
P -->|has many| E[Environment\ne.g. prod, staging, dev]
E -->|has many| SDK[SDK Keys]
P -->|defines| FF[Feature Flag Definitions]
P -->|defines| V[Variant Configurations]
E -->|configures| R[Rules]
E -->|configures| S[Segments]
E -->|runs| EX[Experiments]
Scoping Rules
Project-scoped (shared across environments)
| Entity | Notes |
|---|---|
| Feature Flag Definitions | The flag key and type are project-level |
| Variant Configurations | Which variants exist and their values |
Sharing definitions across environments ensures flag keys are consistent when promoting
from dev → staging → prod.
Environment-scoped
| Entity | Notes |
|---|---|
| Rules | Which users/segments see which variant, in which environment |
| Segments | Rule-based and list-based segment definitions |
| SDK Keys | One or more keys per environment for SDK authentication |
| Experiments | A/B test and metric configurations |
Each environment is fully independent — enabling a flag in prod does not affect staging.
SDK Key Isolation
An SDK key grants read access to exactly one project + environment combination.
The SDK can only see:
- Flag definitions for that project
- Active rules for that environment
- Segment definitions for that environment
It cannot read other environments’ rules or other tenants’ data.
Tenant Isolation
All database queries include a tenant_id predicate. Tenants are fully isolated at the
query layer — there is no cross-tenant data leakage by design, and no shared-nothing
partitioning is required at the database level.
Data Stores
Stitchd uses two data stores with clearly separated responsibilities. Neither store substitutes for the other — the split is intentional.
Overview
graph LR
GW[stitchd-gateway]
FS[stitchd-flag-service]
SS[stitchd-segmentation-service]
AS[stitchd-auth-service]
XS[stitchd-experimentation-service]
ES[stitchd-event-service]
FS -->|config reads/writes\nsqlx| PG[(PostgreSQL\nconfig store)]
SS -->|config reads\nsqlx| PG
AS -->|config reads/writes\nsqlx| PG
XS -->|config reads/writes\nsqlx| PG
ES -->|event writes| CH[(ClickHouse\nevents store)]
SDK[stitchd-sdk] -->|list-segment membership\nREST| GW
GW -->|gRPC| SS
PostgreSQL — Configuration Store
Role: Authoritative source of truth for all feature flag configuration.
PostgreSQL is chosen for its ACID guarantees, rich SQL expressiveness, and compatibility with the standard sqlx connection pool.
| Table Category | Examples |
|---|---|
| Identity | tenants, projects, environments, sdk_keys |
| Feature Flags | feature_flags, variants, flag_values |
| Segmentation | segments, segment_rules, list_segment_members |
| Experimentation | experiments, experiment_metrics |
| Audit | audit_logs |
All writes go through stitchd-db which enforces tenant isolation via parameterized queries.
Migration tool: sqlx-cli — run sqlx migrate run before starting the services.
ClickHouse — Events Store (Upcoming)
Role: Append-only event ingestion and analytical aggregations.
ClickHouse is chosen because experiment events are:
- High-volume — potentially millions of events per day per tenant
- Append-only — events are never updated, only inserted
- Analytically queried — aggregations, time-series, percentile calculations
Keeping events in ClickHouse prevents experiment load from competing with flag evaluation reads in PostgreSQL.
| Data Category | Notes |
|---|---|
| Evaluation events | One row per (flag, context, variant, timestamp) |
| Experiment results | Pre-aggregated metric values per experiment arm |
| Telemetry | Optional flag evaluation counts (for dashboards) |
ClickHouse integration is implemented by
stitchd-events. The HTTP interface (port 8123) is used for writes; native protocol (port 9000) for bulk analytical reads.
Choosing the Right Store
| Question | Store |
|---|---|
| “Which variant should this user see?” | PostgreSQL (via gRPC-synced cache) |
| “Did this experiment reach significance?” | ClickHouse |
| “What rules are configured for this flag?” | PostgreSQL |
| “How many users saw variant A last week?” | ClickHouse |