Skip to content

Changelog

v3.0.2 Latest

What’s new

A drop-in patch — no migrations, no config changes. It fixes over-aggressive rate limiting that could surface a normal dashboard load as a 500, makes the frontend self-heal stale chunks after an upgrade, refreshes the app icons, and — the headline — launches the public documentation site at bridgeport.bridgein.com.


Fixes

Rate limiting no longer trips on normal page loads (#312)

The global limiter (100 req/min per IP) was applied to every route, including static-asset serving and the SPA shell — so a single dashboard load (HTML + a dozen hashed JS/CSS chunks + 30s polling) could exhaust the budget for one legitimate user. The rejection on the static route was then mis-classified as a 500 and captured to error monitoring.

  • Static/SPA GET/HEAD requests are now exempt from the limiter; /api/* and /mcp stay throttled — the limiter protects the programmatic surface, not local file serving.
  • Genuine rate-limit responses now return a correct 429 Too Many Requests with Retry-After instead of a 500, and no longer spam error monitoring.

If you front BridgePort with a reverse proxy or CDN, that’s the right layer to DoS-protect static serving.

Frontend self-heals stale chunks after a deploy (#312)

A vite:preloadError handler now does a one-shot, cooldown-guarded reload when a lazy-loaded route chunk fails to fetch — typically right after an upgrade swaps the hashed assets. “Failed to fetch dynamically imported module” errors recover transparently instead of dead-ending the page.


Improvements

Refreshed app icons (#310)

New favicon and PWA/app-tile icons in the brand aesthetic — the red crane mark on dark #0a0e14 tiles, with a transparent favicon that adapts to light and dark browser tab strips.


Security

  • CVE-2026-12143 / GHSA-hmw2-7cc7-3qxx — CRLF injection in form-data (< 4.0.6), high severity, pinned to ≥ 4.0.6. This dependency lives only in the docs-site build toolchain (website/) and never reaches the app runtime or the Docker image, so the running app was never exposed — closed for hygiene. (#309)

Documentation

Public documentation site launched (#307, #308, #311, #313)

BridgePort now has a full docs site at bridgeport.bridgein.com, built with Astro Starlight from the repo’s docs/ — a single source, so the site and GitHub never drift. Highlights:

  • Full-text search, dark/light, branded to match the app UI
  • API reference auto-generated from the OpenAPI spec
  • Architecture & flow diagrams rendered from Mermaid
  • A changelog sourced from these GitHub Releases (so this release will appear there automatically)
  • Real product screenshots (dashboard, monitoring, services)
  • An llms.txt endpoint so AI agents can consume the docs
  • Auto-deploys via Cloudflare on every docs change and on each published release

API changes

None. (Rate-limit responses now return 429 instead of an erroneous 500 — a fix, not a surface change; see Fixes.) -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgEyiv4hf6iBgr34ICjN6HnEP/vs Yr31eNU5HhdkQaYd4AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQLoiwVIhyZuzlKhDAlmko0bhVpoKrbuRyBug03RBdL0BFftf7YkHAREPYq7n9qD4Hh r2jduikwYktaNiQCaXrQI= -----END SSH SIGNATURE-----

v3.0.1

What’s new

BridgePort v3.0.1 is a reliability patch. Under sustained concurrent write load, a freshly-booted instance could intermittently return transient 500s (typically on GET /api/auth/me) whenever a second writer held the SQLite write lock. Those statements are now automatically retried, and genuinely unresolvable contention returns a retryable 503 instead of an opaque 500. No database migrations — upgrade is drop-in.


Fixes

Transient 500s under sustained write contention (#299)

BridgePort serves requests through a single synchronous better-sqlite3 connection, so it never self-contends — but when a second writer held the write lock (a long external transaction, a WAL checkpoint, or a test harness resetting state), two SQLite modes surfaced as opaque 500s:

  • SQLITE_BUSY — lock held past busy_timeout, mapped to Prisma P1008.
  • SQLITE_BUSY_SNAPSHOT — a stale read snapshot upgrading to a write; returned immediately, so busy_timeout could never absorb it.

The fix:

  • Automatic retry — a Prisma client extension retries transient contention (P1008/P1017/P2024/P2034, SQLITE_BUSY/locked) with jittered exponential backoff. Retry is per-statement and verified safe — no double-writes, including inside an interactive $transaction.
  • Retryable 503, not opaque 500 — when retries are exhausted, the request returns 503 Service Unavailable with a Retry-After header, logged at warn and kept out of Sentry.

Measured with a concurrent CRUD + /api/auth/me repro under a bursting external write lock: 24 → 0 500s.


API changes

No endpoints or fields were added, changed, or removed. One behavior change: transient database-contention errors now return 503 (with Retry-After) instead of 500. Monitoring or alerting that treats all 5xx as outages should treat these 503s as retryable.


Configuration

  • New DB_RETRY_* knobs tune retry attempts and backoff — sensible defaults, no action required.
  • SQLITE_BUSY_TIMEOUT_MS default lowered 50001000. better-sqlite3’s busy-wait is synchronous and blocks the event loop, so the per-attempt wait is kept short while the async retry loop (which frees the loop between attempts) carries longer contention.

See Configuration.


Database migrations

None — this release makes no schema changes.


Ecosystem

Alongside the 3.x line, BridgePort now has first-class infrastructure-as-code tooling (versioned independently of the platform):

  • Terraform / OpenTofu provider — manage environments, servers, config, secrets, registries, images, and services declaratively. Live on both the Terraform and OpenTofu registries; source at terraform-provider-bridgeport.
  • Go SDKgithub.com/bridgeinpt/bridgeport/client, a typed read + write client for the HTTP API.

Upgrade

Drop-in — pull and restart, no migrations or config changes required.

Terminal window
docker pull ghcr.io/bridgeinpt/bridgeport:v3.0.1

-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgEyiv4hf6iBgr34ICjN6HnEP/vs Yr31eNU5HhdkQaYd4AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQAQzL5g91VpEHH4vS6L7IftJp7NJQnkfYUd7BLVWvTbALcCIjoqnf65ol/jEO0QbKz HDrnnGVhKOtcKUU3Y4GgY= -----END SSH SIGNATURE-----

v3.0.0

What’s new

BridgePort 3.0.0 is a major release. The web UI is rebuilt on shadcn/ui with a new Deep Slate dark theme and an opt-in light theme; an opt-in MCP server lets you drive BridgePort from your own AI agent; database backups gain a Grandfather-Father-Son (GFS) tiered rotation policy; and a full configuration audit makes dozens of previously-hardcoded knobs tunable. It also completes the 2.x deprecation cycle by removing the sync success alias — the one breaking change (see below).

Migrations apply automatically on container start, and the GFS rollout is designed to prune nothing until you opt in. As always, pull the new image and restart.


Action required before upgrading

Skip this section unless you have external scripts or integrations calling the sync endpoints.

Sync responses no longer include success

The deprecated top-level success: boolean has been removed from the sync result envelope on all three sync endpoints. Read the canonical status field instead — and note it carries information the boolean never could: a zero-target sync returns 200 with status: "no_targets", which you should surface as a warning, not a green success.

if (res.success) { … }
if (res.status === "ok") { … } // "partial" / "no_targets" are distinct outcomes

Affected: POST /api/config-files/:id/sync-all, POST /api/services/:id/sync-files, POST /api/servers/:serverId/sync-all-files. The BridgePort UI and per-target results[].success are unaffected. This was deprecated in 2.0 and removed on schedule per the API Stability Policy. (#236)


Database migrations

Two migrations, applied automatically and safely on first start:

  • 20260613210010_settings_audit_240 — drops 11 per-environment monitoring columns the scheduler silently ignored (and an orphaned registry-token field), and backfills the new retention/timeout system settings on the singleton row. Your collect* toggles are preserved. (#241)
  • 20260624145904_add_backup_rotation_policy — adds the GFS rotation policy tables/columns. Crucially, it creates an inert policy snapshot for every database that already has backups, so the first post-upgrade sweep deletes nothing. GFS only begins thinning a database once an operator reviews and saves its policy. (#292)

Features

MCP server — drive BridgePort from your AI agent (#237)

An opt-in Model Context Protocol server at POST /mcp exposes a curated slice of the API as agent tools, so you can operate BridgePort from Claude Desktop, Cursor, Claude Code, and other MCP clients. No model or inference runs on the host — you bring your own agent.

  • Disabled by default — set MCP_ENABLED=true to register the route (fail-closed; MCP_ALLOWED_HOSTS opts into DNS-rebinding protection).
  • ~55 tools: a comprehensive read/observe surface plus a safe-operate set (deploy_service, restart_deployment, rollback_deployment_plan, run_database_backup, …). Every call replays a real internal request, so auth, role/scope enforcement, validation, idempotency, and audit logging behave identically to the REST API.
  • Secrets never leave: a recursive redactor strips every secret-named field from all output; env-scoped tokens only see the tools they can actually call.
  • New admin page at /admin/mcp shows live status, the exposed tool/resource inventory, and client setup guidance.

GFS tiered backup rotation & retention (#292)

Replaces the flat retentionDays model with a Grandfather-Father-Son policy: keep the keepLast most-recent backups plus daily / weekly / monthly / yearly tiers (a backup survives if any tier selects it), with a minFloor safety guard and an optional per-database maxTotalBytes cap.

  • Presets (lean / balanced / long_term / custom) with inheritance: a global default in System Settings → inherited per database → overridable per database.
  • Pinning & exemptions: manual backups are never auto-pruned, and any backup can be pinned.
  • Safe pruning: file-first deletion (artifact → row), fully audit-logged, with a confirmation gate when a policy change would prune more than the configured threshold.
  • New instance timezone setting (ISO-week bucketing) and two new notification types (backup.rotation_error, backup.policy_first_prune).

UI rebuilt on shadcn/ui + Deep Slate and light themes (#272)

The bespoke component layer is replaced with shadcn/ui (Radix + Tailwind v4 + CSS-variable theming). Ships the Deep Slate dark theme and an opt-in light theme with a user-menu switcher (system default, no flash-of-unstyled-content).

  • Semantic design tokens throughout (sky primary, burgundy brand, success/warning/info, themed charts), a ⌘K command palette, collapsible sidebar with a mobile sheet, and react-hook-form + zod forms across editors and modals.
  • Every page rewritten; charts, topology, and the code editor re-themed; legacy components and color ramps removed.
  • Resolved a backlog of UI-audit findings and rendering bugs along the way.

Configuration audit & deploy-time tunables (#241, #257)

A full pass over the configuration surface — making existing behavior controllable and honest, with no new product behavior by default.

  • New admin-editable retention settings (notifications, health logs, webhook deliveries, image digests), read hot by the scheduler each cleanup tick, consolidated into one “Retention” section in System Settings.
  • Exposed previously-uncontrollable knobs (pgDumpTimeoutMs, multipart upload limit) and removed settings the app silently ignored.
  • 21 new deploy-time environment variables (MySQL/webhook timeouts, response-cache size, SSH exec buffer, and more), each defaulting to today’s value — zero behavior change unless set.

API changes

See the API Stability Policy.

Added

  • POST /mcp — opt-in MCP transport (off unless MCP_ENABLED=true). (#237)
  • Backup-policy endpoints: PUT/GET …/backup-policy, POST …/backup-policy/preview (viewer-allowed), and idempotent PUT …/backups/:backupId/pin. (#292)
  • Typed querystring/params schemas added to the remaining untyped routes (incl. audit, events, downloads) — the OpenAPI spec now documents ~42 more query parameters. No behavior change. (#273)
  • Each GitHub Release now ships the OpenAPI spec as an openapi.json asset, so consumers can diff the contract between versions without a running instance. (#294)

Removed

  • Sync response top-level success: boolean (deprecated in 2.0) — read status instead. See Action required above. (#236)
  • 11 dead per-environment MonitoringSettings fields (monitoring interval/retention/bounce + a no-op enabled) and SystemSettings.doRegistryToken — monitoring cadence is global (SCHEDULER_*) and bounce thresholds live on notification types. (#241)

Improvements

Frontend bundle split into route + vendor chunks (#281)

The single ~2.3 MB JS bundle is now ~80 lazy-loaded route and vendor chunks (charts, flow, CodeMirror, Radix, Sentry, React), so pages load on demand and vendor code is long-cacheable — resolved by real code-splitting, not by raising the warning limit.

New port-gantry-crane logo & brand loader (#282)

An SVG-native, monochrome-burgundy port gantry crane mark (a crane carrying a container — orchestration = the port), deliberately name-independent. Doubles as the app’s loading animation (honors prefers-reduced-motion), with a full favicon/PWA icon set.


Fixes

Operator/viewer access to the Secrets & Vars page (#275)

Operators saw an empty “No secrets configured” page because one admin-only fetch in a Promise.all rejected the whole batch (403). The admin-only call is now skipped for non-admins, and viewers no longer see write controls that would only 403 on submit.

Dashboard render loop (“Maximum update depth exceeded”) (#276)

useToast() returned a fresh object every render, which cascaded into an infinite render loop in the topology diagram (surfaced after the UI migration). The toast handle is now a referentially stable module-level singleton, with a regression test.

Duplicate logo icon in the sidebar header (#279)


Security

All resolved via dependency updates:

  • form-data → 4.0.6 — CRLF injection (Dependabot #80). (#283)
  • @opentelemetry/core → 2.8.0 — unbounded memory allocation (Dependabot #81). (#284)
  • nodemailer → 9.0.1 — security-group update. (#285)
  • esbuild ≥ 0.28.1 — GHSA-g7r4-m6w7-qqqr (dev-server only; not exploitable in production builds). (#238)

Under the hood

  • Quieter CI builds (#278): skip the ssh2/cpu-features native addon build that can’t compile against Node 26’s V8 (runtime was always on the pure-JS path) and bump pnpm to 11.7.0.
  • Dependency bumps: @fastify/swagger-ui 6 (#289), @fastify/rate-limit 11 (#288), fastify-plugin 6 (#290), actions/checkout 7 (#286), and the js-minor-and-patch group (#280).

Documentation

  • MCP server woven into the cross-cutting docs for the public release (#258), and the branding doc rewritten around the finalized logo system (#282). -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgEyiv4hf6iBgr34ICjN6HnEP/vs Yr31eNU5HhdkQaYd4AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQNGQNwQiZVvgQjvkoy6DGLzn3g4fXzhVC/N0uR2QyXZxf0T4Stkq6fKzgcXuMx1vAq Xvu5OSu7ZYix34K4+t2ws= -----END SSH SIGNATURE-----

v2.3.0

What’s new

BridgePort 2.3.0 adds outbound webhook subscriptions with HMAC-signed, retried delivery and Idempotency-Key support for safe POST retries, ships a typed OpenAPI spec (/openapi.json) generated from the real request schemas, and surfaces service-type/language badges and deployment servers in the UI. One database migration applies automatically on container start — no manual steps.

One behavior change to know about if you use multi-channel Slack routing and per-environment channel overrides together — see below.


Behavior changes

Per-environment Slack overrides now demultiplex fan-outs

Skip this if you don’t set per-environment Slack channel overrides.

Previously, a per-environment Slack channel override only acted as a fallback for unrouted notification types. A type routed to multiple channels globally fired into all of them from every environment, so you couldn’t mute one environment’s noise without losing another’s.

Now an override collapses a fan-out to that environment’s own channel:

  • Exactly one matched route → sent there; the override is not consulted (env-agnostic single routes still reach every environment).
  • More than one match (fan-out) → if the originating environment has a usable override, send only to that environment’s channel; otherwise the full fan-out is preserved.
  • No match → environment override as fallback, else the global default (unchanged).

Impact: any environment that has both a multi-channel routing and a per-environment override set will now receive only its override channel instead of the full fan-out. Single-routed types and unrouted-fallback behavior are unchanged. To split a shared alert per environment: route the type to >1 channel globally, then set each environment’s override to its own channel. (#221)


Database migrations

One migration, applied automatically on container start (BridgePort’s golden rule — zero human intervention).

  • 20260611215341_webhooks_idempotency — adds three tables: WebhookSubscription (env-scoped endpoints, encrypted signing secret), WebhookDelivery (per-attempt delivery records with retry state), and IdempotencyKey (24h dedupe store for mutating POSTs). Additive only — no changes to existing tables. (#234)

Features

Webhook subscriptions (#234)

Environment-scoped outbound webhooks let external systems react to BridgePort events in near-real-time, as an alternative to the existing admin-scoped notification fan-out.

  • Manage subscriptions under /api/environments/:envId/webhooks (create / list / get / delete), with delivery history at GET …/:id/deliveries.
  • Deliveries are HMAC-signed (X-BridgePort-Signature: sha256=…), sent in the background, and retried with exponential backoff (5 attempts).
  • Terminal events fire within seconds: deployment.completed/failed, plan.completed/failed, backup.completed/failed, sync.completed.
  • Signing secrets are encrypted at rest (AES-256-GCM) and never returned by the API (hasSecret boolean only).
  • This is a separate system from the admin-scoped WebhookConfig notifications (X-Webhook-Signature) — different contracts, kept side by side rather than retrofitted.

Idempotency-Key for safe POST retries (#234)

Mutating POSTs now honor a client-supplied Idempotency-Key header. A global hook dedupes against the key for a 24-hour window and replays the original response, so a client can safely retry a POST whose outcome is unknown.

  • Same key, mismatched body → 409 IDEMPOTENCY_KEY_REUSED.
  • Same key, request still in progress → 409.

Typed OpenAPI spec (#227)

/openapi.json and Swagger UI now carry real request contracts, generated from the existing Zod validation schemas — one source of truth, no doc/validation drift.

  • A checked-in openapi.json snapshot plus a CI drift check that fails if routes change without regenerating the spec.
  • Documentation-only (uses Zod 4’s native z.toJSONSchema(), no new runtime deps); runtime validation and the custom error envelope are unchanged.

Service-type & language badges, deployment servers (#223)

  • Service-type badge on each service card (promoted from inline text; keeps the Generic fallback).
  • Config-file language badge (e.g. yaml, nginx) on text config files, alongside the binary badge.
  • Servers on service detail — a linked list of deployment servers above Deployment History, plus a new linked Server column in the history table ( for legacy rows). No schema change.

API changes

Added

  • POST /api/environments/:envId/webhooks — create a webhook subscription. (#234)
  • GET /api/environments/:envId/webhooks — list subscriptions. (#234)
  • GET /api/environments/:envId/webhooks/:id — get a subscription. (#234)
  • DELETE /api/environments/:envId/webhooks/:id — delete a subscription. (#234)
  • GET /api/environments/:envId/webhooks/:id/deliveries — delivery history. (#234)
  • Idempotency-Key request header honored on mutating POSTs. (#234)
  • serviceDeployment.server field on GET /api/services/:id/deployments-history responses (null for legacy rows). (#223)
  • Typed request/error contracts now present throughout /openapi.json. (#227)

Deprecated

  • The sync result envelope’s legacy success alias is flagged deprecated: true in the OpenAPI spec — prefer the canonical field. (#227)

Removed

  • None.

Security

  • GHSA-w5hq-g745-h8pq (uuid) — missing buffer bounds check in v3/v5/v6. Pinned to ^11.1.1 via pnpm override. Transitive and dev/tooling-only (hyperidautocannon, stress tests); does not reach the production runtime or Docker image, and the vulnerable code path was never called. (#231)
  • GHSA-92pp-h63x-v22m (@hono/node-server)serveStatic middleware bypass via repeated slashes. Pinned to ^1.19.13. Transitive and dev-only (@prisma/devprisma); not in the production runtime. (#231)

Under the hood

Toolchain migrated from npm to pnpm (#226)

The entire JS/TS toolchain now runs on a single pnpm 11 workspace (root backend + ui/), resolved by one root pnpm-lock.yaml. Supply-chain controls move to pnpm’s native allowBuilds allowlist and minimumReleaseAge cooldown; CI, the multi-stage Docker build, and Dependabot were all converted. Dev keeps full strict phantom-dependency protection. Contributors: use pnpm install (see updated docs/development/).

UI dependency upgrades

  • React 19 + recharts 3react/react-dom 18.3 → 19.2.7 and recharts 2.15 → 3.8.1 (client-side only; backend/agent/CLI untouched). (#220)
  • Tailwind CSS 4 — 3.4 → 4.3 (Oxide engine, CSS-first @theme config, Vite plugin replaces the PostCSS pipeline). Like-for-like, no design changes. (#233)

Other notable bumps

  • golang.org/x/term (CLI) — 0.43.0 → 0.44.0 (#228)
  • @types/testing-library__jest-dom (dev) — 5.14.9 → 6.0.0 (#229)
  • /build skill fix — runs the two scoped vitest configs instead of bare npm test (which produced ~759 false failures in this repo). (#224) -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgEyiv4hf6iBgr34ICjN6HnEP/vs Yr31eNU5HhdkQaYd4AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQDDO04K4v3ckb7ElqF+Sfamr5V97DJ1h3Y37atrVlIRmgcKAmANljMESORjqwD+/f9 fINOxMjM6nyBRviMjfeg0= -----END SSH SIGNATURE-----

v2.2.0

What’s new

This release adds drift detection — read-only endpoints that diff BridgePort’s stored view of a service, server, or environment against what’s actually running on the host — and promotes the Go HTTP client to a standalone, importable module so external tooling can consume it directly. No database migrations; nothing to do on upgrade beyond pulling the new image.


Features

Drift detection endpoints (#218)

Three new read-only endpoints surface where BridgePort’s database has diverged from the live host — handy after an upgrade, an out-of-band change, or a post-incident sweep, without SSHing in and eyeballing each service by hand.

  • GET /api/services/:id/drift — per-deployment diff (keyed by server) of compose path & content, image digest, exposed ports, attached config-file content, and managed env vars.
  • GET /api/servers/:id/drift — every deployment on one server.
  • GET /api/environments/:envId/drift — environment-wide roll-up, per service and per server.
  • Each field reports match: boolean with expected/actual when it differs. Comparisons that can’t be resolved cleanly (e.g. digest-mode caveats, operator-maintained or shared compose files) return match: null with a reason rather than a false-positive mismatch.

Safe by construction: the host is only ever read (docker inspect + file reads, never mutating commands), the endpoints are viewer-accessible (no operator/admin gate), and no secrets leak — compose/config drift compares redacted-text checksums and env drift returns only BridgePort-managed key names, never decrypted values.

Importable Go client module (#217)

The typed Go HTTP client (previously locked inside cli/internal/api) is now a standalone, versioned module at github.com/bridgeinpt/bridgeport/client, so other tooling — most immediately the planned Terraform provider — can import it instead of forking and hand-chasing API changes.

  • Self-contained module (stdlib + testify-for-tests only), so go get github.com/bridgeinpt/bridgeport/client@<tag> works for external consumers.
  • The CLI now consumes the extracted module; its long-broken module path is fixed (bridgeport-cligithub.com/bridgeinpt/bridgeport/cli). No CLI behavior change.
  • Client README.md (install, bearer auth, *APIError handling, worked example) and a CONTRIBUTING.md section documenting the client/vX.Y.Z multi-module tagging convention.

The client module is versioned independently of the platform — see the note below about its client/ tag.


API changes

Added

  • GET /api/services/:id/drift, GET /api/servers/:id/drift, GET /api/environments/:envId/drift — read-only drift reports, viewer-accessible. (#218)

No deprecations or removals.


Under the hood

  • Go toolchain (Docker base image)golang 1.26-alpine → 1.26.4-alpine (#219) -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgEyiv4hf6iBgr34ICjN6HnEP/vs Yr31eNU5HhdkQaYd4AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQKz9Rl99zkk+CwZFqzUaCeVgDT6GpinTALIbgBKU4AzU0tDv8OjoMPOJ1tP5umAPef StcKA/HFGqAnzuI91uBQw= -----END SSH SIGNATURE-----

v2.1.0

What’s new

This release hardens how BridgePort manages compose files and adds a more inspectable Fragments UI. The headline fix closes a stale-config trap where auto-managed compose could silently overwrite an operator-set composePath and then abort a deploy before touching the running container (observed on v2.0.1). Fragments gain a read-only view and a drill-down “Used by” list, and bcryptjs moves to v3. One migration applies automatically on container start.


Database migrations

One migration, applied automatically on container start — no manual steps.

  • add_auto_manage_compose — adds an autoManageCompose opt-in flag to each environment’s Operations settings, defaulting to off. Safe table-redefine that preserves all existing rows; existing deployments keep their current compose paths and behavior. (#213)

Features

Fragments: read-only view, drill-down usage, and visible includes (#215)

The Fragments page no longer forces you into the editor just to read a fragment, and “Used by” is now something you can actually follow.

  • Read-only view action — an eye icon opens a read-only modal (name, description, content, usage) with an Edit button to switch into the existing editor, mirroring the Config Files view→edit flow.
  • “Used by” is now a list, not a number — the count is a clickable expander that lazily loads the referencing config files and their attached services as links. Zero-usage rows render an unobtrusive ”—”. The fetch is cached per fragment, so re-expanding doesn’t refetch.
  • Included fragments shown in the Config File view — the read-only config-file modal now lists its included fragments in order; previously this was only visible while editing.

Pure frontend — no API or schema changes; it surfaces data the existing endpoints already returned.


API changes

None. No HTTP API surface changed this release. Note that the new API Stability Policy (below) now governs how future changes here are classified and communicated.


Fixes

Compose: operator-set paths are now sacred, generated compose is validated (#213)

BridgePort could overwrite a deployment’s composePath with its own generated path — even when an operator had pointed it at a hand-maintained compose file — and the generated file could key its service on service.name instead of the deployment’s containerName. When those disagreed, the deploy aborted with No such service: <containerName> before recreating the container, so the old container kept serving stale config while vars and synced files all looked correct. A “redeploy” appeared to succeed while changing nothing.

  • An operator-set composePath is never overwritten — it survives deploys, service re-creation, and Terraform re-imports.
  • Generated/template compose is validated before deploy — the top-level services: key must match the deployment’s containerName, otherwise the deploy is refused with an actionable error instead of aborting mid-rollout. The dry-run preview surfaces the same mismatch as a warning.
  • The default-compose generator now keys on containerName (was service.name), fixing the mismatch at the source.
  • Every composePath change is audit-logged with its source (generator / terraform-import), so this class of drift is attributable.

Behavior change for new deployments: auto-setting composePath is now opt-in per environment (autoManageCompose, default off) under Settings → Operations → Compose Files. Existing deployments are unaffected. For a brand-new deployment that uses a generated compose file with the opt-in off, BridgePort no longer persists composePath, so a later restart uses docker restart <container> rather than docker compose (redeploys are unaffected — the file is regenerated and compose up runs regardless). Enable autoManageCompose to restore compose-managed restarts for new deployments.

Slack: clearer deployment notifications (#210)

Slack deployment messages were missing the one thing you want at a glance — which image deployed — and were inconsistent between success and failure.

  • The deployed image reference is now shown on its own line.
  • The internal Type: system.deployment_success debug line was removed.
  • Failure reason is inline on failed deployments (truncated to 500 chars) — no click-through.
  • The View Service button now appears on both success and failure (previously failures only).

Under the hood

bcryptjs 2 → 3 (#211)

Bumped the auth password-hashing library from 2.4.3 → 3.0.3, a major version behind. Existing $2a$ hashes minted on v2 continue to authenticate under v3 (the bcrypt wire format is stable across the bump; v3 only changes the default prefix to $2b$ for new hashes) — verified by a dedicated backward-compatibility regression test. No call-site or login changes; no users are re-hashed or locked out. Dropped the now-redundant @types/bcryptjs stub since v3 bundles its own types.

Other notable bumps

  • @xyflow/react 12.10 → 12.11, react-router-dom 7.16 → 7.17, @sentry/* 10.55 → 10.56 (#206)
  • Backend dependency-group bumps: @aws-sdk/*, ioredis, mysql2, @types/node (#207)
  • CI: codecov/codecov-action 6 → 7 (#205)

Documentation

HTTP API stability & deprecation policy (#212)

Added docs/api-stability.md (linked from the README and CONTRIBUTING): what semver means for the HTTP API, what counts as breaking vs additive, the deprecation window, the per-release “API changes” discipline, and how clients should pin against /openapi.json and discover server version/capabilities. Existing 2.x deprecations are listed retroactively with their removal targets.

README & docs refresh (#204)

Redesigned README header (centered hero, badge rows, nav) and a docs refresh reflecting BridgePort’s growth into a multi-server, topology-aware Docker ops platform — still SSH-based, self-hosted, SQLite, no Kubernetes.

Code of Conduct (#209)

Added the Contributor Covenant v2.1.

v2.0.2

BridgePort v2.0.2 is a maintenance release with three targeted fixes: binary config files can now be replaced in place (and a data-loss bug when editing their metadata is closed), database/monitoring charts correctly render Postgres metrics and multi-database “Top Tables”, and the View Logs modal reliably opens at the newest entry. No database migrations — upgrade is a straight image swap.


Three related gaps on the Config Files page:

  • Binary files now have a replace mechanism. The edit modal previously said “Re-upload to replace” but offered no control, and Upload Asset only ever created new files (409 on a duplicate name). A new operator-only endpoint POST /api/config-files/:id/replace-asset swaps content/mimeType/fileSize in place — keeping service attachments, sync assignments, and history — and the edit modal’s binary branch now has a wired-up Replace file input. Old content is saved to FileHistory for rollback.
  • API errors now surface as toasts. Create/upload/edit handlers swallowed failures (e.g. a duplicate-name 409) silently, producing an unhandled rejection instead of feedback. Errors now show via toast.error.
  • Editing a binary file’s metadata no longer wipes its content. Binary content is stripped to '' in API responses; saving a description edit round-tripped that empty string back into the stored payload (data loss — the next sync would write an empty file). The UI now omits content for binary files on save, and the API rejects empty content on binary rows with a 400 backstop.

A cluster of database-monitoring bugs found while investigating empty metrics on live environments:

  • Empty Postgres charts / raw-byte Top Tables. node-postgres returns int8/bigint/numeric columns as strings to avoid precision loss, but several consumers checked typeof value === 'number' and dropped them. A shared coerceNumeric() is now applied both at collection and on the history read-path, so existing string data renders immediately and Top Tables shows human-readable sizes (3.8 MB).
  • Only one database’s Top Tables rendered. When databases collect a few milliseconds apart, history timestamps form a union and the UI read the rows snapshot at the global last index — dropping every database except whichever collected last. The read now scans back to each database’s own latest non-null snapshot.
  • Redis “No metrics data collected yet”. The monitoring path omitted the username that ping/backup already send, so AUTH failed on restricted-ACL managed Valkey/Redis and the socket dropped. Username is now passed through; password-only credentials are unaffected.
  • Cold-start empty states. A transient first-load failure (backend warming up) latched a misleading “No metrics available” with no retry. The initial load now retries with bounded exponential backoff, keeping skeletons up — fixing Servers, Services, and Databases uniformly.

The View Logs modal opened scrolled to the oldest entry instead of the most recent. Under React concurrent mode, the requestAnimationFrame scroll from #191 could run before the DOM commit, reading a stale height. The scroll is now driven from a useLayoutEffect keyed on the log data (guaranteed to run after commit, before paint), so first load pins to the newest entry and Load older preserves scroll position while walking backward.


Routine dependency maintenance — no functional or security-relevant changes:

  • Backend minor/patch group (9 updates): @aws-sdk/client-s3, @sentry/node, ioredis, mysql2, nodemailer, vitest, tsx, others (#196)
  • Frontend minor/patch group (7 updates): @sentry/react, date-fns, react-router-dom, zustand, vite, vitest (#195)
  • GitHub Actions version group: actions/download-artifact v7→v8, actions/github-script v8→v9 (#194) -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgEyiv4hf6iBgr34ICjN6HnEP/vs Yr31eNU5HhdkQaYd4AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQCl9F46hsFQJWnAWb49M6a1OLr4eQMrmAuaMa2At0m0PZ9wbCgfuJaEEDCogMGujlJ JEEpAHQqKfjZjpiB1dvQ0= -----END SSH SIGNATURE-----

v2.0.1

What’s new

BridgePort 2.0.1 is a maintenance release that hardens 2.0. It fixes a concurrent deploy race that was failing deploys and leaving ghost services behind, repairs multi-host monitoring charts, cold-start list pages, and the container logs modal, and closes a secret-disclosure security hole where read-only tokens could reveal secret values. It also resolves the confusing dual “type” fields from 2.0 by consolidating onto the admin-managed Service Type, and seeds eight new built-in service types.

Schema changes apply automatically on container start — the single migration in this release finishes the type consolidation by dropping the now-unused column, with no data loss and no manual steps.


Heads-up before upgrading

Skip this unless you used the free-form service Type field added in 2.0.

Service “type” consolidated onto the Service Type dropdown (#187)

2.0 shipped two overlapping “type” concepts — the admin-managed Service Type dropdown and a separate free-form Type text field (typeTag) — and only the free-form one powered the Services-list filters, which was confusing and broke type filtering for some services. 2.0.1 removes the free-form field and consolidates everything onto the Service Type dropdown, which now drives the filter chips, the “No type” chip, and the ?type= URL param.

  • The migration 20260529130549_remove_service_type_tag rebuilds the Service table to drop the typeTag column, preserving every other column — the only data lost is the removed free-form type strings.
  • To get filtering back: define types under /admin/service-types, then assign them in the Configure Service dialog.
  • Until a service has a Service Type assigned, it shows as “Generic” / “No type”.

Database migrations

One migration, applied automatically on container start — no manual steps.

  • 20260529130549_remove_service_type_tag — rebuilds the Service table to drop the now-unused typeTag column, copying every other column across (no data loss beyond the removed free-form type strings). Completes the Service Type consolidation above. (#187)

Fixes

Concurrent compose-up race produced failed deploys and ghost services (#186)

The most impactful fix in this release. Two services attached to the same docker-compose.yml with auto-update enabled could deploy concurrently, where one service’s depends_on cascaded into recreating the other’s container mid-deploy — failing both deploys and leaving an orphaned container that discovery then materialized as a ghost service.

  • --no-deps on single-service force-recreate (both v2 and v1 paths) stops a deploy from cascade-recreating dependencies owned by their own BridgePort service.
  • A new per-(server, composePath) in-process keyed mutex serializes deploys touching the same compose file on the same server.

Multi-host monitoring charts dropped all but one series (#185)

On the Services and Servers monitoring pages, the CPU/Memory/Network charts rendered only one entity when an environment spanned multiple hosts, even though the table below listed them all — downsampling was collapsing staggered-timestamp series to null. Each row now falls back to its own most recent sample within the bucket, so every series stays populated.

List pages flashed an empty state on cold start (#184)

The Services, Servers, Databases, Container Images, and Config Files pages could briefly show “no results” before the environment resolved. The paginated fetch now treats a not-yet-ready fetch as pending and renders a skeleton, and Layout gained a bounded retry with backoff so a transient cold-start failure can’t strand a page on a skeleton.

Logs modal opened scrolled to the oldest entry (#191)

The container logs modal opened at the top (oldest line), making recent logs look missing. It now pins to the bottom on open, matching docker logs, while paging back to older entries still preserves scroll position.


Security

  • Read-only tokens could reveal secret values (#190)GET /api/secrets/:id/value, which decrypts and returns secret plaintext, had no role check, so the global read-method exemption let any authenticated principal (including a viewer token) read decrypted secrets; confirmed against a live instance. Reveal is now admin-only, with the requireAdmin guard running before the secret lookup so no plaintext can leak for operators or viewers. The UI hides the reveal control for non-admins, and /api/auth/me now splits the all-roles secrets:read (key/metadata listing) from a new admin-only secrets:reveal scope. The allowSecretReveal toggle and neverReveal flag remain as additional gates.

Features

Eight new built-in service types (#188)

Ships ready-to-use service-type definitions on top of the existing django, nodejs, and generic, each with genuinely useful exec commands:

  • FastAPI — python shell, pip list, alembic upgrade/revision
  • Flask — flask shell/routes, db upgrade, python repl
  • Caddy — version, validate, reload, fmt, list-modules
  • Nginx — config test, reload, version
  • Celery — status, inspect active/scheduled/stats
  • Keycloak — version, show-config (read-only kc.sh)
  • Redis — redis-cli, ping, info, dbsize
  • PostgreSQL — psql, list databases, version

They appear automatically on the next deploy via syncPlugins(); admin-customized types are left untouched and only missing commands are added — no manual step, no migration.


Documentation

  • Read-only API access for live-instance debugging (#189) — documents how to mint a minimally-scoped viewer service-account token, set BRIDGEPORT_URL/BRIDGEPORT_TOKEN, and run example read-only curls, across .env.example, docs/operations/troubleshooting.md, and a CLAUDE.md pointer with a read-only guardrail for coding agents. -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgEyiv4hf6iBgr34ICjN6HnEP/vs Yr31eNU5HhdkQaYd4AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQPlms3dhn0NjE+dgV27VB+qobyazAcBxCGnwpMkuF/K3BTPpVWb/m13mpYavPyzfeA Y4Wkxty1DkXBY9Rd3NCAI= -----END SSH SIGNATURE-----

v2.0.0

What’s new

BridgePort 2.0 is a major release built around a redesigned service model, six new operator features, and a large performance wave.

  • Service is now a template — one Service (image, base compose, base env, deploy strategy) fans out to per-server ServiceDeployments, each with its own env/file overrides, deployed sequentially or in parallel.
  • One-click server bootstrap — install Docker + Compose, deploy the agent, tune sysctl, add swap.
  • Reusable config fragments, atomic batched syncs with rollback, and dry-run previews.
  • Self-describing HTTP API — OpenAPI spec, Swagger UI, and a standardized error envelope.
  • Richer topology diagram — external entities, server clustering, resizable boxes, stable anchors.
  • Performance — worst production transactions cut 5–12x; agent metrics-ingest throughput up ~5x.

Schema changes apply automatically on container start — existing 1:1 Service→Server records are backfilled into ServiceDeployments with no data loss and no manual steps. Programmatic API and CLI users should read Action required below before upgrading.


Performance

A sustained optimization wave, anchored by an 18-scenario stress suite (target: RPS ≥ 500, p99 ≤ 50 ms per endpoint) and validated against the worst production transactions in Sentry.

Hotpath before → after

PathBeforeAfterPR
Agent metrics ingest (25 conns)115 RPS · p50 208 ms · p99 635 ms639 RPS · p50 38 ms · p99 47 ms#183
metrics-summary (worst prod txn)p99 8.2 s in productionp99 17–46 ms#138, #181
health-status (was N+1)p99 224 ms · 184 RPSp99 42 ms · 789 RPS#138, #147
server-metrics-historyp99 395 ms · 1012 RPSp99 12 ms · 4200 RPS (+315%)#163, #181
service-metrics-historyp99 57 ms · 622 RPSp99 12 ms · 3585 RPS (+476%)#181
container-images-listp50/p99 56/81 ms · 529 RPSp50/p99 11/21 ms · 669 RPS#163
Dashboard first paint~6 s<500 ms#174

What changed

  • Metrics ingest batched into one transaction (#183) — each agent push was a serial chain of ~10–26 individual SQLite writes, each grabbing the single writer lock. Now:
    • one prisma.$transaction per push, createMany for service metrics, merged per-deployment updates, O(1) container→deployment map, throttled heartbeat.
  • Single-flight metrics cache (#181) — bursts of identical dashboard polls collapse to one compute:
    • server-metrics-history +315% RPS, service-metrics-history +476%, metrics-summary +423%.
    • cache sits behind existing auth/scope enforcement and is keyed by environment.
  • Monitoring pages — per-card loading (#178) — dropped the top-level render-gate; header/sidebar/lists paint immediately while each chart owns its own state. History endpoints gained ?maxPoints (LTTB downsample) and ?since= delta mode.
  • N+1 and hydration fixes (#138, #147, #145, #146, #148, #149, #150):
    • rewrote getEnvironmentMetricsSummary (Sentry’s worst transaction, p99 8.2 s in prod).
    • health-status reads denormalized cache columns instead of scanning HealthCheckLog.
    • moved sendSystemNotification fan-out to an in-process queue so request handlers no longer block on per-user writes.
    • added SecretUsage/VarUsage join tables, columnar metrics-history responses, compound indexes.
  • Dashboard (#174) — ungated per-section rendering; replaced the per-DB backup-info fan-out with a single batched databases/backup-summary endpoint.

Action required before upgrading

Skip any item whose feature you don’t use through the API or CLI. The web UI handles all of these transparently.

1. Service is now a template with per-server deployments (#137)

  • Runtime fields (containerName, status, health/check results, discovery) and per-server envOverrides moved to a new ServiceDeployment entity.
  • Image-update fields (autoUpdate, latestAvailableTag, lastUpdateCheckAt) moved to ContainerImage.
  • Existing data auto-migrates — each legacy 1:1 Service→Server record becomes one ServiceDeployment. No manual steps.
  • CLI users: update your binary — the Service struct changed shape; older builds will misread responses.
  • API clients reading service.server: a flattened services array and a service.server accessor are kept for back-compat this release, but new code should read service.serviceDeployments[].

2. PATCH of read-only fields is now rejected (#159)

  • A PATCH whose body contains a derived/read-only field (status, exposedPorts, lastCheckedAt, …) fails atomically with 422 + code: READONLY_FIELD, naming the field.
  • Affects services, servers, config-files, container-images, databases, registries, secrets.
  • Fix: send only the fields you intend to change — don’t echo back computed fields.

3. Standardized error envelope (#155)

  • Every non-2xx response now returns:
{ "code": "VALIDATION_ERROR", "message": "...", "field": "...", "hint": "...", "requestId": "..." }
  • code is a documented enum (VALIDATION_ERROR, READONLY_FIELD, UNAUTHORIZED, FORBIDDEN_SCOPE, NOT_FOUND, CONFLICT, IDEMPOTENCY_KEY_REUSED, RATE_LIMITED, INTERNAL).
  • 5xx messages are redacted from clients but still logged + sent to Sentry.
  • Clients that parse error must read message/code instead.

4. Sync responses return an envelope (#159)

  • /config-files/:id/sync-all, /services/:id/sync-files, /servers/:serverId/sync-all-files now return { status, targetsAttempted, targetsSucceeded, targetsFailed, results }.
  • status is 'ok' | 'no_targets' | 'partial' | 'failed'; zero-target syncs now return 200 + 'no_targets' (was 400).
  • The old success: boolean field is a deprecated alias for this release only — migrate off it.

Features

Service templates + per-server deployments (#137)

  • New ServiceDeployment entity owns per-server runtime, status, agent check results, and envOverrides.
  • Deploy strategy chosen at deploy time:
    • sequential — halts on first failure with rollback context.
    • parallelPromise.allSettled, aggregated statuses.
  • ServiceDetail gains Overview / Deployments / Config tabs; per-deployment env overrides editable inline.

Server bootstrap (#156)

  • One Bootstrap action installs Docker + Compose plugin, deploys the agent, applies container-tuned sysctl defaults, and optionally creates a swap file.
  • Ubuntu/Debian only; idempotent (skip-if-present, no version upgrades); requires passwordless sudo / root SSH (preflighted).
  • Live progress streams over SSE into a BootstrapModal; separate Add swap recovery action logs before/after free -m.
  • fstab backed up once before any change; idempotent exact-line append (never sed -i).

Reusable config fragments (#162)

  • Env-scoped, named text blocks that multiple config files include via an ordered list.
  • Rendered as concat(fragment1, …, file.content) with placeholder substitution over the result (dotenv / compose env_file last-wins semantics).
  • Purely additive — files with no fragments attached render byte-for-byte unchanged.
  • Editing a fragment auto-resyncs every file that includes it; deleting an in-use fragment returns 409 listing dependents.
  • New Fragments CRUD page, ordered selector with reorder + preview pane, and POST /api/config-files/:id/preview.

Atomic multi-resource sync (#161)

  • POST /api/sync/batch runs config-file syncs as an all-or-nothing batch with optional rollback.
  • Idempotency-Key replays an identical body and returns 409 on body conflicts.
  • Per-op snapshots enable reverse-order restore + re-sync; GET /api/sync/batch/:batchId inspects past batches; audit rows link the batch ID.
  • v1 scope: config-file syncs only, single environment per batch (service-deploy deferred).

Dry-run previews (#160)

  • Preview a mutating call without performing it via ?dryRun=true or X-Dry-Run: true.
  • Supported on deploy, deployment-plan execute, config-file sync-all, and per-service sync-files.
  • Stops before any host write / container cycle / history row; image digests resolved from the registry (no docker pull); secret values redacted to *** (references preserved).
  • Sync dry-runs return a per-target unified diff vs the current host file.

Topology diagram upgrades (#154)

  • External entities — model off-platform sources/sinks as pill nodes with handles on all four sides; included in Mermaid export.
  • Resizable server boxes and server clustering (collapsible dashed containers that aggregate child edges).
  • Stable connection anchorsleft/right/top/bottom handle IDs preserved verbatim on connect.

Self-describing HTTP API (#155)

  • OpenAPI 3 spec at GET /openapi.json and Swagger UI at /api/docs (generated from registered routes).
  • GET /api/auth/me now returns top-level role, environments[], and scopes[] so clients can check authorization before making a call.
  • (Error-envelope change is under Action required.)

No-silent-success guarantees (#159)

  • Read-only-field PATCHes are rejected; zero-target syncs are surfaced explicitly rather than masquerading as success.
  • Client-visible details under Action required.

Server iteration in config templates (#134)

  • Go-style {{range servers ...}}{{end}} with tag / name (glob) / environment filters.
  • Per-server fields in the body: .name, .hostname, .publicIp, .privateIp, .id, .tags.
  • Two-stage render (iteration first, then ${KEY}), alphabetical by name for deterministic checksums; no new dependency.

Auto-resync config files on value change (#133)

  • PATCH /api/secrets/:id and PATCH /api/vars/:id re-render every config file in that env that references ${KEY}.
  • Fires only when the value actually changes; runs fire-and-forget, never blocks the PATCH.
  • New ConfigFile.autoResync flag lets operators pin a file’s content; triggered syncs are tagged in the audit log.

Free-form service type tagging (#153)

  • Group services by workload type (django, postgres, redis, …) via a free-form typeTag.
  • Configure-modal input with autocomplete from existing tags; filter chip row on the Services list persisted via ?type=.

2.0 quality-of-life bundle (#136)

  • Restart bug fix — services with a compose path now run compose down + compose up --force-recreate so config changes are picked up (falls back to docker restart only when no compose path is set).
  • Per-env Slack channel override — env Settings → Notifications tab; explicit routing rules still win.
  • Config file syntax highlighting — CodeMirror editor, language auto-detected from filename.
  • Config-file “Attached to services” entries now link to the service detail page.

Docker logs visibility (#135)

  • Per-container docker logs captured during each deploy step and shown in the deployment plan view (no SSH).
  • Service logs modal gains a Load older button with scroll preservation; defaultLogLines now drives both capture and the viewer.

Fixes

  • Service→ServiceDeployment split fallout (#163, #152) — fixed crashes from the stale service.server shape on the Config Files page (#152), the Registries linked-services list, and the Add Dependency picker (all now read serviceDeployments[]).
  • Compose host port dropped (#132) — auto-managed compose no longer drops the host port when exposedPorts has no explicit host mapping.
  • Fragments sidebar icon (#173) — the Fragments page got its own icon instead of sharing one.

Security

  • CodeQL #34 (js/clear-text-logging, high) (#182)getRegistryCredentials() wraps decryption in try/catch and re-throws a sanitized, secret-free error, severing the path by which a decryption failure could log encrypted material / nonce. The scheduler now skips a single unreadable credential instead of aborting the whole update run.
  • Supply-chain hardening (#176):
    • root + ui/ .npmrc set min-release-age=1 and ignore-scripts=true to match the Dockerfile.
    • Dependabot publish cooldown per ecosystem (npm 5-day, others 3-day).
    • cleared the autocannon → hyperid → uuid@8.3.2 advisory chain via a uuid@^11 override (npm audit: 0 vulns).
  • Heredoc → writeFile (#163) — config-file sync paths no longer write rendered content through a shell heredoc, so content can’t terminate it early or be reparsed.

Under the hood

  • Stress-test framework + CI (#138) — 18-scenario suite, soft-mode GitHub Actions workflow, sticky PR comments, thresholds checked in.
  • /build skill (#131) — drives a GitHub issue end-to-end to a review-clean PR.
  • Dependency bumps@fastify/cors → 11, pino-pretty → 13 (#177); TypeScript → ^6.0.3 across both workspaces (#175, #167); grouped Dependabot bumps: #120, #121, #122, #165.

Documentation

  • Rewritten services.md for the template/deployment split.
  • New server-bootstrap.md and supply-chain.md guides.
  • Expanded config-files.md (fragments, server iteration, syntax highlighting).
  • Refreshed architecture, API, monitoring, and environment-settings references.

v1.3.0

BridgePort 1.3.0 makes container operations friendlier in three places: deploys now log into private registries automatically, services keep their identity when you rename them, and the dashboard diagram is finally a pleasure to use. Three database migrations apply automatically on container start.


Three migrations are included; all apply automatically on container start with no manual steps.

  • 20260521120000_add_server_registry_login — new ServerRegistryLogin table that caches per-(server, registry) login state so deploys can skip redundant docker login calls. (#106)
  • 20260522120000_unique_service_container_name — adds @@unique([serverId, containerName]) to Service. Safe over existing data because discovery has always set both fields together. (#116)
  • 20260522161709_add_connection_handle_ids — adds sourceHandle / targetHandle columns to ServiceConnection so the diagram can persist which side of a node a connection was drawn from. (#119)

BridgePort now runs docker login over SSH on the target server before pulling private images, so operators no longer need to log in by hand on each box. Login state is cached per (server, registry) in the new ServerRegistryLogin table and is automatically invalidated when you edit the registry’s token, username, password, registryUrl, or type — metadata-only edits leave the cache intact.

  • DigitalOcean, Docker Hub, and generic registries are all supported, with per-type credential mapping.
  • Socket-mode deploys (BridgePort talking to a local Docker daemon) skip the persistent-login path and pass auth in-process to dockerode’s pull() via authconfig, so no state is written to the BridgePort host.
  • Login failure aborts the deploy with docker login failed: <reason> instead of falling through to a misleading “image not found” later.

Service display name and container name are now properly decoupled. Discovery matches Docker containers against the containerName field, so renaming a service to something friendlier (e.g. keycloak-1-production) no longer causes the next scan to create a duplicate row and mark the old one missing.

  • The Configure modal has a hint under Container Name explaining that it drives discovery while display name is free-form.
  • A new @@unique([serverId, containerName]) constraint enforces the invariant at the database level.
  • Docs updated under docs/guides/servers.md and docs/guides/services.md.

The topology diagram had two interaction bugs and an unclear discovery model:

  • Drag-to-connect was unreliable. Connection handles were 8×8 px, nearly invisible, and connectionMode defaulted to strict — so dragging from one node’s right handle to another’s right handle (or releasing on the node body instead of the tiny target dot) silently failed.
  • The X button on manual edges sometimes did nothing. A stale module-level handler ref meant the delete callback could go missing after re-renders.
  • No drag-free alternative. AddConnectionModal existed but wasn’t wired up.

This release fixes all three:

  • Bigger 10×10 handles that grow to 14×14 with a blue ring on node hover and green when they’re a valid drop target. Service and database nodes also gained top/bottom handles, so you can draw connections in any direction.
  • connectionMode={Loose} and a 28-pixel snap radius so any handle pair can connect and you don’t have to land exactly on the dot.
  • Delete callback now flows through edge.data.onDelete (the standard ReactFlow pattern) with optimistic UI — the edge disappears immediately and the API call follows.
  • A new + Add Connection button in the diagram toolbar opens AddConnectionModal for a discoverable, drag-free path.

Connection tests against managed Valkey 8 instances (e.g. DigitalOcean Managed Valkey) were failing with a misleading “Connection is closed” because the default-user ACL on those clusters can NOPERM-block INFO server. The test now uses PING for liveness and falls back to INFO only for best-effort version detection. ACL-restricted users also now get their username passed through (matching the postgres/mysql branches), and underlying connection errors surface a real message instead of being eaten by an empty error handler.


Internal change to the .claude/commands/release.md workflow so future release notes (like this one) are structured, skimmable, and front-load any “Action required” items rather than dumping a flat list of PR bullets.

src/lib/ssh.ts gained helpers used by the new registry-login flow (writing the password to a 0600 temp file and piping into docker login --password-stdin, never as a CLI argument). Covered by new unit tests in src/lib/ssh.test.ts.

v1.2.0

What’s new

BridgePort v1.2.0 introduces scoped API tokens and service accounts for finer-grained automation access, ships a much-improved config file scanner (new detection, polished diff UX, fewer false positives), and rolls out a proper release pipeline with stable image channels. Database migrations apply automatically on container restart — no manual SQL.


Action required before upgrading

Skip this section if you don’t use these features.

1. External scripts that mint per-user API tokens

POST /api/auth/tokens has been removed. Existing tokens keep working (auto-migrated to all-environments scope with the owner’s current role), but any script that minted its own tokens needs updating:

  • Use POST /api/admin/tokens (admin-only), or
  • Mint tokens via the new /admin/integrations UI

Token issuance is now an admin operation so scoped tokens can be tracked centrally. (#96)

2. Pinning to :latest or :stable

These tags no longer track master. They now mean “last released version”. If you want to keep tracking master, switch to the new :edge channel:

# Before — :latest used to point at master HEAD
image: ghcr.io/bridgeinpt/bridgeport:latest
# After
image: ghcr.io/bridgeinpt/bridgeport:edge # master HEAD
image: ghcr.io/bridgeinpt/bridgeport:latest # latest release (this one: v1.2.0)
image: ghcr.io/bridgeinpt/bridgeport:1.2 # minor-locked
image: ghcr.io/bridgeinpt/bridgeport:1 # major-locked

Full channels table: docs/operations/upgrades.md. (#103)


Database migrations

One migration applies automatically on container restart:

  • 20260520120000_add_api_token_scoping_and_service_accounts — adds scope columns to ApiToken, creates the ServiceAccount table, and adds apiTokenId / serviceAccountId columns to AuditLog for forensics. (#96)

No manual SQL required (per the database-migration golden rule).


Features

Scoped API tokens & service accounts (#96)

Tokens can now be scoped to specific environments:

  • Calls outside the token’s allowlist return 403; GET /api/environments is filtered automatically
  • Effective role is min(token, owner) — demote a user and their tokens demote immediately
  • All ~120 audit-log call sites now record the token or service account that performed each action

A new /admin/integrations page provides a sectioned UI for:

  • API Tokens — list / create with scope picker, copy-once display, revoke
  • Service Accounts — full CRUD, individual disable
  • Webhooks and OAuth Apps are placeholders for future work

Config file scanner

Missing reference detection (#102). A new missing_reference suggestion surfaces ${KEY} and ${KEY:-default} references in config files that have no matching secret or variable defined. These are sorted to the top of scan results so they’re easy to spot and fix.

Sharper preview & confirm UX (#102):

  • Apply dialog now prefills the actual value — no more retyping
  • Preview shows collapsible per-file diff hunks instead of full file dumps; auto-expanded for ≤3 files
  • Disabled buttons render as visibly disabled across the UI

Fewer false positives (#104). Three classes of unhelpful suggestions are gone:

  • YAML extraction is UPPER_SNAKE_CASE only — restart: unless-stopped no longer triggers a ${RESTART} suggestion
  • Cross-key repetition within the same file is no longer a detection signal
  • Variable substitution only rewrites full RHS values — no more accidental substring replacements (e.g. ENVIRONMENT=staging rewriting app-staging.bridgein.com inside another variable)

Sentry admin test tab (#99)

A new tab under /admin/notifications → Sentry lets admins:

  • See backend + frontend DSN status and resolved environment at a glance
  • Send synthetic test errors with one click (backend captures + flushes; frontend throws on next tick to mimic real uncaught errors)
  • View inline env-var setup instructions when no DSN is configured

Fixes

Sentry release tag (#99)

APP_VERSION was only reaching the UI build stage, so the backend fell back to package.json (1.0.0). Both /health and Sentry deploy correlation reported the wrong version. Now propagated to the production stage and used everywhere it’s needed.


Security

  • CVE-2026-39406 — patched @hono/node-server (middleware bypass in serveStatic via repeated slashes). Pulled in transitively by @prisma/dev which pins an exact version, so a parent bump can’t fix it. Patched via npm overrides to ^1.19.13. Dev-only scope, but clears the alert. (#100)
  • Command injection in agent deploy — SSH agent deployment now writes the systemd unit file via SFTP instead of a shell heredoc, closing CodeQL alerts #2 and #3. ('EOF' heredoc quoting doesn’t protect against the delimiter word appearing inside the content.) (#71)
  • fast-jwt 6.2.2 → 6.2.4 and other backend-security-group bumps. (#84, #94)

Release pipeline (#103)

This release ships through the new pipeline introduced in this PR:

  • release.yml triggers on v*.*.* tags, builds the image with docker/metadata-action semver tags (:vX.Y.Z, :X.Y.Z, :X.Y, :X, :stable, :latest), and publishes the GitHub Release from the annotated tag message verbatim
  • Prereleases (e.g. v1.2.0-rc.1) only publish exact-version tags — never :latest, :stable, major or minor
  • build.yml is now “Build Edge Image” — publishes :edge + immutable :YYYYMMDDHH-sha, and skips builds for doc-only changes via paths-ignore
  • A new /release slash command drives the curated workflow used to produce these notes

Under the hood

Prisma 6 → 7 (#81)

Manual migration (replaces Dependabot PRs that couldn’t auto-merge due to the deployment-model change):

  • url moved from schema.prisma into a new prisma.config.ts
  • PrismaClient wired to @prisma/adapter-better-sqlite3 driver adapter
  • binaryTargets dropped from the generator block
  • Dockerfile rebuilds better-sqlite3 in the prod-deps layer for Alpine

Other notable bumps

  • Node base image22-alpine26-alpine (#86)
  • dockerode — 4 → 5 (#76)
  • Backend / frontend / CLI dependency-group bumps: #72, #73, #74, #75, #80, #82, #83, #85, #92, #93, #95

Documentation

  • New canonical Channels table in docs/operations/upgrades.md (summary copied into the README)
  • Pinning recommendations added to docker/docker-compose.yml and docs/installation.md
  • Refreshed guides: users, secrets, API reference, configuration (#96, #99, #102, #103)

v1.1.0 — security hardening

v1.1.0 is a security-hardening release. Three critical and five high-severity CVEs patched, SSH command composition hardened against injection, global rate limiting added, Slack webhook validation tightened. All backwards-compatible for self-hosters — pull, restart, migrations run, done.

⚠️ Behavior change to be aware of

Global rate limiting is now active. 100 requests per minute per client IP, across all routes except /health and /api/client-config. If you have programmatic integrations that poll aggressively, they may start getting HTTP 429 responses with a retryAfterSeconds field. Adjust client polling or loosen the limit via custom config if needed.

Security fixes

Patched CVEs

  • fast-jwt 5.0.6 → 6.2.2 (via @fastify/jwt 9 → 10 — PR #69) closes:
    • Critical — Cache confusion via cacheKeyBuilder collisions returning claims from a different token
    • Critical — Incomplete fix for CVE-2023-48223 (JWT algorithm confusion via whitespace-prefixed RSA keys)
    • Highfast-jwt accepts unknown crit header extensions (RFC 7515 violation)
    • Moderate — Stateful RegExp non-deterministic allowed-claim validation (logical DoS)
    • Moderate — ReDoS in allowed* claim validation during token verification
  • Numerous additional CVEs patched via grouped dependency updates (backend, frontend, CLI, agent, Docker).

Code-level hardening

  • Slack webhook URL validation (#63) — replaced a substring check (.includes('hooks.slack.com')) with proper new URL(url).hostname === 'hooks.slack.com'. URLs like https://evil.com/?x=hooks.slack.com are now correctly rejected.
  • SSH command injection surface (#67, #68) — introduced shellEscape() in src/lib/ssh.ts and applied it at every call site where a user-configured value (paths, container names, etc.) is interpolated into a shell command string. Covers deploy, database-backup, database-query-executor, config-files, agent-deploy, and service log streaming. Double-quoting alone was insufficient — $, backticks, and $() were still interpreted.
  • Global rate limiting (#64) — @fastify/rate-limit registered globally, 100 req/min/IP, /health and /api/client-config allow-listed so external monitors don’t get throttled.

Dependencies

Significant bumps:

  • @fastify/jwt 9.1.0 → 10.0.0 (security)
  • @fastify/multipart 9.4.0 → 10.0.0
  • @fastify/cors 10.x → 11.x (bundled in grouped backend security update)
  • zod 3.x → 4.x (backend — with forward-compatible code changes in #62)
  • fast-jwt 5.0.6 → 6.2.2 (transitive via @fastify/jwt)
  • @types/node 22.x → 25.x
  • Docker base image golang:1.22-alpine1.26-alpine
  • CLI Go toolchain 1.22 → 1.25
  • Plus grouped backend-security, frontend-security, cli-minor-and-patch, agent-minor-and-patch, actions-version groups.

Developer experience

  • shellEscape() helper exported from src/lib/ssh.ts — wrap any value interpolated into a command string.
  • New convention documented in CLAUDE.md (Backend Patterns) and docs/development/testing-guide.md (Unit test rule 7 on keeping vi.mock factories in sync with module exports).
  • Dependabot grouping (#50) — security updates and minor+patch bumps now land as one PR per ecosystem per week, instead of dozens of individual PRs.

Upgrading

Terminal window
docker pull ghcr.io/bridgeinpt/bridgeport:latest
docker stop bridgeport && docker rm bridgeport && docker run -d ... # same args as before

Migrations apply automatically on first start. No manual steps.

Full changelog

https://github.com/bridgeinpt/bridgeport/compare/v1.0.0…v1.1.0

v1.0.0 — first public release

BRIDGEPORT is a lightweight, self-hosted deployment management tool for Docker-based infrastructure. Manage servers, deploy containers, monitor health, and orchestrate multi-service rollouts from a single web UI — no Kubernetes required.

This is the first public release. Apache-2.0 licensed.

The problem it solves

Managing Docker containers across multiple servers usually means SSH-ing into each machine, running the right docker pull / docker compose by hand, hoping nothing breaks, and having zero visibility into what’s running where. Kubernetes is overkill when you have 3 or 10 servers. BRIDGEPORT fills the gap between “manual SSH” and “full orchestrator”.

What’s in v1.0

  • Multi-server management — connect servers via SSH or Docker socket; auto-discover running containers
  • One-click deploys with auto-rollback on failed health checks
  • Real-time monitoring — server, service, and database metrics via the bundled Go agent or SSH polling
  • Health checks — container, URL, TCP, and TLS-certificate checks with bounce protection
  • Encrypted secrets (AES-256-GCM) and SSH keys scoped per environment
  • Database backups to any S3-compatible store (DO Spaces, AWS S3, MinIO, Backblaze B2, Wasabi, R2)
  • Notifications — in-app, email (SMTP), Slack, and outgoing webhooks
  • Interactive service topology — your stack, dependencies, and health at a glance
  • Deployment plans — multi-step, dependency-aware rollouts across services
  • CLIbridgeport ssh / logs / exec / deploy from the terminal
  • Plugin system — JSON-defined service and database types with monitoring queries
  • RBAC with admin / operator / viewer roles

Quick start

Terminal window
docker run -d \
--name bridgeport \
-p 3000:3000 \
-v bridgeport-data:/data \
-e DATABASE_URL=file:/data/bridgeport.db \
-e MASTER_KEY=$(openssl rand -base64 32) \
-e JWT_SECRET=$(openssl rand -base64 32) \
-e ADMIN_EMAIL=admin@example.com \
-e ADMIN_PASSWORD=changeme123 \
ghcr.io/bridgeinpt/bridgeport:latest

Open http://localhost:3000 and log in. For production — HTTPS, Docker Compose, hardening — see the Installation Guide.


What’s Changed

New Contributors

Full Changelog: https://github.com/bridgeinpt/bridgeport/commits/v1.0.0