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
| Mode | Resolved branch | Use case |
|---|---|---|
LATEST (default) | application's current default_branch | New-features project that always wants whatever's current. Rename-safe — if the app's default branch is renamed main → trunk, the membership auto-follows. |
PINNED | the membership's tracked_branch literal | Maintenance 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):
| Strategy | Pattern enforced |
|---|---|
NONE (default) | no validation |
TRUNK_BASED | ^(main|master)$ |
RELEASE_BRANCHES | ^(main|release/\d+(\.\d+)?(\.\d+)?(\.x)?)$ |
GITFLOW | ^(main|develop|release/.+|hotfix/.+)$ |
CUSTOM | regex 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)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/applications | List all (active by default; ?activeOnly=false to include soft-deleted) |
| GET | /api/applications/{id} | Read one |
| POST | /api/applications | Create |
| PUT | /api/applications/{id} | Update |
| DELETE | /api/applications/{id} | Soft-delete (sets is_active=false) |
| GET | /api/applications/{id}/projects | List 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:
| Field | Source | Use |
|---|---|---|
applicationId | applications.id | reference the canonical app from other tables / new endpoints |
membershipId | project_application_memberships.id | unlink via DELETE /api/applications/{appId}/link-project/{projectId} |
trackingMode | membership | LATEST / PINNED display + edit |
trackedBranch | membership | branch 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 rowPUT/PATCH—{X}in the path is the membership id; updates the Application referenced by that membershipDELETE— 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.
| Layer | Owns | Example |
|---|---|---|
| Application | the relative path | /v3/api-docs |
| EnvironmentTarget | the base URL | http://user-service:8089 |
| (Composed) | full runtime URL | http://user-service:8089/v3/api-docs |
Defaults
- New
SPRING_BOOTapplications 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-docson every existingSPRING_BOOTapplication 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
env_target.swaggerJsonUrl(explicit full-URL override) — highest.env_target.baseUrl + env_target.swaggerPath(env owns both).env_target.baseUrl + application.swaggerPath(the common case).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).
| Value | Meaning |
|---|---|
PUBLIC | App is intentionally public — no auth needed. |
AUTHENTICATED | App 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").