Skip to main content
A milestone is a named checkpoint inside a project — typically a release target, a deadline, or a logical chunk of work. Each milestone tracks an issue rollup (issue_count, done_count) so the FE can render burn-down without a separate query. List and create are nested under the owning project; update and delete address the milestone id directly. Implementation: internal/api/milestone_handler.go. Backed by the milestones table; issue counts are computed from missions WHERE mission_type = 'issue'.
All endpoints require an authenticated session and workspace context. The URL space is split:
  • List / Create are nested under the project: /api/v1/projects/{projectId}/milestones. The project must belong to the calling workspace (projects.workspace_id), otherwise 404.
  • Update / Delete address the milestone directly: /api/v1/milestones/{milestoneId}. Workspace ownership is verified by joining milestones → projects → workspace_id.

Endpoints

Milestone shape

GET /api/v1/projects/{projectId}/milestones

Request: projectId is a required path parameter. No request body. List milestones in a project, ordered by position ASC, created_at ASC. Auth: authenticated session + workspace context. Response: 200 OK — JSON array (never null).
The List query uses a LEFT JOIN against an aggregated subquery so milestones with zero issues still appear (counts come back as 0, not null).

POST /api/v1/projects/{projectId}/milestones

Create a new milestone in the project. Position is auto-assigned at the end of the project’s milestone list. Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role (requireRole("create")). Request body: Response: 201 Created with the milestone object (issue_count: 0, done_count: 0). WebSocket event: milestone.created broadcast on the workspace channel with { "id": "<id>", "project_id": "<projectId>" }.

PATCH /api/v1/milestones/{milestoneId}

Partial update. Workspace ownership is verified by joining milestones → projects → workspace_id — a cross-workspace id returns 404, not 403, so the surface can’t be probed for which milestone ids exist. Auth: authenticated session + workspace context + OWNER, ADMIN, or MANAGER role. Request body: every field optional. Response: 200 OK with the full updated milestone object (rollup counts are recomputed inline via correlated subqueries). WebSocket event: milestone.updated broadcast on the workspace channel with { "id": "<id>", "project_id": "<projectId>" }.

DELETE /api/v1/milestones/{milestoneId}

Request: milestoneId is a required path parameter. No request body.
Hard delete the milestone. Attached issues are not deleted — they’re unlinked from the milestone (UPDATE missions SET milestone_id = NULL WHERE milestone_id = ?) inside the same transaction as the milestone row delete. This keeps history intact while letting the project be reorganised without orphan FK errors.
Auth: authenticated session + workspace context + OWNER or ADMIN role (requireRole("manage")). Response: 204 No Content. WebSocket event: milestone.deleted broadcast on the workspace channel with { "id": "<id>", "project_id": "<projectId>" }.

See also

  • Issuesmilestone_id on a mission attaches it here; delete unlinks instead of cascading.
  • Recurring Issues — can target a milestone via milestone_id so every fire lands in the right bucket.
  • Crews — projects belong to a workspace, not a crew, but crews are the runtime container for the missions filed against the milestone.