Skip to main content

Yabanin — Development Guide

Prerequisites​

  • Go 1.27+ (go version should report go1.27.x or newer; the module builds clean on 1.27)
  • Docker with Docker Compose v2
  • make (bundled with Xcode Command Line Tools on macOS)
  • psql — optional, needed only for local migration runs outside Docker

Quick Start​

# 1. Copy the environment template
cp .env.example .env # edit if you need non-default values

# 2. Bring up Postgres, Redis, and all three services
make docker-up

# 3. Run the smoke test (starts infra if not running, checks health)
bash scripts/smoke_test.sh

# 4. Tear everything down
make docker-down

After docker-up the services are available at:

ServiceListen (container)Host (compose)Health
gateway:80828082/healthz
gateway admin/metrics:90909090—
admin control plane:80869091/health
loggerd:80858085/healthz
mcp SSE:8787— (helm only)—

Project Structure​

.
├── services/ # Service binaries (each is its own package)
│ ├── gateway/cmd/main.go # Reverse-proxy / API router (data-plane)
│ ├── loggerd/cmd/main.go # Redis stream consumer → Postgres writer
│ └── admin/cmd/main.go # Admin panel + CRUD APIs (control-plane)
├── pkg/ # Shared library packages
│ ├── config/ # YAML config loading + env-var resolution
│ ├── auth/ # API key & session authentication
│ ├── adminauth/ # Admin-specific HMAC cookie auth
│ ├── adminstore/ # Admin user store (Postgres)
│ ├── rulestore/ # Routing rules CRUD store
│ ├── evaluator/ # Rule matching engine
│ ├── openai/ # OpenAI API request/response types
│ ├── resolver/ # Model routing resolution
│ ├── redisutil/ # Redis client wrapper
│ ├── log/ # Structured logging (slog)
│ ├── logging/ # Logrus bridge / formatting
│ ├── events/ # Event payload types
│ ├── eventpublisher/ # Redis stream publisher interface
│ ├── cli/ # Service lifecycle helpers
│ └── encryption/ # AES-GCM token encryption
├── migrations/ # SQL migration files (applied in lexical order)
│ ├── 0001_init.sql # Core schema: orgs, models, api_keys, sessions, messages, rules, logs
│ ├── 0002_admin_and_teams.sql
│ ├── 0003_audit_logs.sql
│ ├── 0004_updated_at.sql
│ └── 0005_add_api_key_team_id.sql
├── deploy/ # Production deployment artifacts
│ ├── docker-compose.yml # Full stack definition (postgres, redis, gateway, loggerd, admin)
│ ├── .env # Docker Compose environment variables
│ └── secrets/ # Docker secrets (one file per secret)
│ ├── pg_uri
│ ├── pg_password
│ ├── session_key
│ ├── cache_hmac
│ └── admin_secrets
├── scripts/ # Helper scripts
│ └── smoke_test.sh # E2E smoke test (health checks + proxy call)
├── config.yaml # Per-service YAML config (merged with env vars)
├── Dockerfile # Multi-stage Go build for all services
├── Makefile # Build, test, run, docker targets
└── go.mod # Module: github.com/reireirei/yabanin

Note: the unified binary source lives in cmd/yabanin/ (historical spelling with "i"); the binary and CLI are named yabanin. Do not create new yabanin-spelled paths.

Development Workflow​

All commands are via make:

For the same reproducible interface locally, from an agent, or in future CI, start with make quality-changed (plan), then make quality-changed QUALITY_EXECUTE=1 (execute). make quality-full runs all standard profiles and records logs/results in artifacts/quality/. See scripts/quality/README.md in the repository for prerequisites, profiles, explicit Docker integration, and validating evidence against source.

CommandDescription
make buildCompile all three services (yabanin-gateway, yabanin-loggerd, yabanin-admin)
make testRun all unit tests with race detection
make test-integrationRun integration tests (uses testcontainers for real Postgres/Redis)
make lintFull static analysis with go vet and golangci-lint
make analyze-fastFast static-analysis loop after Go edits
make analyze-securityModule verification and reachable vulnerability scan
make analyze-testsRace detector, shuffled tests, and coverage report
make analyzeLegacy Go-only aggregate: lint, security, and tests
make tidygo mod tidy
make run-gatewayBuild and run gateway in foreground
make run-loggerdBuild and run loggerd in foreground
make run-adminBuild and run admin in foreground
make run-mcpMCP partner bridge (stdio; set YABANIN_MCP_HTTP=1 for SSE)
make dev-opencodeGenerate OpenCode config and start the fake subscription bridge
make opencode-remote-wireWire OpenCode agents to the public yabanin.ai stand (key: YABANIN_GATEWAY_API_KEY)
make test-subscription-bridgeFake Codex/Cursor sidecar contract tests (Node)
make benchRun benchmarks

Run a single service's tests:

go test -race -count=1 ./services/gateway/...
go test -race -count=1 ./pkg/rulesengine/...

The analysis targets install pinned versions of golangci-lint and govulncheck into $(go env GOPATH)/bin when necessary. Project-local OpenCode skills under .opencode/skills/ automatically direct agents to the applicable gate after Go, security-sensitive, dependency, test, or concurrency changes.

OpenCode orchestration (local)​

Three separate workflows:

  1. yb audit — offline analyzer. No gateway, no MCP, no subscription bridge.
  2. Ordinary gateway — OpenAI-compatible /v1/chat/completions for Z.AI Coding Plan and local models. Feature flag YABANIN_ZAI_CODING_ENABLED is off by default.
  3. OpenCode orchestration — OpenCode talks to the gateway for chat/tools, and to local MCP tr_delegate for Codex/Cursor agents. Delegation is off unless YABANIN_DELEGATION_ENABLED=true.

OpenCode wiring (the isolated local stand make stand-opencode was retired on 2026-10-12; its launcher scripts were removed):

make opencode-remote-wire # wires global OpenCode agents (yabanin, luna, sol-review) to https://yabanin.ai/gw

The wire target needs YABANIN_GATEWAY_API_KEY (operator-issued stand/org key) and a locally built yabanin binary for the MCP server (make build). It updates ~/.local/share/opencode/auth.json and ~/.config/opencode/opencode.jsonc (both chmod 600). Restart OpenCode and Tab-switch to the agent.

Credentials stay on the machine: gateway key in auth.json (never committed), Codex from ~/.codex/auth.json, Cursor from official agent login (subscription, not a dashboard API key).

One-command local mock loop:

make docker-up # or make run-gateway against local infra
export YABANIN_API_KEY=... # gateway client key; do not commit
export YABANIN_PARTNER_API_KEY=... # MCP → gateway key; do not commit
make dev-opencode # writes examples/opencode.generated.json and starts a fake bridge on :8091

Then point OpenCode at the generated config (or copy examples/opencode.json). Login for real agents stays in official CLIs:

  • Z.AI Coding Plan: enable YABANIN_ZAI_CODING_ENABLED=true and store the Coding Plan key on the zai-coding model in the registry (do not commit it)
  • Codex: official codex login or CODEX_API_KEY (never browser cookies)
  • Cursor: official agent login (Cursor subscription). Do not set CURSOR_API_KEY

Set YABANIN_BRIDGE_FAKE=0 when you want the sidecar in services/subscription-bridge/ to call the official SDKs. The leading model should use native OpenCode tools for ordinary work and tr_delegate only for hard coding tasks; see examples/opencode-yabanin.md.

Docker Deployment​

docker-compose.yml​

deploy/docker-compose.yml defines five services:

  • postgres — PostgreSQL 16 Alpine, health-checked, read-only filesystem
  • redis — Redis 7 Alpine with AOF persistence, read-only
  • migrate — one-shot container that runs all migrations/*.sql against Postgres (runs once on first up)
  • gateway — the data-plane reverse proxy (image: yabanin-gateway:latest)
  • loggerd — async log consumer (image: yabanin-loggerd:latest)
  • admin — control-plane admin panel (image: yabanin-admin:latest)

All app services sit on the app-internal bridge network. Ports 8082, 8085, 9091, and 5432/6379 are exposed for local development.

Building images​

# Build all three service images locally
make docker-build-all

# Each image is tagged: yabanin-gateway, yabanin-loggerd, yabanin-admin

The Dockerfile uses a Go 1.25 Alpine builder with multi-stage builds and cache mounts. The final stage runs as a non-root user (yabanin).

Secrets management​

Secrets are passed via Docker secrets (mounted at /run/secrets/<name>) and consumed through the _FILE env-var convention:

Secret fileEnv var used by app
deploy/secrets/pg_uriYABANIN_PG_URI_FILE → reads DB connection string
deploy/secrets/session_keyYABANIN_SESSION_KEY_FILE → HMAC cookie signing key
deploy/secrets/cache_hmacYABANIN_CACHE_HMAC_SECRET_FILE → Redis response signing

To add a new secret:

  1. Create the file: echo -n "my-secret-value" > deploy/secrets/my_secret
  2. Add it to .dockerignore (never commit secrets)
  3. Declare it in deploy/docker-compose.yml under secrets: and reference it per-service
  4. The _FILE suffix is auto-resolved by pkg/config/env()

Testing​

Unit tests​

make test # all packages, race detector enabled
go test -race -count=1 ./pkg/rulesengine/... # single package

Integration tests​

Integration tests use testcontainers-go to spin up real Postgres and Redis containers:

make test-integration # runs services/gateway/integration/...
go test -race -count=1 ./services/loggerd/integration/...

Test helpers live in each service's integration/ subdirectory. Use test_helpers.go for shared setup (seed data, client creation) and *_test.go for test cases.

Smoke / E2E test​

bash scripts/smoke_test.sh # starts compose, waits for health, proxies a request, tears down

Adding a New Service​

  1. Create the directory structure:

    mkdir -p services/<name>/cmd
  2. Write services/<name>/cmd/main.go following the existing pattern:

    package main

    import (
    "github.com/reireirei/yabanin/pkg/config"
    "github.com/reireirei/yabanin/pkg/logging"
    )

    func main() {
    cfg, _ := config.Load("config.yaml")
    // validate cfg.<ServiceName>, connect to Postgres/Redis, start server
    }
  3. Add a make target in Makefile:

    <name>:
    go build -o yabanin-<name> ./services/<name>/cmd/...

    run-<name>: build
    ./yabanin-<name>

    Add <name> to the .PHONY line and to the build: dependency.

  4. Add config fields in pkg/config/config.go:

    type MyService struct { ... }
    func DefaultMyService() MyService { ... }
    func (c MyService) Validate() error { ... }
    // Add to All struct and DefaultAll()
  5. Add Docker support:

    • Copy the service block in deploy/docker-compose.yml from an existing service as a template
    • Update SERVICE build arg, ports, healthcheck, secrets
    • Run make docker-build-all to build the image
  6. Add tests under services/<name>/integration/ if it needs integration coverage.

Migrations​

Migration files live in migrations/ and are applied in lexical order (filename prefix determines sequence). Each file should be idempotent where possible (CREATE TABLE IF NOT EXISTS).

Running migrations​

# Via Docker compose (uses the migrate one-shot service)
make docker-up # migrate runs automatically on first up

# Via local psql
export YABANIN_PG_URI="postgres://yabanin:yabanin@localhost/yabanin?sslmode=disable"
make migrate-up

# Manually
for f in migrations/*.sql; do psql $YABANIN_PG_URI -f "$f"; done

Writing a new migration​

  1. Create migrations/0006_<description>.sql (next sequential number)
  2. Use CREATE TABLE IF NOT EXISTS, ALTER TABLE, etc.
  3. Test locally: run make migrate-up against a fresh DB
  4. The Docker Compose migrate service runs migrations on every up — use docker compose down --volumes && make docker-up to test a full reset

Configuration​

config.yaml​

A single config.yaml at the project root holds all service configs, each under its own top-level key:

gateway:
listen_address: ":8082"
admin_listen: ":9090"
provider: "localhost:8081"
redis_addr: "redis:6379"
pg_uri: "postgres://yabanin:yabanin@postgres/yabanin?sslmode=disable"
log_level: "info"
log_format: "json"
auth_mode: "header" # header | strict
api_key: ""
fallback_api_key: "yabanin-dev-key"
fallback_model: "smart" # smart | cheap | turbo
event_stream: "events:queue"

loggerd:
listen_address: ":8085"
stream_name: "events:queue"
redis_addr: "redis:6379"
pg_uri: "postgres://yabanin:yabanin@postgres/yabanin?sslmode=disable"
log_level: "info"
log_format: "json"

admin:
listen_address: ":8086"
pg_uri: "postgres://yabanin:yabanin@postgres/yabanin?sslmode=disable"
session_key: "" # HMAC signing key for admin cookies
admin_domain: "localhost"
encryption_key: "" # 32-byte key for AES-GCM token encryption

Environment variables​

Every config field can be overridden via YABANIN_* env vars. The _FILE suffix reads the value from a file (for Docker secrets / Kubernetes):

Env varDefaultDescription
YABANIN_CONFIGconfig.yamlPath to YAML config file
YABANIN_CONFIG_STRICToffFail-closed env parsing for CI: 1/true makes a malformed numeric env value (YABANIN_* int/float) fail config load with the variable named; default (unset/0) warns and keeps the built-in default. Duration fields (*_timeout, *_ttl, stream_max_duration, …) are always checked by Validate() regardless of this flag
YABANIN_GATEWAY_LISTEN:8082Gateway HTTP listen address
YABANIN_GATEWAY_ADMIN:9090Gateway admin metrics endpoint
YABANIN_PROVIDER_ADDRlocalhost:8081Upstream API provider address
YABANIN_REDIS_ADDRlocalhost:6379Redis connection address
YABANIN_PG_URI—PostgreSQL DSN (required)
YABANIN_LOG_LEVELinfoLog level: debug, info, warn, error
YABANIN_LOGGERD_STREAMevents:queueRedis stream name
YABANIN_ADMIN_LISTEN:8086Admin panel listen address
YABANIN_ADMIN_DOMAINlocalhostCookie domain for admin sessions
YABANIN_SESSION_KEY—HMAC signing key (required for admin)
YABANIN_CACHE_HMAC_SECRET—Secret for signing Redis-cached responses

Config loading priority​

  1. Defaults — hardcoded in pkg/config/*.go
  2. YAML config file — loaded from path in YABANIN_CONFIG (or config.yaml)
  3. Environment variables — override YAML values; _FILE suffix reads from file paths

The resolution order is: defaults → YAML merge → env var override. For secrets, use the _FILE variant to point at Docker secret mounts.

Malformed numeric env values (YABANIN_CIRCUIT_FAILURE_THRESHOLD=abc, …) always produce a startup warning that names the variable and keep the built-in default. Set YABANIN_CONFIG_STRICT=1 (recommended in CI) to turn that warning into a load failure. Malformed duration values (YAML or env) fail Validate() at startup in both modes; existing valid configs are unaffected.

Boolean env vars accept exactly 1 or true (case-insensitive) as true; every other non-empty value (0, false, yes, garbage, untrimmed) is false — not the fallback. Only unset/empty keeps the default, so for flags that default to true the variable must be left unset to keep it. This is a silent contract: boolean parsing never warns and YABANIN_CONFIG_STRICT does not apply to it.