This guide walks you through setting up OpenDecree, creating your first schema and tenant, setting config values, and reading them from a Go application.
- Docker and Docker Compose
- Go 1.24+ (for the SDK and CLI)
git clone https://github.com/opendecree/decree.git
cd decree
# Start PostgreSQL, Redis, run migrations, and start the service.
# The first run builds the server and migration images from source,
# so it may take a few minutes; subsequent runs reuse the cached images.
docker compose up -d --wait serviceThe gRPC service is now available at localhost:9090. No JWT setup needed — the service defaults to metadata-based auth.
brew tap opendecree/tap
brew install decreego install github.com/opendecree/decree/cmd/decree@v0.12.0-alpha.5Set your identity (required for all operations):
export DECREE_SUBJECT=admin@example.comA schema defines the structure of your configuration — what fields exist, their types, and constraints. Create a file called decree.schema.yaml (or <name>.decree.schema.yaml if your repo holds multiple schemas):
# yaml-language-server: $schema=https://schemas.opendecree.dev/schema/v0.1.0/decree-schema.json
spec_version: "v1"
name: payments
description: Payment processing configuration
fields:
payments.enabled:
type: bool
description: Whether payment processing is active
payments.fee_rate:
type: number
description: Fee percentage per transaction
constraints:
minimum: 0
maximum: 1
payments.currency:
type: string
description: Default settlement currency
constraints:
enum: [USD, EUR, GBP]
payments.max_retries:
type: integer
description: Maximum retry attempts for failed payments
constraints:
minimum: 0
maximum: 10
payments.timeout:
type: duration
description: Payment processing timeoutImport and publish it:
# Import and auto-publish in one step
decree schema import --publish decree.schema.yamlOr import as draft first, then publish separately:
decree schema import decree.schema.yaml
decree schema publish <schema-id> 1Only published schema versions can be assigned to tenants.
A tenant is a consumer of configuration — an organization, environment, or service instance bound to a schema version:
decree tenant create --name acme --schema <schema-id> --schema-version 1Note the tenant ID from the output — you'll use it for all config operations.
# Set individual values
decree config set <tenant-id> payments.enabled true
decree config set <tenant-id> payments.fee_rate 0.025
decree config set <tenant-id> payments.currency USD
decree config set <tenant-id> payments.max_retries 3
decree config set <tenant-id> payments.timeout 30s
# Or set multiple values at once
decree config set-many <tenant-id> \
payments.enabled=true \
payments.fee_rate=0.025 \
payments.currency=USD \
--description "Initial payment config"Read them back:
decree config get-all <tenant-id>Install the SDK:
go get github.com/opendecree/decree/sdk/configclient@v0.12.0-alpha.5Read configuration values with typed getters:
package main
import (
"context"
"fmt"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "github.com/opendecree/decree/api/centralconfig/v1"
"github.com/opendecree/decree/sdk/configclient"
)
func main() {
// Connect to the service
conn, err := grpc.NewClient("localhost:9090",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Create a config client
client := configclient.New(
pb.NewConfigServiceClient(conn),
configclient.WithSubject("myapp"),
)
ctx := context.Background()
tenantID := "<your-tenant-id>"
// Read typed values
enabled, _ := client.GetBool(ctx, tenantID, "payments.enabled")
feeRate, _ := client.GetFloat(ctx, tenantID, "payments.fee_rate")
currency, _ := client.Get(ctx, tenantID, "payments.currency")
retries, _ := client.GetInt(ctx, tenantID, "payments.max_retries")
timeout, _ := client.GetDuration(ctx, tenantID, "payments.timeout")
fmt.Printf("Payments enabled: %v\n", enabled)
fmt.Printf("Fee rate: %.3f\n", feeRate)
fmt.Printf("Currency: %s\n", currency)
fmt.Printf("Max retries: %d\n", retries)
fmt.Printf("Timeout: %s\n", timeout)
}When handling a request, you may want all config reads to come from the same version — even if config is being updated concurrently:
// Pin to the current version
snap, _ := client.Snapshot(ctx, tenantID)
// All reads use the same version
fee, _ := snap.Get(ctx, "payments.fee_rate")
currency, _ := snap.Get(ctx, "payments.currency")
// Guaranteed consistent — both from the same config versionFor long-running services that need to react to config changes in real-time, use the configwatcher SDK:
go get github.com/opendecree/decree/sdk/configwatcher@v0.12.0-alpha.5import "github.com/opendecree/decree/sdk/configwatcher"
// Register typed fields with defaults
w := configwatcher.New(conn, tenantID,
configwatcher.WithSubject("myapp"),
)
feeRate := w.Float("payments.fee_rate", 0.01)
enabled := w.Bool("payments.enabled", false)
// Start watching (loads initial values, then subscribes to changes)
w.Start(ctx)
defer w.Close()
// Read current values (always fresh, never blocks)
fmt.Println(feeRate.Get()) // 0.025
fmt.Println(enabled.Get()) // true
// React to changes
go func() {
for change := range feeRate.Changes() {
log.Printf("Fee rate changed: %v → %v", change.Old, change.New)
}
}()Every config change creates a new version. You can list versions and rollback:
# List versions
decree config versions <tenant-id>
# Rollback to version 1
decree config rollback <tenant-id> 1Export your config for backup, review, or migration between environments:
# Export config as YAML
decree config export <tenant-id> > config-backup.yaml
# Import into another tenant or environment
decree config import <other-tenant-id> config-backup.yaml- Concepts — understand schemas, tenants, typed values, and versioning in depth
- API Reference — full gRPC service and message definitions
- SDKs — configclient, adminclient, and configwatcher documentation
- CLI Reference — all
decreecommands - Server Configuration — environment variables and deployment