Skip to main content

Applications & branch tracking

An Application in Proofarc is a deployable thing your team owns — a REST API, a web UI, a mobile app. Applications exist independently of Projects: the same application can be tracked under multiple projects (e.g. one project for "new features", another for "maintenance"), without duplicating its git URL, registry credentials, or jira key.

This page describes the data model as of PR1 of the app-project decoupling work. The UI exposes it in PR3; PR1 ships the API surface and the data model only.

Mental model

Application (canonical, top-level)
│ metadata: name, git_url, default_branch, registry, jira…

└── ProjectApplicationMembership (thin join)
│ project_id ──► Project
│ application_id ──► Application
│ tracking_mode LATEST | PINNED
│ tracked_branch "release/3.x" (when PINNED)

One Application, N memberships, N Projects. Each membership is a declaration: "this project tracks this application, on this branch."

Tracking modes

ModeResolved branchUse case
LATEST (default)application's current default_branchNew-features project that always wants whatever's current. Rename-safe — if the app's default branch is renamed maintrunk, the membership auto-follows.
PINNEDthe membership's tracked_branch literalMaintenance project on release/3.x. Will not drift even if the application's default_branch changes.

Project-level branch strategy

Each Project owns a branch_strategy field that validates membership tracked_branch values with a soft warning (never blocks save):

StrategyPattern enforced
NONE (default)no validation
TRUNK_BASED^(main|master)$
RELEASE_BRANCHES^(main|release/\d+(\.\d+)?(\.\d+)?(\.x)?)$
GITFLOW^(main|develop|release/.+|hotfix/.+)$
CUSTOMregex you set on Project.branchNamingPattern

Built-in regexes live in BranchStrategy.java. The DB only stores the strategy name plus your custom regex (when relevant) — one source of truth.

API surface (PR1)

MethodPathPurpose
GET/api/applicationsList all (active by default; ?activeOnly=false to include soft-deleted)
GET/api/applications/{id}Read one
POST/api/applicationsCreate
PUT/api/applications/{id}Update
DELETE/api/applications/{id}Soft-delete (sets is_active=false)
GET/api/applications/{id}/projectsList the memberships (which projects track this app, on which branch)
POST/api/applications/{id}/link-project/{projectId}Idempotent link with {trackingMode, trackedBranch} body
DELETE/api/applications/{id}/link-project/{projectId}Unlink (idempotent)

The existing project-scoped endpoint /api/projects/{id}/applications continues to work unchanged for now; PR2 will switch its read path to the new memberships table.

Example: same app, two projects

# Create the canonical app once
APP_ID=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "user-service",
"applicationTech": "SPRING_BOOT",
"gitUrl": "https://github.com/acme/user-service.git",
"defaultBranch": "main"
}' "$URL/api/applications" | jq -r .id)

# Track it under the new-features project on main (LATEST → auto-follow)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"trackingMode": "LATEST"}' \
"$URL/api/applications/$APP_ID/link-project/2"

# Track the SAME app under the maintenance project on release/3.x (PINNED)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"trackingMode": "PINNED", "trackedBranch": "release/3.x"}' \
"$URL/api/applications/$APP_ID/link-project/5"

# Inspect — same app, two memberships, two different branches
curl -s -H "Authorization: Bearer $TOKEN" \
"$URL/api/applications/$APP_ID/projects" | jq

Output:

[
{"projectId": 2, "trackingMode": "LATEST", "trackedBranch": null, "resolvedBranch": "main"},
{"projectId": 5, "trackingMode": "PINNED", "trackedBranch": "release/3.x", "resolvedBranch": "release/3.x"}
]

Migration (PR2)

Every existing project_applications row has been backfilled into the new tables. The migration script database/migrations/decouple-applications.sql is idempotent — safe to re-run. Dedup by name: same logical app appearing under multiple projects → one applications row + N memberships.

While the legacy /api/projects/{id}/applications endpoint is still the read path the UI uses, the backend dual-writes: any create / update / delete via the legacy endpoint mirrors into the new canonical Application + membership tables. The new shape is always current and can be queried via /api/applications/{id}/projects for cross-project views.

Read path switched (PR4)

GET /api/projects/{id}/applications now reads from the canonical applications table joined with project_application_memberships. The response shape stays backwards-compatible (id still maps to the legacy project_applications.id), but gains four new fields:

FieldSourceUse
applicationIdapplications.idreference the canonical app from other tables / new endpoints
membershipIdproject_application_memberships.idunlink via DELETE /api/applications/{appId}/link-project/{projectId}
trackingModemembershipLATEST / PINNED display + edit
trackedBranchmembershipbranch literal when PINNED

The legacy ProjectApplication entity and its project_applications table are marked @Deprecated(forRemoval = true). They stay alive because deployment_jobs, build_jobs, scan_job, and release_notes still hold FK references — a future PR will migrate those, then drop the table.

Legacy table dropped (PR5)

The project_applications table is gone. Application + the membership join are the canonical and only path. ProjectApplication entity and ProjectApplicationRepository are deleted from the codebase; the mirrorToCanonical dual-write helper is removed.

FK references on dependent tables (deployment_jobs, build_jobs, scan_job, release_notes) had 0 non-null rows when the migration ran — no row-level data migration was needed. The two existing FK constraints (scan_job_application_id_fkey, release_notes_application_id_fkey) were re-targeted to point at applications.id so future inserts on those columns work against the canonical table.

The legacy /api/projects/{projectId}/applications endpoint (POST/PUT/PATCH/DELETE) still exists but its semantics are now:

  • POST — create or find a canonical Application by name + link it to the project via a new membership row
  • PUT / PATCH{X} in the path is the membership id; updates the Application referenced by that membership
  • DELETE — unlink the membership (the Application stays alive in case other projects reference it)

This means existing UI (e.g. ProjectSettings) keeps working without changes — the controller endpoints translate transparently.

Swagger / OpenAPI on applications (release#107)

An Application can declare a relative swagger path — the URL path its OpenAPI spec is reachable at, independent of where it's deployed. The full runtime swagger URL is composed from this path plus the environment target's base URL.

LayerOwnsExample
Applicationthe relative path/v3/api-docs
EnvironmentTargetthe base URLhttp://user-service:8089
(Composed)full runtime URLhttp://user-service:8089/v3/api-docs

Defaults

  • New SPRING_BOOT applications default to /v3/api-docs (springdoc-openapi's standard). Other tech stacks default to null and require explicit user input.
  • The legacy backfill migration sets /v3/api-docs on every existing SPRING_BOOT application that has no swagger_path yet.

Per-environment overrides

When a specific environment exposes swagger at a non-standard URL (e.g. behind a reverse proxy), set either:

  • EnvironmentTarget.swaggerJsonUrl — full URL override, wins over everything else.
  • EnvironmentTarget.swaggerPath — per-env path that beats the app default when composed with the env's base URL.

Resolution order

  1. env_target.swaggerJsonUrl (explicit full-URL override) — highest.
  2. env_target.baseUrl + env_target.swaggerPath (env owns both).
  3. env_target.baseUrl + application.swaggerPath (the common case).
  4. null (no swagger configured anywhere).

Endpoint

GET /api/applications/{id}/swagger-url?environment={id|name} returns the full resolution payload:

{
"applicationId": 2,
"applicationName": "User Service API",
"environmentId": 1,
"environmentName": "development",
"environmentTargetId": 100,
"environmentTargetUrl": "http://user-service:8089",
"swaggerPath": "/v3/api-docs",
"computedSwaggerUrl": "http://user-service:8089/v3/api-docs",
"source": "composed"
}

source is one of env_override, env_composed, composed, or none — explains which resolution branch matched.

The MCP resolve_swagger_url(application, environment) tool wraps this endpoint for agents.

Authentication requirement (release#126)

An Application also declares whether it requires authentication. This is the intent — what the app fundamentally needs — independent of where it's deployed. Per-environment overrides handle the cases where reality differs from intent (see projects-environments).

ValueMeaning
PUBLICApp is intentionally public — no auth needed.
AUTHENTICATEDApp requires auth for normal use.
UNKNOWN (default)Not declared yet — agents ask the user before guessing.

Set this on the Application form (Authentication requirement select). Existing applications default to UNKNOWN — explicitly mark them as you triage.

Why bother

The MCP list_project_applications tool surfaces authRequirement, and list_environments returns a resolved effectiveAuthRequirement that composes the app default with any env-level override. Agents running through demo flows use this to decide whether to authenticate before calling — without it, an empty credentialTags: [] list is ambiguous (could mean "no auth needed" or "auth needed but unconfigured").