pbflags

module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 4, 2026 License: MIT

README

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

  1. Global KILLED -> compiled default (polled every ~30s)
  2. Per-entity override KILLED/DEFAULT -> compiled default
  3. Per-entity override ENABLED -> override value
  4. Global DEFAULT -> compiled default
  5. Global ENABLED -> configured value
  6. 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

Directories

Path Synopsis
cmd
pbflags-server command
Binary pbflags-server is the feature flag evaluation service.
Binary pbflags-server is the feature flag evaluation service.
pbflags-sync command
pbflags-sync reads a descriptors.pb file and syncs feature/flag definitions into PostgreSQL.
pbflags-sync reads a descriptors.pb file and syncs feature/flag definitions into PostgreSQL.
protoc-gen-pbflags command
protoc-gen-pbflags generates type-safe flag client code from feature proto definitions.
protoc-gen-pbflags generates type-safe flag client code from feature proto definitions.
Package db provides embedded database migrations for pbflags.
Package db provides embedded database migrations for pbflags.
gen
internal
admin/web
Package web provides an embedded admin dashboard for the pbflags feature flag system.
Package web provides an embedded admin dashboard for the pbflags feature flag system.
codegen/gogen
Package gogen generates Go flag client code from feature proto definitions.
Package gogen generates Go flag client code from feature proto definitions.
codegen/javagen
Package javagen generates Java flag client code from feature proto definitions.
Package javagen generates Java flag client code from feature proto definitions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL