# perfgate-server
Centralized baseline management for teams that run benchmarks across multiple CI runners.
[](../../LICENSE-MIT)
## Why
Performance baselines live on individual CI runners. When different runners execute benchmarks, they each see different baselines -- or none at all. Promoting, versioning, and auditing baselines becomes a manual chore that does not scale.
`perfgate-server` is a REST API that stores baselines centrally so every CI job, repository, and team member works from the same source of truth. It ships as a single binary with built-in storage, auth, and a web dashboard.
## Quick start
```bash
cargo install perfgate-server
# Shared SQLite server
perfgate-server --storage-type sqlite --database-url ./perfgate.db \
--api-keys admin:pg_live_<32+alnum>:my-project
# Local single-user sandbox
cargo run -p perfgate-cli -- serve --doctor
cargo run -p perfgate-cli -- serve --no-open
```
## Feature highlights
| **Storage backends** | In-memory, SQLite, PostgreSQL |
| **Auth** | API keys (scoped to project + benchmark regex), JWT (HS256), GitHub Actions OIDC, GitLab OIDC, custom OIDC |
| **Role-based access** | Viewer, Contributor, Promoter, Admin |
| **Web dashboard** | Embedded SPA served at `/` with baseline, verdict, flakiness, and audit views |
| **Fleet analytics** | Dependency-change impact tracking and cross-project alerts |
| **Verdict history** | Record and query pass/warn/fail verdicts over time |
| **Observability** | Structured JSON logging, request IDs, `/health`, `/metrics` |
| **Graceful shutdown** | Handles SIGTERM / Ctrl-C cleanly |
## REST API
All data endpoints live under `/api/v1`. The health check and dashboard are at the root.
| `GET` | `/health` | -- | Health check with storage status |
| `GET` | `/metrics` | -- | Prometheus metrics |
| `GET` | `/` | -- | Web dashboard |
| `GET` | `/api/v1/info` | -- | Server info and local-mode status |
| `POST` | `/api/v1/projects/{project}/baselines` | Y | Upload a baseline |
| `GET` | `/api/v1/projects/{project}/baselines` | Y | List baselines (filterable) |
| `GET` | `/api/v1/projects/{project}/baselines/{bench}/latest` | Y | Get latest baseline |
| `GET` | `/api/v1/projects/{project}/baselines/{bench}/versions/{ver}` | Y | Get specific version |
| `DELETE` | `/api/v1/projects/{project}/baselines/{bench}/versions/{ver}` | Y | Soft-delete a version |
| `POST` | `/api/v1/projects/{project}/baselines/{bench}/promote` | Y | Promote a version |
| `POST` | `/api/v1/projects/{project}/verdicts` | Y | Submit a verdict |
| `GET` | `/api/v1/projects/{project}/verdicts` | Y | List verdicts |
| `POST` | `/api/v1/projects/{project}/decisions` | Y | Upload a performance decision |
| `GET` | `/api/v1/projects/{project}/decisions` | Y | List performance decisions |
| `GET` | `/api/v1/projects/{project}/decisions/latest` | Y | Get latest performance decision |
| `POST` | `/api/v1/projects/{project}/decisions/prune` | Y | Prune old performance decisions |
| `GET` | `/api/v1/audit` | Y | List audit events |
| `POST` | `/api/v1/keys` | Y | Create an API key |
| `GET` | `/api/v1/keys` | Y | List API keys |
| `DELETE` | `/api/v1/keys/{id}` | Y | Revoke an API key |
| `DELETE` | `/api/v1/admin/cleanup` | Y | Run artifact cleanup |
| `POST` | `/api/v1/fleet/dependency-event` | Y | Record dependency change events |
| `GET` | `/api/v1/fleet/alerts` | Y | List fleet-wide alerts |
| `GET` | `/api/v1/fleet/dependency/{dep}/impact` | Y | Query dependency impact |
## Authentication
Pass an API key as a Bearer token (`Authorization: Bearer pg_live_<32-char-random>`).
Keys are scoped to a project and optionally restricted by benchmark regex:
```bash
--api-keys contributor:pg_live_abc123:my-project:^bench-.*$
```
For GitHub Actions CI, use OIDC (`--github-oidc org/repo:project-id:contributor`).
GitLab OIDC and custom OIDC providers are also supported. JWT tokens (HS256)
are supported via `--jwt-secret`.
You can also load API-key policy documents from exactly one external source:
`--api-keys-env`, `--api-keys-file`, or `--api-keys-command`. These documents
may be JSON/TOML arrays or wrapped under `keys` / `api_keys`.
`--api-keys-command` runs through PowerShell on Windows and `sh` elsewhere.
Loaded policy `id`, `role`, `project`, optional `benchmark_regex`, and optional
`expires_at` are preserved by the runtime auth path.
## Configuration
| `--bind` | `0.0.0.0` | Bind address |
| `--port` | `8080` | Port |
| `--storage-type` | `memory` | `memory`, `sqlite`, or `postgres` |
| `--database-url` | -- | DB path (SQLite) or connection string (Postgres) |
| `--pg-max-connections` | `10` | Maximum PostgreSQL pool connections |
| `--pg-min-connections` | `2` | Minimum idle PostgreSQL pool connections |
| `--pg-idle-timeout` | `300` | Idle connection timeout in seconds |
| `--pg-max-lifetime` | `1800` | Maximum connection lifetime in seconds |
| `--pg-acquire-timeout` | `5` | Timeout for acquiring a pooled connection in seconds |
| `--pg-statement-timeout` | `30` | PostgreSQL statement timeout set on new connections in seconds |
| `--api-keys` | -- | `role:key[:project[:benchmark_regex]]` (repeatable) |
| `--api-keys-env` | -- | env var containing one API-key policy document |
| `--api-keys-file` | -- | file containing one API-key policy document |
| `--api-keys-command` | -- | command that prints one API-key policy document |
| `--github-oidc` | -- | `org/repo:project_id:role` (repeatable) |
| `--gitlab-oidc` | -- | `group/project:project_id:role` (repeatable) |
| `--oidc-provider` | -- | custom OIDC issuer/JWKS/audience mapping |
| `--jwt-secret` | -- | HS256 secret for JWT auth |
| `--no-cors` | `false` | Disable CORS |
| `--timeout` | `30` | Request timeout (seconds) |
| `--log-level` | `info` | `trace`, `debug`, `info`, `warn`, `error` |
| `--log-format` | `json` | `json` or `pretty` |
| `--retention-days` | `0` | Artifact retention period; `0` disables background cleanup |
| `--cleanup-interval-hours` | `1` | Interval between background artifact cleanup passes |
## Storage backends
| **memory** | Tests / short-lived demos | None | Zero config |
| **sqlite** | Single-node production | Disk | `--database-url ./perfgate.db` |
| **postgres** | Multi-node / HA | Disk | `--database-url postgresql://host/db` |
SQLite file databases are opened with `journal_mode=WAL` and a 5 second
`busy_timeout` on every server-managed connection. This keeps readers from
blocking writers in normal single-node deployments. In-memory SQLite databases
skip WAL because SQLite reports `journal_mode=memory` for those connections.
PostgreSQL storage uses a sqlx connection pool. The pool pings connections
before reuse, applies `statement_timeout` on new connections, retries transient
connection failures, and exposes pool occupancy from `/health`. If storage is
unhealthy, `/health` includes a coarse non-secret `storage.detail` code while
the full storage error stays in server logs:
```bash
perfgate-server \
--storage-type postgres \
--database-url postgresql://perfgate:secret@db.example.com/perfgate \
--pg-max-connections 20 \
--pg-min-connections 4 \
--pg-acquire-timeout 10 \
--pg-statement-timeout 30
```
For artifact object storage, embedded deployments can set
`ServerConfig::artifacts_url` to an `object_store` URL such as `s3://...`,
`gs://...`, `az://...`, or `file://...`. The server binary exposes retention
cadence flags, but cleanup only runs when an artifact store is configured. When
using S3, GCS, Azure, or another managed object store, configure provider-side
lifecycle policies as the durable retention backstop and use perfgate cleanup
as application-level hygiene.
## Metrics
`/metrics` exposes Prometheus text output for request volume/latency and the
server operations that matter when the service becomes CI-critical:
```text
perfgate_server_requests_total
perfgate_server_request_duration_seconds
perfgate_baselines_total
perfgate_verdicts_total
perfgate_upload_failures_total
perfgate_auth_failures_total
perfgate_storage_errors_total
```
## Library usage
```rust
use perfgate_server::{ServerConfig, StorageBackend, run_server};
#[tokio::main]
async fn main() {
let config = ServerConfig::new()
.bind("0.0.0.0:8080").unwrap()
.storage_backend(StorageBackend::Sqlite)
.sqlite_path("perfgate.db");
run_server(config).await.unwrap();
}
```
See also: [Getting Started with Baseline Server](../../docs/GETTING_STARTED_BASELINE_SERVER.md)
## License
MIT OR Apache-2.0