Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .proto files)
  • 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

ProtocolDefault PortPurpose
HTTP/REST8080SDK evaluation, event ingestion, admin management
gRPC50050Definition-sync streaming (FlagSyncService) for SDK clients

Routing Rules

Incoming requests are dispatched by path prefix:

Path PrefixAuthUpstream Service
POST /v1/auth/loginnonestitchd-auth-service
/v1/admin/**Bearer JWT (system-org only)stitchd-auth-service
/v1/management/**Bearer JWTstitchd-auth-service / stitchd-flag-service
/v1/environments/{id}/evaluatex-sdk-keystitchd-flag-service
/v1/environments/{id}/eventsx-sdk-key or Bearer JWTstitchd-event-service
/v1/environments/{id}/segments/**x-sdk-key or Bearer JWTstitchd-segmentation-service
/v1/environments/{id}/flags/**Bearer JWTstitchd-flag-service
/v1/environments/{id}/experiments/**Bearer JWTstitchd-experimentation-service
/v1/environments/{id}/event-definitions/**Bearer JWTstitchd-event-service

Auth Header Matrix

Route groupHeader requiredValue
SDK routes (evaluate, ingest events, list-check)x-sdk-keyEnvironment SDK key
Admin & management routesAuthorizationBearer <jwt>
Flag / segment / event / experiment CRUDAuthorizationBearer <jwt>
Loginnone

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 StatusHTTP Status
NOT_FOUND404
UNAUTHENTICATED401
PERMISSION_DENIED403
INVALID_ARGUMENT400
UNAVAILABLE502
INTERNAL500

What’s New Since the Monolith

The gateway adds the following endpoints that did not exist in stitchd-server:

EndpointDescription
POST /v1/environments/{id}/events/batchBulk event ingestion in a single request
POST /v1/environments/{id}/segments/batch-list-checkBulk 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}/hashingPer-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

ServiceProto packageGateway port
FlagSyncServiceflags.v150050

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:

FieldTypeDescription
environment_idstringEnvironment to subscribe to

SyncResponse fields:

FieldTypeDescription
flagsrepeated FlagDefinitionCurrent set of flag definitions
segmentsrepeated SegmentDefinitionCurrent set of segment definitions
sequence_numberint64Monotonically 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

  1. Obtain a JWT by posting credentials to POST /v1/auth/login.
  2. 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.

MethodPathDescription
POST/v1/admin/orgsCreate a new organisation
POST/v1/admin/seed-userBootstrap the first admin user

Management Endpoints

MethodPathDescription
POST/v1/management/projectsCreate a project
POST/v1/management/projects/{id}/environmentsCreate an environment within a project
POST/v1/management/environments/{id}/sdk-keysIssue a new SDK key
POST/v1/management/usersCreate a user account

Flag Management

MethodPathDescription
GET/v1/environments/{env_id}/flagsList all flags
POST/v1/environments/{env_id}/flagsCreate 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}/variantsAdd a variant
PUT/v1/environments/{env_id}/flags/{key}/rulesReplace targeting rules
PUT/v1/environments/{env_id}/flags/{key}/hashingUpdate hashing config

Segment Management

MethodPathDescription
GET/v1/environments/{env_id}/segmentsList all segments
POST/v1/environments/{env_id}/segmentsCreate 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

MethodPathDescription
GET/v1/environments/{env_id}/event-definitionsList event definitions
POST/v1/environments/{env_id}/event-definitionsCreate 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

MethodPathDescription
GET/v1/environments/{env_id}/experimentsList experiments
POST/v1/environments/{env_id}/experimentsCreate 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}/transitionTransition experiment state
GET/v1/environments/{env_id}/experiments/{id}/iterationsList iterations
GET/v1/environments/{env_id}/experiments/{id}/resultsGet statistical results

OpenAPI / Swagger UI

The full machine-readable spec is available at:

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:

  1. Compiles stitchd-gateway in debug mode
  2. Runs stitchd-gateway --export-openapi docs/src/api/openapi.json
  3. The binary serialises the utoipa-derived ApiDoc struct 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:

  1. Annotate the handler with #[utoipa::path(...)]
  2. Add the handler path to ApiDoc in crates/stitchd-gateway/src/openapi.rs
  3. Register any new request/response types in the components(schemas(...)) block
  4. Run cargo xtask docs to 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:

SchemeTypeHeader
sdk_keyAPI Keyx-sdk-key
bearer_jwtHTTP BearerAuthorization: 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.

FieldTypeDescription
bearer_tokenstringA signed JWT issued by the human-auth flow.
sdk_keystringA 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.

FieldTypeDescription
tenant_idstring
environment_idstring
rolesrepeated string
permissionsrepeated string
subjectstringThe resolved actor identity (user_id for JWT, sdk_key_id for SDK keys).
is_systemboolTrue 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

FieldTypeDescription
emailstring
passwordstring
org_idstringOptional org scope; if empty the first org the user belongs to is used.

Message: LoginResponse

FieldTypeDescription
access_tokenstring
refresh_tokenstring
expires_inint64Seconds until the access token expires.
user_idstring
org_idstring

Message: SwitchOrgRequest

FieldTypeDescription
current_tokenstring
target_org_idstring

Message: SwitchOrgResponse

FieldTypeDescription
access_tokenstring
refresh_tokenstring
expires_inint64
org_idstring

Message: UserOrgEntry

FieldTypeDescription
org_idstring
org_namestring
rolestring

Message: ListUserOrgsRequest

FieldTypeDescription
current_tokenstring

Message: ListUserOrgsResponse

FieldTypeDescription
orgsrepeated 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

FieldTypeDescription
issuer_urlstring
client_idstring
client_secretstringPlaintext on create/update; redacted (empty) on read.
scopesrepeated string

Message: SamlConfig

FieldTypeDescription
idp_metadata_urlstringExactly one of the following must be set.
idp_metadata_xmlstring
name_id_formatstring
sp_entity_idstring

Message: AuthProviderResponse

A redacted provider record (secrets never returned in read responses).

FieldTypeDescription
idstring
org_idstring
provider_typestring“oidc”
display_namestring
enabledbool
created_atstringRFC3339
updated_atstringRFC3339
acs_urlstringACS URL is returned on create for SAML providers.
oidcOidcConfig
samlSamlConfig

Message: CreateAuthProviderRequest

FieldTypeDescription
org_idstring
display_namestring
enabledbool
oidcOidcConfig
samlSamlConfig

Message: CreateAuthProviderResponse

FieldTypeDescription
providerAuthProviderResponse

Message: ListAuthProvidersRequest

FieldTypeDescription
org_idstring

Message: ListAuthProvidersResponse

FieldTypeDescription
providersrepeated AuthProviderResponse

Message: GetAuthProviderRequest

FieldTypeDescription
provider_idstring

Message: GetAuthProviderResponse

FieldTypeDescription
providerAuthProviderResponse

Message: UpdateAuthProviderRequest

FieldTypeDescription
provider_idstring
display_namestring
enabledbool
oidcOidcConfig
samlSamlConfig

Message: UpdateAuthProviderResponse

FieldTypeDescription
providerAuthProviderResponse

Message: DeleteAuthProviderRequest

FieldTypeDescription
provider_idstring

Message: DeleteAuthProviderResponse

FieldTypeDescription
provider_idstring
xmlstringWell-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

FieldTypeDescription
provider_idstring
org_idstring
redirect_uristring

Message: OidcAuthorizeResponse

FieldTypeDescription
redirect_urlstringThe URL to redirect the user-agent to.

Message: OidcCallbackRequest

FieldTypeDescription
provider_idstring
codestring
statestringCSRF state value returned by the IdP — must match a stored pending state.
redirect_uristring

Message: OidcCallbackResponse

FieldTypeDescription
access_tokenstring
refresh_tokenstring
expires_inint64
user_idstring
org_idstring

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

FieldTypeDescription
provider_idstring
org_idstring
acs_urlstringACS URL that the IdP should POST the assertion back to.

Message: SamlSsoResponse

FieldTypeDescription
redirect_urlstringIdP redirect URL with deflated+base64+URL-encoded SAMLRequest.
relay_statestringRelayState value that will be returned by the IdP in the ACS callback.

Message: SamlAcsRequest

FieldTypeDescription
provider_idstring
saml_response_b64stringBase64-encoded SAMLResponse from the IdP.
relay_statestringRelayState returned by the IdP — must match a stored pending state.

Message: SamlAcsResponse

FieldTypeDescription
access_tokenstring
refresh_tokenstring
expires_inint64
user_idstring
org_idstring

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

FieldTypeDescription
namestring

Message: CreateOrgResponse

FieldTypeDescription
org_idstring
org_namestring

Message: CreateProjectRequest

FieldTypeDescription
org_idstring
namestring

Message: CreateProjectResponse

FieldTypeDescription
project_idstring
project_namestring

Message: CreateEnvironmentRequest

FieldTypeDescription
project_idstring
namestring

Message: CreateEnvironmentResponse

FieldTypeDescription
environment_idstring
environment_namestring

Message: CreateSdkKeyRequest

FieldTypeDescription
environment_idstring

Message: CreateSdkKeyResponse

FieldTypeDescription
sdk_key_idstring
raw_keystringThe raw key — only returned on creation, never stored in plaintext.

Message: CreateUserRequest

FieldTypeDescription
org_idstring
emailstring
display_namestring
passwordstring
org_rolestring“org_admin” or “org_member”

Message: CreateUserResponse

FieldTypeDescription
user_idstring
emailstring
display_namestring

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.

FieldTypeDescription
int_valueint64
double_valuedouble
string_valuestring
bool_valuebool
semver_valuestringSemVer 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 instance
  • parameters: typed attributes used in rule evaluation
  • private_parameters: names of parameters that must NOT appear in any log or telemetry output
FieldTypeDescription
context_typestring
keystring
parametersmap<string, ParameterValue>
private_parametersrepeated 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

FieldTypeDescription
environment_idstring
flag_keystring

Message: ListFlagsRequest

FieldTypeDescription
environment_idstring

Message: ListFlagsResponse

FieldTypeDescription
flagsrepeated FeatureFlag

Enum: MutationKind

ValueDescription
MUTATION_KIND_UNSPECIFIED
MUTATION_KIND_CREATE
MUTATION_KIND_UPDATE
MUTATION_KIND_DELETE
MUTATION_KIND_ARCHIVE

Message: MutateFlagRequest

FieldTypeDescription
environment_idstring
kindMutationKind
flagFeatureFlag
versionuint64Optimistic-locking version — required for UPDATE / DELETE / ARCHIVE. Server rejects with ABORTED if stored version differs.

Message: MutateFlagResponse

FieldTypeDescription
flagFeatureFlag
versionuint64

Message: GetFlagDefinitionsRequest

FieldTypeDescription
environment_idstring

Message: FlagHashingConfig

Controls which context parameters are used for percentage-rollout hashing.

FieldTypeDescription
parameter_keystring
parameter_typestring
orderint32

Message: UpdateFlagHashingRequest

FieldTypeDescription
environment_idstring
flag_keystring
configsrepeated FlagHashingConfig

Message: UpdateFlagHashingResponse

FieldTypeDescription
flagFeatureFlag
configsrepeated 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

FieldTypeDescription
bool_valuebool
int_valueint64
double_valuedouble
string_valuestring
json_valuestringJSON serialised as string

Message: Variant

FieldTypeDescription
keystring
valueVariantValue

Message: AllocationBucket

A single bucket in a percentage rollout.

FieldTypeDescription
variant_keystringVariant key this bucket maps to.
weight_milliuint32Weight 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

FieldTypeDescription
context_hash_specsmap<string, ContextHashSpec>Context types and their parameter names to include in the hash. An empty parameter list means use the context key only.
bucketsrepeated AllocationBucket

Message: ContextHashSpec

FieldTypeDescription
parameter_namesrepeated stringIf empty, only the context key is hashed.

Message: FlagRule

FieldTypeDescription
rule_payloadbytesOpaque serialised rule payload — parsed by the client rule engine. Full typed rule messages will replace this in the rules track.
variant_keystring
allocationPercentageAllocation

Enum: FlagValueType

ValueDescription
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

FieldTypeDescription
keystring
enabledbool
value_typeFlagValueType
variantsrepeated Variant
rulesrepeated FlagRuleEvaluated in order; first match wins. Last rule is the default fallback.

Message: SyncRequest

FieldTypeDescription
contextsrepeated stitchd.common.v1.ContextContexts the client was initialised with.

Message: SyncResponse

FieldTypeDescription
flagsrepeated FeatureFlag
server_timestamp_msint64Epoch millis — client uses this to detect stale payloads.
rule_segmentsrepeated stitchd.segments.v1.RuleSegmentRule-based segment definitions for local evaluation.
list_segmentsrepeated stitchd.segments.v1.ListSegmentMetaList-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_idstringUUID 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.

FieldTypeDescription
keystring
context_typestring
included_keysrepeated string
excluded_keysrepeated 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.)

FieldTypeDescription
keystring
context_typestring
rule_payloadbytesOpaque serialised rule payload — parsed by the client rule engine.
idstringUUID 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.

FieldTypeDescription
keystringSegment key, used in flag rules and as the segment_key in list-check requests.
context_typestringThe context type this segment applies to (e.g. “user”, “org”).
idstringUUID 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).

FieldTypeDescription
list_segmentsrepeated ListSegment
rule_segmentsrepeated 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

FieldTypeDescription
environment_idstring
segment_keystring

Message: ListSegmentsRequest

FieldTypeDescription
environment_idstring

Message: ListSegmentsResponse

FieldTypeDescription
rule_segmentsrepeated RuleSegment
list_segmentsrepeated ListSegmentMeta

Message: EvaluateMembershipRequest

FieldTypeDescription
environment_idstring
segment_keystring
context_keystringThe context key to test for membership.
context_typestring

Message: EvaluateMembershipResponse

FieldTypeDescription
is_memberbool

Enum: SegmentMutationKind

ValueDescription
SEGMENT_MUTATION_KIND_UNSPECIFIED
SEGMENT_MUTATION_KIND_CREATE
SEGMENT_MUTATION_KIND_UPDATE
SEGMENT_MUTATION_KIND_DELETE

Message: MutateSegmentRequest

FieldTypeDescription
environment_idstring
kindSegmentMutationKind
rule_segmentRuleSegment
list_segmentListSegment
versionuint64

Message: MutateSegmentResponse

FieldTypeDescription
rule_segmentRuleSegment
list_segmentListSegment
versionuint64

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

FieldTypeDescription
bool_valuebool
int_valueint64
double_valuedouble

Message: Event

A single metric event emitted by the client SDK. Only pre-registered event keys with known types are accepted.

FieldTypeDescription
metric_keystringThe registered event key.
context_typestringMinimal context: type + key only (no parameters — not needed for events).
context_keystring
valueMetricValueThe metric value — must match the registered type for this metric_key.
timestamp_msint64Client-side timestamp in epoch milliseconds.

Message: IngestRequest

FieldTypeDescription
eventsrepeated Event

Message: IngestResponse

FieldTypeDescription
accepted_countuint32Number of events accepted (unknown keys are rejected and not counted).
rejected_keysrepeated stringKeys 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.

FieldTypeDescription
keystring
descriptionstring
value_typeMetricValueTypeExpected value type for this metric.
environment_idstring

Enum: MetricValueType

ValueDescription
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

ValueDescription
EXPERIMENT_STATUS_UNSPECIFIED
EXPERIMENT_STATUS_DRAFT
EXPERIMENT_STATUS_ACTIVE
EXPERIMENT_STATUS_PAUSED
EXPERIMENT_STATUS_CONCLUDED

Message: Experiment

FieldTypeDescription
idstring
environment_idstring
namestring
descriptionstring
flag_keystring
statusExperimentStatus
variant_keysrepeated string
created_at_msint64
updated_at_msint64
versionuint64

Message: ExperimentIteration

FieldTypeDescription
idstring
experiment_idstring
iteration_numberint32
started_at_msint64
ended_at_msint64Zero when the iteration is still running.
metric_keysrepeated string
traffic_allocationdouble

Message: VariantResult

FieldTypeDescription
variant_keystring
participant_countuint64
metric_valuesmap<string, double>
p_valuedouble
p_value_presentbool

Message: ExperimentResults

FieldTypeDescription
experiment_idstring
variant_resultsrepeated VariantResult
computed_at_msint64
is_stalebool
next_run_at_msint64
computation_statusstring

Message: CreateExperimentRequest

FieldTypeDescription
experimentExperiment

Message: UpdateExperimentRequest

FieldTypeDescription
experimentExperiment

Message: DeleteExperimentRequest

FieldTypeDescription
environment_idstring
experiment_idstring

Message: GetExperimentRequest

FieldTypeDescription
environment_idstring
experiment_idstring

Message: ListExperimentsRequest

FieldTypeDescription
environment_idstring

Message: ListExperimentsResponse

FieldTypeDescription
experimentsrepeated Experiment

Message: GetResultsRequest

FieldTypeDescription
environment_idstring
experiment_idstring

Message: TransitionExperimentRequest

FieldTypeDescription
environment_idstring
experiment_idstring
new_statusExperimentStatus
reasonstringHuman-readable reason for the transition (optional).

Message: ListIterationsRequest

FieldTypeDescription
environment_idstring
experiment_idstring

Message: ListIterationsResponse

FieldTypeDescription
iterationsrepeated 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

FieldTypeDescription
experiment_idstringUUID of the experiment to recompute stats for.

Message: TriggerRecomputeResponse

FieldTypeDescription
job_idstringUUID of the created stats job.
statusstringInitial status — always “pending”.
created_at_msint64Milliseconds since Unix epoch when the job was created.

Message: GetJobStatusRequest

FieldTypeDescription
job_idstringUUID of the stats job to query.

Message: GetJobStatusResponse

FieldTypeDescription
job_idstring
statusstring
started_at_msint64Zero when the job has not yet started.
completed_at_msint64Zero when the job has not yet completed.
errorstringEmpty 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.rs module docs. Run cargo xtask docs to 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

DependencyMinimum VersionPurpose
PostgreSQL16+Feature flag config, tenants, RBAC, audit logs
ClickHouse24+Experiment events and metric aggregations (upcoming)
Rust toolchainstableBuilding 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 (default 8080) — REST Admin API
  • GRPC_PORT (default 9090) — 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-events crate 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

VariableDescription
DATABASE_URLPostgreSQL connection string, e.g. postgres://user:pass@host/stitchd

Optional

VariableDefaultDescription
HTTP_PORT8080Port for the REST Admin API
GRPC_PORT9090Port 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:

  1. Create a new key via the API
  2. Deploy the new key to your application (update SdkConfig)
  3. Verify the new key is active and receiving traffic
  4. 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

CrateRoleType
stitchd-gatewayREST + gRPC gateway — single entry point for all external trafficBinary
stitchd-auth-serviceAuthentication (login, JWT) and organisation/project managementBinary
stitchd-flag-serviceFlag definitions, variant management, SDK flag-sync streamingBinary
stitchd-segmentation-serviceSegment membership evaluation and list-segment checksBinary
stitchd-event-serviceExperiment event ingestion, forwarded to ClickHouseBinary
stitchd-experimentation-serviceExperiment CRUD and result aggregationBinary
stitchd-stats-serviceScheduled statistics computation (60-min loop), on-demand recompute jobs, stats_jobs + stats_schedule managementBinary
stitchd-sdkServer-side Rust SDK — in-process flag evaluationLibrary
stitchd-coreDomain model, rule engine, segmentation logic, hashing, ID typesLibrary
stitchd-dbDatabase access layer (sqlx repositories + ClickHouse)Library
stitchd-protoProtobuf definitions and generated tonic stubs for all servicesLibrary
stitchd-eventsClickHouse event ingestion and migration helpersLibrary
xtaskBuild tool: mdBook docs generation, tool installationBinary

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 match
  • Or(Vec<ConditionExpr>) — any child must match
  • Not(Box<ConditionExpr>) — negation
  • Leaf { 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)

EntityNotes
Feature Flag DefinitionsThe flag key and type are project-level
Variant ConfigurationsWhich variants exist and their values

Sharing definitions across environments ensures flag keys are consistent when promoting from devstagingprod.

Environment-scoped

EntityNotes
RulesWhich users/segments see which variant, in which environment
SegmentsRule-based and list-based segment definitions
SDK KeysOne or more keys per environment for SDK authentication
ExperimentsA/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 CategoryExamples
Identitytenants, projects, environments, sdk_keys
Feature Flagsfeature_flags, variants, flag_values
Segmentationsegments, segment_rules, list_segment_members
Experimentationexperiments, experiment_metrics
Auditaudit_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 CategoryNotes
Evaluation eventsOne row per (flag, context, variant, timestamp)
Experiment resultsPre-aggregated metric values per experiment arm
TelemetryOptional 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

QuestionStore
“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