pbflags
Protocol Buffer-based feature flags with type-safe code generation, multi-tier caching, and a never-throw guarantee.
Note: This project is a learning exercise and research exploration into protobuf-driven feature flag design. It was extracted from a production system to study the patterns independently. If you're building a real product and need feature flags, you probably want Flipt, OpenFeature, or Unleash instead. Those are battle-tested, well-supported, and have ecosystems around them. pbflags exists because we found the proto-as-source-of-truth pattern interesting and wanted to share it.
Overview
pbflags lets you define feature flags as protobuf messages and generates type-safe client code for Go and Java (with TypeScript, Rust, and Node planned). Flags are evaluated by a standalone server that supports three deployment modes:
- Root mode: Direct PostgreSQL access, serves as the source of truth
- Proxy mode: Connects to an upstream evaluator, reduces database connection fan-out
- Combined mode: Root mode with an embedded admin API
Architecture
┌─────────────┐ ┌─────────────────┐ ┌────────────┐
│ Your App │────▶│ pbflags-server │────▶│ PostgreSQL │
│ (Go/Java) │ │ (evaluator) │ │ │
└─────────────┘ └─────────────────┘ └────────────┘
Generated Three-tier cache: Flag state,
type-safe - Kill set (30s) overrides,
client - Global state (5m) audit log
- Overrides (5m LRU)
Quick Start
1. Define flags in proto
syntax = "proto3";
import "pbflags/options.proto";
message Notifications {
option (pbflags.feature) = {
id: "notifications"
description: "Notification delivery controls"
owner: "platform-team"
};
bool email_enabled = 1 [(pbflags.flag) = {
description: "Enable email notifications"
default: { bool_value: { value: true } }
layer: LAYER_USER
}];
string digest_frequency = 2 [(pbflags.flag) = {
description: "Digest email frequency"
default: { string_value: { value: "daily" } }
layer: LAYER_GLOBAL
}];
}
2. Generate client code
# Install the codegen plugin
go install github.com/SpotlightGOV/pbflags/cmd/protoc-gen-pbflags@latest
# Generate via buf
buf generate
Example buf.gen.yaml for Go:
version: v2
plugins:
- local: protoc-gen-pbflags
out: gen/flags
opt:
- lang=go
- package_prefix=github.com/yourorg/yourrepo/gen/flags
inputs:
- directory: proto
Example for Java:
version: v2
plugins:
- local: protoc-gen-pbflags
out: src/main/java
opt:
- lang=java
- java_package=com.yourorg.flags.generated
inputs:
- directory: proto
3. Use in your application (Go)
// Create a client connected to the evaluator
client := notificationsflags.NewNotificationsFlagsClient(evaluatorClient)
// Type-safe flag access with compiled defaults
emailEnabled := client.EmailEnabled(ctx, userID) // bool
frequency := client.DigestFrequency(ctx) // string
4. Use in your application (Java)
// Create via factory method (framework-agnostic)
NotificationsFlags flags = NotificationsFlags.forEvaluator(evaluator);
// Type-safe flag access
boolean emailEnabled = flags.emailEnabled().get(userId);
String frequency = flags.digestFrequency().get();
Java client setup
// Simple: connect by target address
FlagEvaluatorClient client = new FlagEvaluatorClient("localhost:9201");
// Advanced: custom channel (TLS, interceptors, in-process testing)
ManagedChannel channel = ManagedChannelBuilder.forTarget("localhost:9201")
.useTransportSecurity()
.build();
FlagEvaluatorClient client = FlagEvaluatorClient.forChannel(channel);
Java testing
// Add test dependency
// testImplementation("org.spotlightgov.pbflags:pbflags-java-testing:0.3.0")
class MyTest {
@RegisterExtension
static final TestFlagExtension flags = new TestFlagExtension();
@Test
void testOverride() {
flags.set(NotificationsFlags.EMAIL_ENABLED_ID, false);
var nf = NotificationsFlags.forEvaluator(flags.evaluator());
assertFalse(nf.emailEnabled().get());
}
}
Dagger integration (opt-in)
Add java_dagger=true to codegen options to generate a Dagger @Module with @Binds entries and @Inject/@Singleton annotations on implementations:
opt:
- lang=java
- java_package=com.yourorg.flags.generated
- java_dagger=true
This generates FlagRegistryModule.java which binds each *Flags interface to its *FlagsImpl. Include the module in your Dagger component and inject the interfaces directly.
Running the Server
Docker (multi-arch: amd64 + arm64)
docker pull ghcr.io/spotlightgov/pbflags-server
Docker Compose (local development)
docker compose -f docker/docker-compose.yml up
This starts PostgreSQL + pbflags-server in combined mode (evaluator + admin API).
Binary
# Root mode (direct database access)
pbflags-server \
--database=postgres://user:pass@localhost:5432/mydb?sslmode=disable \
--descriptors=descriptors.pb \
--listen=:9201
# Combined mode (root + admin API)
pbflags-server \
--database=postgres://user:pass@localhost:5432/mydb?sslmode=disable \
--descriptors=descriptors.pb \
--listen=:9201 \
--admin=:9200
# Proxy mode (connects to upstream)
pbflags-server \
--server=http://root-evaluator:9201 \
--descriptors=descriptors.pb \
--listen=:9201
Database Migrations
Schema is managed by goose. Run migrations before first startup or after upgrading:
pbflags-server \
--database=postgres://user:pass@localhost:5432/mydb?sslmode=disable \
--upgrade
This applies all pending migrations and exits. Migration state is tracked in the goose_db_version table.
Database schema sync
# Sync flag definitions from descriptors.pb into PostgreSQL
pbflags-sync \
--database=postgres://user:pass@localhost:5432/mydb?sslmode=disable \
--descriptors=descriptors.pb
Admin Web UI
When running in combined mode (--admin), pbflags serves an embedded web dashboard for flag management. The UI is built with server-rendered HTML and htmx.
Features
- Dashboard: Overview of all features and flags with inline state toggles (ENABLED/DEFAULT/KILLED)
- Flag Detail: Per-flag view with state/value editing, override management (USER layer flags), and recent audit history
- Audit Log: Filterable log of all state changes with actor attribution
- Override Management: Add and remove per-entity overrides for USER layer flags
Enabling
Pass the --admin flag (or set PBFLAGS_ADMIN) to start the admin UI:
pbflags-server \
--database=postgres://... \
--descriptors=descriptors.pb \
--admin=:9200
The admin UI is then available at http://localhost:9200/.
Security
- CSRF protection: All mutating requests (POST/DELETE) require a valid CSRF token via double-submit cookie pattern. htmx sends the token automatically.
- Input validation: Flag IDs are validated against the
feature_id/field_number format before processing.
- Internal network only: The admin UI has no authentication. Deploy it behind a VPN, bastion, or internal network. Do not expose it to the public internet.
Proto Definitions (BSR)
Proto definitions are published to the Buf Schema Registry. Consumers can depend on them directly:
# buf.yaml
deps:
- buf.build/spotlightgov/pbflags
Configuration
Environment variables override CLI flags:
| Variable |
Description |
PBFLAGS_DESCRIPTORS |
Path to descriptors.pb |
PBFLAGS_DATABASE |
PostgreSQL connection string (root mode) |
PBFLAGS_SERVER |
Upstream evaluator URL (proxy mode) |
PBFLAGS_LISTEN |
Evaluator listen address (default: localhost:9201) |
PBFLAGS_ADMIN |
Admin API listen address (enables combined mode) |
Flag Evaluation Precedence
- Global KILLED -> compiled default (polled every ~30s)
- Per-entity override ENABLED -> override value
- Per-entity override DEFAULT -> compiled default
- Global DEFAULT -> compiled default
- Global ENABLED -> configured value
- Fallback -> compiled default (always safe)
Key Design Principles
- Never-throw guarantee: All evaluation errors return the compiled default
- Type-safe code generation: Generated interfaces with compile-time type checking
- Graceful degradation: Stale cache served during outages, compiled defaults as last resort
- Fast kill switches: ~30s polling for emergency shutoffs
- Immutable identity: Flag identity is
feature_id/field_number, safe to rename fields
- Audit trail: All state changes logged with actor and timestamp
Repository Structure
pbflags/
├── proto/pbflags/ # Core proto definitions (options, types, services)
├── proto/example/ # Example feature flag definitions
├── gen/ # Generated Go protobuf code
├── cmd/
│ ├── pbflags-server/ # Evaluator server binary
│ ├── pbflags-sync/ # Database schema sync from descriptors
│ └── protoc-gen-pbflags/ # Code generation plugin (Go, Java)
├── internal/
│ ├── evaluator/ # Evaluation engine, caching, health tracking
│ ├── admin/ # Admin API (flag management, audit log)
│ │ └── web/ # Embedded web UI (htmx dashboard)
│ └── codegen/ # Code generators (Go, Java)
├── clients/java/ # Java client library (Gradle)
├── clients/java/testing/ # Java test utilities (InMemoryFlagEvaluator, JUnit 5)
├── db/migrations/ # PostgreSQL schema
└── docker/ # Dockerfile and docker-compose
Clients
| Language |
Status |
Package |
| Go |
Stable |
go get github.com/SpotlightGOV/pbflags |
| Java |
Stable |
org.spotlightgov.pbflags:pbflags-java (Maven Central) |
| Java Testing |
Stable |
org.spotlightgov.pbflags:pbflags-java-testing |
| TypeScript |
Planned |
- |
| Rust |
Planned |
- |
| Node |
Planned |
- |
License
MIT