Run it and use the API

A self-hosted feature-flag service. Ship new code switched off, then turn it on from a dashboard: per environment, for chosen groups of users, or for a percentage of them. Apps evaluate flags in memory, with no network call per check, and hear about changes within about a second.

New to feature flags, or not a developer? Start with the plain-language guide: what Switchyard does, and how to use the dashboard step by step.

What it does

Status: phases 1–9 are built and tested; see PLAN.md. Deploying it publicly on free plans is a step-by-step guide away.

Folder What
server The Spring Boot service: admin API and SDK API
dashboard The Angular admin UI
clients/java switchyard-java, the zero-dependency Java client
clients/js switchyard-js, the zero-dependency client for Node, Deno, Bun and edge runtimes, with an OpenFeature provider
clients/web switchyard-web, for web pages, with React hooks
clients/java-openfeature switchyard-openfeature, the OpenFeature provider for Java
demo 100 pretend users that light up as you change a flag
spec How flags are evaluated, plus test vectors every client must pass
benchmarks JMH for the Java client, k6 for the SDK API
docs The user guide and the deploy guide
site The public website: a landing page, plus these docs as web pages (npm run build, then npm run preview for http://localhost:4300)

Run it locally

Needs Java 17+, Docker (for Postgres) and, for the dashboard, Node.js 22.22.3+ or 24.15+.

bash
# 1. Postgres
docker compose up -d

# 2. Server on http://localhost:8080, allowing the dashboard's origin
cd server
SWITCHYARD_ADMIN_TOKEN=dev-admin-token SWITCHYARD_DASHBOARD_ORIGINS=http://localhost:4200 ./mvnw spring-boot:run

# 3. Dashboard on http://localhost:4200 (in another terminal)
cd dashboard
npm install
npm start

Open http://localhost:4200 and sign in with server URL http://localhost:8080 and token dev-admin-token.

No Postgres to hand? With Docker running, ./mvnw spring-boot:test-run starts the server with a throwaway database instead of step 2.

Use it from Java

java
SwitchyardClient flags = SwitchyardClient.builder(sdkKey).baseUri("http://localhost:8080").build();
if (flags.isEnabled("new-checkout", false)) { ... }

// With targeting rules or a percentage rollout, say who the check is for
EvaluationContext user = EvaluationContext.builder(userId).attribute("email", email).build();
if (flags.isEnabled("new-checkout", user, false)) { ... }

// Multivariate flags: typed values, with a default for anything unexpected
String color = flags.stringVariation("checkout-button", user, "green");

How to add the library and how it behaves: clients/java/README.md.

Use it from JavaScript

ts
import { SwitchyardClient } from 'switchyard-js';

const flags = new SwitchyardClient({ sdkKey, baseUrl: 'http://localhost:8080' });
if (flags.isEnabled('new-checkout', { key: userId, attributes: { email } }, false)) { ... }

Server-side only (the SDK key can read every rule). Details: clients/js/README.md. For web pages and React, see clients/web. To see both working, run the demo.

Use the API directly

Everything the dashboard does goes through the admin API:

bash
ADMIN="Authorization: Bearer dev-admin-token"

# Create a project. The response shows each environment's SDK key once: keep them.
curl -s -X POST localhost:8080/api/projects -H "$ADMIN" -H "Content-Type: application/json" \
  -d '{"key":"shop","name":"Shop"}'

# Create a flag (off everywhere), then turn it on in production
curl -s -X POST localhost:8080/api/projects/shop/flags -H "$ADMIN" -H "Content-Type: application/json" \
  -d '{"key":"new-checkout","name":"New checkout"}'
curl -s -X PUT localhost:8080/api/projects/shop/flags/new-checkout/environments/production \
  -H "$ADMIN" -H "Content-Type: application/json" -d '{"enabled":true}'

# Staff always, India and Singapore at 50%, everyone else at 10%
curl -s -X PUT localhost:8080/api/projects/shop/flags/new-checkout/environments/production/targeting \
  -H "$ADMIN" -H "Content-Type: application/json" -d '{
    "rules": [
      { "clauses": [{ "attribute": "email", "operator": "ends_with", "values": ["@acme.com"] }],
        "serve": { "value": true } },
      { "clauses": [{ "attribute": "country", "operator": "in", "values": ["IN", "SG"] }],
        "serve": { "rollout": { "percentage": 50 } } }
    ],
    "fallthrough": { "rollout": { "percentage": 10 } }
  }'

# What a client library sees, with the production SDK key
curl -si localhost:8080/sdk/v1/flags -H "Authorization: Bearer <production sdk key>"
# Repeat with  -H 'If-None-Match: "<etag from above>"'  to get 304 when nothing changed

# Who changed what, and undo one change by its id
curl -s localhost:8080/api/projects/shop/audit -H "$ADMIN"
curl -s -X POST localhost:8080/api/projects/shop/audit/<id>/revert -H "$ADMIN"

Scripts and CI pipelines are better off with an API token (dashboard: API tokens, admins only): it has its own role, can be limited to one project, and History names it.

bash
TOKEN="Authorization: Bearer api-…"

# Switch on in production at 09:00 UTC tomorrow
curl -s -X POST localhost:8080/api/projects/shop/changes -H "$TOKEN" -H "Content-Type: application/json" \
  -d '{"flag":"new-checkout","environment":"production","enabled":true,"at":"2026-11-02T09:00:00Z"}'

# What's waiting for an approval or a time
curl -s localhost:8080/api/projects/shop/changes -H "$TOKEN"

Scripts using the admin token can name themselves in History with -H "X-Switchyard-Actor: deploy-bot". The admin token also skips "requires approval", for emergencies.

Every endpoint is listed in PLAN.md. Errors use RFC 9457 problem details, with an errors map naming each invalid field. How clients evaluate rules and rollouts is specified in spec/EVALUATION.md.

Configuration (server)

Variable Default Purpose
DATABASE_URL jdbc:postgresql://localhost:5432/switchyard JDBC URL
DATABASE_USER / DATABASE_PASSWORD switchyard / switchyard Database credentials
DATABASE_POOL_SIZE 5 Connection pool size
SWITCHYARD_ADMIN_TOKEN (empty) Bearer token for /api/**: scripts, and signing in to the dashboard without GitHub. With neither this nor GitHub sign-in set, the admin API is off
SWITCHYARD_DASHBOARD_ORIGINS (empty) Comma-separated origins allowed to call the admin API from a browser, e.g. http://localhost:4200. Also the only places GitHub sign-in returns to
SWITCHYARD_GITHUB_CLIENT_ID / SWITCHYARD_GITHUB_CLIENT_SECRET (empty) A GitHub OAuth app, for GitHub sign-in (how)
SWITCHYARD_GITHUB_ALLOWED_USERS (empty) GitHub usernames allowed to sign in, comma-separated
SWITCHYARD_GITHUB_ADMINS (empty) GitHub usernames that are always admins (and may sign in). At least one of these two lists is needed for GitHub sign-in
SWITCHYARD_GITHUB_DEFAULT_ROLE editor The role people get on their first sign-in: viewer, editor or admin. Admins change it on the Members page
SWITCHYARD_GITHUB_CALLBACK_URL (from the request) The callback URL registered with GitHub: <server>/auth/github/callback
SWITCHYARD_SESSION_TTL 12h How long a GitHub sign-in lasts
STREAM_MAX_CONNECTIONS 2000 Live-update streams kept open at once; more get 503 and keep polling
SWITCHYARD_CHANGES_CHECK_EVERY PT15S How often the server applies scheduled changes that are due (ISO-8601 duration)
PORT 8080 HTTP port

Tests

Part Command Notes
Java client cd clients/java && ./mvnw verify Includes the shared evaluation vectors
Server cd server && ./mvnw verify Install the Java client first (./mvnw install in clients/java): the contract tests use it. Integration tests need Docker
Dashboard cd dashboard && npx ng test --watch=false Vitest
JavaScript client cd clients/js && npm test node --test; includes the shared evaluation vectors

CI runs all four on every pull request. On main it also builds the server's GraalVM native image and publishes it to GitHub Container Registry.

Stack

Spring Boot 4 · Java 17 · Postgres (Neon in production) · Flyway · JdbcClient · Server-Sent Events · GraalVM native image · Angular 22 + Angular Material · TypeScript · GitHub Actions · Render · Cloudflare Pages