API Stability & Deprecation Policy
This document is the compatibility contract for BRIDGEPORT’s HTTP API. It tells integrators what they can rely on, how breaking changes are versioned, how long a deprecated surface stays alive, and how to discover what a running instance speaks. Read it before you build anything against the API.
This policy covers the HTTP API — every endpoint under /api, plus the unauthenticated GET /openapi.json and GET /health endpoints. For these surfaces, the guarantees below apply.
Covered:
- Request and response shapes (field names, types, nesting).
- HTTP status codes and the standardized error envelope (
code,message,field,hint,requestId). - The stable
codeenum on error responses (VALIDATION_ERROR,READONLY_FIELD,UNAUTHORIZED,FORBIDDEN_SCOPE,FORBIDDEN_ROLE,NOT_FOUND,CONFLICT,IDEMPOTENCY_KEY_REUSED,RATE_LIMITED,INTERNAL). - Authentication mechanics (
Authorization: Bearer, the SSE?token=query param, scope enforcement rules). - The machine-readable contract published at
GET /openapi.json.
Not covered (these may change at any time, in any release, without a deprecation window):
- The SQLite / Prisma schema and its migrations — an internal implementation detail. Never read the database directly; everything is exposed through the API.
- The monitoring agent wire protocol between the agent and the backend.
- Internal environment variables and configuration flags (covered separately by the Configuration Reference, which has its own compatibility notes).
- The web UI — markup, routes, component behavior, and bundled assets are not an API. Build against the documented endpoints, not the pages.
Versioning & semver
Section titled “Versioning & semver”BRIDGEPORT releases follow Semantic Versioning (MAJOR.MINOR.PATCH), and the wire format of the HTTP API is the thing semver protects:
- MAJOR (e.g.
1.x→2.0) — may contain breaking wire-format changes. The2.0release, for example, redesigned the service model (see Service → ServiceDeployment below) and was a deliberate, clean break. - MINOR (e.g.
2.0→2.1) — additive only. New endpoints, new optional fields, new error codes. Existing integrations keep working untouched. - PATCH (e.g.
2.0.1→2.0.2) — bug fixes and security fixes only. No new surface, no removed surface.
Breaking vs non-breaking changes
Section titled “Breaking vs non-breaking changes”A change is breaking if a previously-valid client could observe a different, incompatible result. Breaking changes ship only in a major release.
| Change | Breaking? | Allowed in |
|---|---|---|
| Removing or renaming a field | Breaking | Major only |
Changing a field’s type (e.g. string → object) | Breaking | Major only |
| Changing an endpoint’s HTTP status code | Breaking | Major only |
| Adding or tightening validation that rejects previously-accepted input | Breaking | Major only |
| Removing an endpoint | Breaking | Major only |
| Removing an enum value from a field | Breaking | Major only |
| Adding a new optional field to a response | Non-breaking | Minor / patch |
| Adding a new endpoint | Non-breaking | Minor / patch |
| Adding a new enum value to a field documented as open/extensible | Non-breaking | Minor / patch |
Adding a new error code | Non-breaking | Minor / patch |
Clients must be tolerant readers: ignore unknown fields rather than rejecting them, and branch on the stable code enum — never on message substrings or, for error handling, on HTTP status alone. A response gaining a new optional field, or an error gaining a new code, is explicitly non-breaking and can happen in any minor release.
The canonical contract
Section titled “The canonical contract”The single source of truth for the wire format is the OpenAPI 3.0.3 specification, generated at runtime via @fastify/swagger:
| What | Path | Auth |
|---|---|---|
| Raw OpenAPI 3 spec (JSON) | GET /openapi.json | No |
| Swagger UI (interactive) | GET /api/docs | No |
| Per-release snapshot | openapi.json asset on each GitHub Release | No |
Both endpoints are unauthenticated so CI tools, code generators, and reverse-proxy probes can pull the spec without minting a token. info.version in the spec is sourced from the build’s APP_VERSION. Every tagged release also attaches the spec as an openapi.json asset, so you can diff the contract between versions without a running instance — this is what lets downstream consumers (e.g. the terraform-provider-bridgeport spec-diff job) flag new or changed operations.
Pin the snapshot. For reproducible integrations, fetch /openapi.json from the version you build against (or download the openapi.json asset from its release) and commit it to your repo. Diff it against a later instance’s spec to detect drift before upgrading:
# Snapshot the contract you build againstcurl -s https://deploy.example.com/openapi.json > openapi.pinned.json
# Later, before upgrading, diff against the new instancecurl -s https://deploy.example.com/openapi.json > openapi.new.jsondiff <(jq -S . openapi.pinned.json) <(jq -S . openapi.new.json)Where the spec models a deprecated field or parameter, it carries deprecated: true so generated clients and linters can surface it automatically. Spec coverage of deprecations is best-effort and still expanding, though — treat the Current deprecations table below as the authoritative, complete list rather than relying on a spec diff alone.
Provider compatibility CI
Section titled “Provider compatibility CI”The OpenAPI snapshot lets a consumer diff the declared contract. It can’t tell you whether a change quietly altered runtime behavior the spec doesn’t model — a default that shifted, a validation rule that tightened, a response that reshaped in a way the schema still nominally allows. To catch those, we run the real terraform-provider-bridgeport acceptance suite against every API-affecting PR.
What enforces this
Section titled “What enforces this”The Provider Compatibility workflow (.github/workflows/provider-compat.yml):
- Builds the server image from the PR (same build args as the release/edge image).
- Checks out
terraform-provider-bridgeportat its latest release tag (resolved dynamically, not pinned). - Runs that provider’s acceptance suite (
TF_ACC=1) against a disposable instance of the PR-built image, via the provider’s ownscripts/acc-harness.sh— which spins up the container, waits for/health, mints an admin token, runs the Terraform acceptance tests, and tears the container down.
This is the runtime complement to the per-release openapi.json snapshot (see The canonical contract): the snapshot lets the provider repo run a spec-diff from its side; this job catches the behavioral breaks the spec diff can’t see. A green provider suite is a merge gate — a red suite fails the platform PR, and that is the enforcement behind this whole policy.
When it runs
Section titled “When it runs”| Trigger | Why |
|---|---|
Pull requests to master that touch API-affecting paths | The provider only talks to /api, so the job skips docs-, UI-, agent-, CLI-, and client-only PRs (paths-ignore). |
Nightly (schedule, 04:00 UTC) | Catches indirect drift no diff would flag on a given PR, and picks up newly-released provider versions. |
On demand (workflow_dispatch) | Takes an optional provider_ref input to test against an arbitrary ref — e.g. the provider’s master to validate an in-flight provider change against the current platform. |
Failure playbook
Section titled “Failure playbook”A red provider suite means one of two things, and they have very different responses:
- Accidental break (the common case). The change unintentionally altered a covered API surface (see Scope for what’s covered). Fix it here, in this PR, before merge — restore the compatible shape. The green suite is the gate; don’t merge around it.
- Intentional, policy-compliant break. The change is a deliberate breaking change shipping in a major release per Versioning & semver. It must not be force-merged past a red suite silently. It ships with: (a) a Deprecation entry where a window applies, (b) the required “API changes” release-notes entry (Changelog discipline), and (c) a provider-side follow-up issue filed before this PR merges, so
terraform-provider-bridgeportis updated and re-released against the new contract. Only then do you coordinate the merge.
| Symptom | Owner | Action |
|---|---|---|
| Red because the change accidentally altered a covered surface | Platform PR author | Restore the compatible shape in this PR; re-run until green. |
| Red because the change is an intended major break | Platform PR author + provider maintainer | Deprecate (where a window applies), document the “API changes” release-notes entry, file a provider follow-up issue before merge, then coordinate the merge + provider re-release. |
Deprecation policy
Section titled “Deprecation policy”We don’t break things without warning. When an API surface needs to go away, it is first deprecated, kept working for a guaranteed window, and only then removed.
Deprecation window
Section titled “Deprecation window”A surface deprecated in a given release is supported through the remainder of that major series and is removed no earlier than the next major release.
Concretely: anything deprecated anywhere in 2.x keeps working for all of 2.x and is only eligible for removal in 3.0. This gives integrators the full lifetime of a major series to migrate.
How deprecations are signaled
Section titled “How deprecations are signaled”Every deprecation is recorded in the two authoritative places below, and — where the spec models the affected field — surfaced in a third, machine-readable one:
- The Current deprecations table in this document — the complete, always-current list with replacements and removal targets. This is the canonical record.
- An “API changes → Deprecated” entry in the release notes — tells you when a surface was deprecated. See Changelog discipline below.
deprecated: truein/openapi.json— machine-readable and picked up by code generators and OpenAPI linters, applied where the spec models the deprecated field. Coverage is best-effort and still expanding, so don’t rely on a spec diff alone to catch every deprecation.
If you’re integrating, the table above is your source of truth; the release notes tell you when the clock started, and the spec annotations help your tooling flag the cases it already covers.
Changelog discipline
Section titled “Changelog discipline”Per-release notes live in the annotated git tag message for each release (rendered on the corresponding GitHub Release page), not in a flat CHANGELOG.md. Every release’s notes carry an explicit:
## API changes
### Added- New `GET /api/...` endpoint for ...
### Deprecated- `<field>` on `<endpoint>` — use `<replacement>` instead. Removal target: <major>.
### Removed- `<surface>` (deprecated in <version>) is gone — migrate to `<replacement>`.This section is required in every release’s notes and says None when there are no API-surface changes. It is the authoritative, dated record of additions, deprecations, and removals — distinct from feature/fix descriptions, which describe behavior, not contract.
Version discovery
Section titled “Version discovery”A running instance exposes a small amount of build metadata through the unauthenticated health endpoint:
curl -s https://deploy.example.com/health{ "status": "ok", "timestamp": "2026-06-08T12:00:00.000Z", "version": "2026060812-a1b2c3d", "bundledAgentVersion": "2026053114-9f8e7d6", "cliVersion": "2026052010-1a2b3c4"}version— the build stamp of the running backend, in the formatYYYYMMDDHH-{7-char SHA}. Use it to identify exactly which build is deployed and to correlate with logs/Sentry releases.bundledAgentVersion/cliVersion— the agent and CLI binaries bundled with this build.cliVersionis the build served byGET /api/downloads/cli;bundledAgentVersionis the agent image this instance deploys to your servers.
version is a build identifier, not a semantic version. The MAJOR.MINOR.PATCH semver lives only in the git release tag (e.g. v2.0.2); the server does not currently expose its semver or a runtime capability list. So you cannot read the semver from /health today.
For the wire contract, do not try to derive behavior from the build stamp — pin against the /openapi.json snapshot (The canonical contract) instead, and map the build stamp to a release by checking which tag it was cut from.
Roadmap (not a present capability): exposing the semantic version and a machine-readable capability list at runtime — to enable true client-side version/capability negotiation — is future work. Treat any such negotiation as not yet available; rely on the pinned spec for now.
Current deprecations
Section titled “Current deprecations”The following surfaces are deprecated but still present. Per the deprecation window above, they remain available throughout 2.x and are scheduled for removal in 3.0.
| Surface | Deprecated in | Replacement | Removal target |
|---|---|---|---|
services[] flattened shape + service.server accessor | 2.0 | Read service.serviceDeployments[] | 3.0 (next major) |
Service → ServiceDeployment split
Section titled “Service → ServiceDeployment split”In 2.0 the service model was split: Service became a template (image, env, health, compose), and per-server runtime (container name, status, discovery, ports) moved to ServiceDeployment. For back-compat, responses still include a flattened services[] array (one row per deployment) and a service.server-style accessor that resolves to the first deployment’s server. New integrations should read service.serviceDeployments[] rather than the flattened shape.
Removed in 3.0
Section titled “Removed in 3.0”The following surfaces were deprecated in 2.x and have now been removed. Integrations must migrate before upgrading to 3.0.
| Surface | Deprecated in | Removed in | Migration |
|---|---|---|---|
Sync response top-level success: boolean | 2.0 (#127) | 3.0 (#235) | Read status instead |
Sync response success alias (removed)
Section titled “Sync response success alias (removed)”The sync endpoints (POST /config-files/:id/sync-all, POST /services/:id/sync-files, POST /servers/:serverId/sync-all-files) used to return a top-level success: boolean alongside status. That alias was removed in 3.0; read status === 'ok' instead. Note that status carries information the boolean couldn’t: a zero-target sync returns 200 with status: 'no_targets', which you should surface as a warning, not as a green success — it is distinct from 'ok'. The envelope is now:
{ "status": "partial", "targetsAttempted": 3, "targetsSucceeded": 2, "targetsFailed": 1, "results": [ ... ]}Reporting issues & questions
Section titled “Reporting issues & questions”- Found a breaking change that wasn’t announced? That’s a bug — open an issue on the GitHub issue tracker with the
versionfrom/healthand arequestIdfrom the affected response. - Need a deprecation window extended, or unsure how to migrate? Open an issue describing your integration; we’d rather hear about it before a major release than after.
- Security-sensitive API behavior? Follow the Security Policy — do not open a public issue.
Related Docs
Section titled “Related Docs”- API Reference — Authentication, endpoints, and the error envelope in detail
- Configuration Reference — Environment variables and their compatibility notes
- Upgrades — How upgrades work, rollback, and agent updates
- Security Policy — Supported versions and vulnerability reporting