You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/06/12 22:19:17 UTC

(airavata-custos) 01/01: Integrated swag-driven OpenAPI spec generation

This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch swag-api
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git

commit 1ec838001799aa57ff997c784bb175980047261a
Author: lahiruj <la...@gmail.com>
AuthorDate: Fri Jun 12 18:19:06 2026 -0400

    Integrated swag-driven OpenAPI spec generation
---
 Makefile                                           |   30 +-
 api/README.md                                      |   55 +
 api/core.openapi.yaml                              | 3766 ++++++++++++++++++++
 cmd/server/main.go                                 |   12 +
 .../ACCESS/AMIE-Processor/api/amie.openapi.yaml    |   81 +
 .../ACCESS/AMIE-Processor/pkg/amie/loader.go       |   12 +
 .../ACCESS/AMIE-Processor/server/handlers.go       |   14 +-
 go.mod                                             |   20 +
 go.sum                                             |   71 +
 internal/server/audit_handlers.go                  |   45 +-
 internal/server/privilege.go                       |   74 +-
 internal/server/role.go                            |  130 +-
 internal/server/server.go                          |  660 ++++
 13 files changed, 4944 insertions(+), 26 deletions(-)

diff --git a/Makefile b/Makefile
index cc758c242..8eb569f9c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,39 @@
 SHELL := /bin/bash
-.PHONY: help integration-test-amie integration-test-amie-down
+.PHONY: help gen-api build verify-no-drift test integration-test verify integration-test-amie integration-test-amie-down
 
 help:
 	@echo "Targets:"
+	@echo "  gen-api                     regenerate every OpenAPI spec from swag annotations"
+	@echo "  build                       gen-api, then go build ./..."
+	@echo "  verify-no-drift             gen-api, then fail if any *.openapi.yaml changed"
+	@echo "  test                        go test ./..."
+	@echo "  integration-test            full integration suite (AMIE for now)"
+	@echo "  verify                      verify-no-drift + go vet + test"
 	@echo "  integration-test-amie       run the AMIE connector integration suite end-to-end"
 	@echo "  integration-test-amie-down  tear down the AMIE test stack only"
 
+gen-api:
+	go generate ./...
+
+build: gen-api
+	go build ./...
+
+verify-no-drift: gen-api
+	@if [ -n "$$(git status --porcelain api/ connectors/*/api/ 2>/dev/null)" ]; then \
+		echo "OpenAPI drift detected — run 'make gen-api' and commit the result."; \
+		git status --porcelain api/ connectors/*/api/; \
+		exit 1; \
+	fi
+
+test:
+	go test ./...
+
+verify: verify-no-drift
+	go vet ./...
+	$(MAKE) test
+
+integration-test: integration-test-amie
+
 integration-test-amie:
 	bash scripts/run-amie-integration-tests.sh
 
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 000000000..11f9ea4d1
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,55 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# API specs
+
+Auto-generated OpenAPI specs for Apache Custos.
+
+## Layout
+
+One spec per code-ownership unit:
+
+| Spec | Path | Scope |
+|---|---|---|
+| Core | `api/core.openapi.yaml` | Core REST endpoints. |
+| Connector | `connectors/<NAME>/api/<name>.openapi.yaml` | A connector's endpoints under `/connectors/<name>/`. |
+
+Connector specs ship alongside the connector code; the core spec never
+declares connector endpoints. The reference connector today is AMIE at
+`connectors/ACCESS/AMIE-Processor/api/amie.openapi.yaml`.
+
+## Generating
+
+Specs are produced from [`swag`](https://github.com/swaggo/swag)
+annotations on each handler in the Go source. Regenerate after editing
+any annotation:
+
+```
+make gen-api
+```
+
+CI enforces freshness via `make verify-no-drift`: modifying a handler
+annotation without committing the regenerated spec fails the build. The
+`swag` version is pinned in `go.sum`; `go generate` invokes it through
+`go run`, so contributors do not need to install the binary separately.
+
+## Consuming
+
+Both files are standard OpenAPI documents and load into any
+OpenAPI-aware client, viewer, or codegen tool.
diff --git a/api/core.openapi.yaml b/api/core.openapi.yaml
new file mode 100644
index 000000000..e3334c3d2
--- /dev/null
+++ b/api/core.openapi.yaml
@@ -0,0 +1,3766 @@
+basePath: /
+definitions:
+  models.AllocationStatus:
+    enum:
+    - ACTIVE
+    - INACTIVE
+    - DELETED
+    type: string
+    x-enum-varnames:
+    - ACTIVE
+    - INACTIVE
+    - DELETED
+  models.ComputeAllocation:
+    properties:
+      compute_cluster_id:
+        description: The ID of the compute cluster where the allocation is provisioned.
+        type: string
+      end_time:
+        type: string
+      id:
+        type: string
+      initial_su_amount:
+        description: SUs allocated at the time of allocation creation.
+        type: integer
+      name:
+        type: string
+      project_id:
+        type: string
+      start_time:
+        type: string
+      status:
+        allOf:
+        - $ref: '#/definitions/models.AllocationStatus'
+        description: ACTIVE, INACTIVE, DELETED, etc.
+    type: object
+  models.ComputeAllocationChangeRequest:
+    properties:
+      approver_id:
+        description: The ID of the user who approved/rejected the change request,
+          if applicable.
+        type: string
+      change_status:
+        description: '"PENDING", "APPROVED", "REJECTED", etc.'
+        type: string
+      compute_allocation_id:
+        type: string
+      id:
+        type: string
+      reason:
+        description: The reason for the change request, e.g., "Need more SUs for upcoming
+          jobs", "Requesting reduction in SUs due to project completion", etc.
+        type: string
+      requested_status:
+        allOf:
+        - $ref: '#/definitions/models.AllocationStatus'
+        description: ACTIVE, INACTIVE, DELETED, etc.
+      requested_su_amount:
+        description: The requested allocation amount in SUs, e.g., 1200 SUs, etc.
+        type: integer
+      requester_id:
+        description: The ID of the user who made the change request.
+        type: string
+      timestamp:
+        description: The time when the change request was made.
+        type: string
+    type: object
+  models.ComputeAllocationChangeRequestEvent:
+    properties:
+      compute_allocation_change_request_id:
+        type: string
+      description:
+        description: Optional description of the event, e.g., "Change request created
+          by user", "Change request approved by admin", etc.
+        type: string
+      event_type:
+        description: '"CREATED", "APPROVED", "REJECTED", etc.'
+        type: string
+      id:
+        type: string
+      timestamp:
+        description: The time when the event occurred.
+        type: string
+    type: object
+  models.ComputeAllocationDiff:
+    properties:
+      compute_allocation_id:
+        type: string
+      description:
+        description: Optional description of the diff, e.g., "SU usage updated based
+          on job completion", "Allocation marked as INACTIVE due to end time reached",
+          etc.
+        type: string
+      diff_type:
+        description: '"USAGE_UPDATE", "ALLOCATION_STATUS_CHANGE", etc.'
+        type: string
+      id:
+        type: string
+      new_su_amount:
+        description: New allocation amount in SUs, e.g., 900 SUs, etc.
+        type: integer
+      status:
+        allOf:
+        - $ref: '#/definitions/models.AllocationStatus'
+        description: ACTIVE, INACTIVE, DELETED, etc.
+      timestamp:
+        description: The time when the diff was generated.
+        type: string
+    type: object
+  models.ComputeAllocationMembership:
+    properties:
+      compute_allocation_id:
+        type: string
+      end_time:
+        type: string
+      id:
+        type: string
+      membership_status:
+        allOf:
+        - $ref: '#/definitions/models.AllocationStatus'
+        description: ACTIVE, INACTIVE, etc.
+      start_time:
+        type: string
+      user_id:
+        type: string
+    type: object
+  models.ComputeAllocationMembershipResourceOverride:
+    properties:
+      compute_allocation_membership_id:
+        type: string
+      compute_allocation_resource_id:
+        type: string
+      id:
+        type: string
+      override_resource_amount:
+        description: The overridden amount of the resource for the user (e.g., number
+          of CPUs, GPUs).
+        type: integer
+      override_resource_time:
+        description: The overridden wall-clock time in minutes that the resource amount
+          is granted for.
+        type: integer
+    type: object
+  models.ComputeAllocationResource:
+    properties:
+      compute_cluster_id:
+        description: The ID of the compute cluster the resource (partition) belongs
+          to.
+        type: string
+      id:
+        type: string
+      name:
+        description: resource / partition name, e.g., "cpu-01", "gpu-01", "gpu-interactive",
+          etc.
+        type: string
+      resource_amount:
+        description: Number of CPUs, GPUs.
+        type: integer
+      resource_type:
+        description: 'TRES: cpu, gres/gpu'
+        type: string
+    type: object
+  models.ComputeAllocationResourceMapping:
+    properties:
+      compute_allocation_id:
+        type: string
+      compute_allocation_resource_id:
+        type: string
+      id:
+        type: string
+      resource_amount:
+        description: Amount of the resource allocated to this allocation (e.g., number
+          of CPUs, GPUs).
+        type: integer
+      resource_time:
+        description: Wall-clock time in minutes that the allocated amount is granted
+          for.
+        type: integer
+    type: object
+  models.ComputeAllocationResourceRate:
+    properties:
+      compute_allocation_resource_id:
+        type: string
+      end_time:
+        description: The time when this rate expires.
+        type: string
+      id:
+        type: string
+      rate:
+        description: The rate for the resource in SUs per unit, e.g., 0.5 SU per CPU
+          hour, 2 SU per GPU hour, etc.
+        type: number
+      start_time:
+        description: The time when this rate becomes effective.
+        type: string
+    type: object
+  models.ComputeAllocationUsage:
+    properties:
+      compute_allocation_id:
+        type: string
+      compute_allocation_resource_id:
+        description: The specific resource consumed, e.g., 20 CPU hours, 10 GPU hours,
+          etc.
+        type: string
+      id:
+        type: string
+      job_id:
+        description: The ID of the job that consumed the allocation.
+        type: string
+      last_updated:
+        description: The last time the usage was updated. SU should be calculated
+          up to this point in time and charge rates should be applied based on the
+          rates effective at this time.
+        type: string
+      used_raw_amount:
+        description: The raw amount of resource used, e.g., 20 CPU hours, 10 GPU hours,
+          etc.
+        type: number
+      used_su_amount:
+        description: SUs used by the allocation, e.g., 200 SUs, etc.
+        type: number
+      user_id:
+        description: The ID of the user who used the allocation.
+        type: string
+    type: object
+  models.ComputeCluster:
+    properties:
+      id:
+        type: string
+      name:
+        description: A human-readable name for the compute cluster, e.g., "Cluster
+          A", "Cluster B", etc.
+        type: string
+    type: object
+  models.ComputeClusterUser:
+    properties:
+      compute_cluster_id:
+        type: string
+      id:
+        type: string
+      local_username:
+        description: The username of the user on the compute cluster, which may be
+          different from their Airavata Custos username.
+        type: string
+      user_id:
+        type: string
+    type: object
+  models.Organization:
+    properties:
+      id:
+        type: string
+      name:
+        type: string
+      originated_id:
+        description: 'The ID of the organization in origination. For example: ACCESS
+          Record ID.'
+        type: string
+    type: object
+  models.PrivilegeKey:
+    enum:
+    - amie:read
+    - amie:write
+    - hpc:read
+    - hpc:write
+    - signer:read
+    - signer:write
+    - privileges:grant
+    - roles:manage
+    type: string
+    x-enum-varnames:
+    - PrivilegeAMIERead
+    - PrivilegeAMIEWrite
+    - PrivilegeHPCRead
+    - PrivilegeHPCWrite
+    - PrivilegeSignerRead
+    - PrivilegeSignerWrite
+    - PrivilegeGrant
+    - PrivilegeRolesManage
+  models.Project:
+    properties:
+      created_time:
+        type: string
+      id:
+        type: string
+      originated_id:
+        description: 'The ID of the project in origination. For example: ACCESS Record
+          ID.'
+        type: string
+      origination:
+        description: ACCESS, NAIRR, XRASS, etc.
+        type: string
+      project_pi_id:
+        type: string
+      status:
+        $ref: '#/definitions/models.ProjectStatus'
+      title:
+        type: string
+    type: object
+  models.ProjectStatus:
+    enum:
+    - ACTIVE
+    - INACTIVE
+    - DELETED
+    type: string
+    x-enum-varnames:
+    - ProjectActive
+    - ProjectInactive
+    - ProjectDeleted
+  models.Role:
+    properties:
+      created_at:
+        type: string
+      description:
+        type: string
+      id:
+        type: string
+      is_system:
+        type: boolean
+      name:
+        type: string
+    type: object
+  models.TraceEvent:
+    properties:
+      created_at:
+        type: string
+      description:
+        type: string
+      entity_id:
+        type: string
+      entity_type:
+        type: string
+      event_type:
+        type: string
+      parent_span_id:
+        type: string
+      source:
+        type: string
+      span_id:
+        type: string
+      status:
+        type: string
+    type: object
+  models.TraceNode:
+    properties:
+      children:
+        items:
+          $ref: '#/definitions/models.TraceNode'
+        type: array
+      created_at:
+        type: string
+      description:
+        type: string
+      entity_id:
+        type: string
+      entity_type:
+        type: string
+      event_type:
+        type: string
+      parent_span_id:
+        type: string
+      source:
+        type: string
+      span_id:
+        type: string
+      status:
+        type: string
+    type: object
+  models.TraceSummary:
+    properties:
+      ended_at:
+        type: string
+      event_count:
+        type: integer
+      root_operation:
+        type: string
+      source:
+        type: string
+      started_at:
+        type: string
+      status:
+        type: string
+      trace_id:
+        type: string
+    type: object
+  models.User:
+    properties:
+      email:
+        type: string
+      first_name:
+        type: string
+      id:
+        type: string
+      last_name:
+        type: string
+      middle_name:
+        type: string
+      organization_id:
+        type: string
+      status:
+        $ref: '#/definitions/models.UserStatus'
+    type: object
+  models.UserIdentity:
+    properties:
+      created_at:
+        type: string
+      email:
+        description: the email configured by the source
+        type: string
+      external_id:
+        description: the source's native identifier
+        type: string
+      id:
+        type: string
+      metadata:
+        description: JSON-encoded source-specific fields
+        type: string
+      oidc_sub:
+        description: OIDC subject when the source issues one
+        type: string
+      source:
+        description: e.g. access, nairr, cilogon
+        type: string
+      user_id:
+        type: string
+    type: object
+  models.UserPrivilege:
+    properties:
+      granted_at:
+        type: string
+      granted_by:
+        type: string
+      id:
+        type: string
+      privilege:
+        $ref: '#/definitions/models.PrivilegeKey'
+      reason:
+        type: string
+      user_id:
+        type: string
+    type: object
+  models.UserRole:
+    properties:
+      granted_at:
+        type: string
+      granted_by:
+        type: string
+      reason:
+        type: string
+      role_id:
+        type: string
+      user_id:
+        type: string
+    type: object
+  models.UserStatus:
+    enum:
+    - ACTIVE
+    - INACTIVE
+    - SUSPENDED
+    - MERGED
+    type: string
+    x-enum-varnames:
+    - UserActive
+    - UserInactive
+    - UserSuspended
+    - UserMerged
+  server.attachResourceRequest:
+    properties:
+      compute_allocation_resource_id:
+        type: string
+      resource_amount:
+        type: integer
+      resource_time:
+        type: integer
+    type: object
+  server.createRoleRequest:
+    properties:
+      description:
+        type: string
+      name:
+        type: string
+    type: object
+  server.grantPrivilegeRequest:
+    properties:
+      privilege:
+        $ref: '#/definitions/models.PrivilegeKey'
+      reason:
+        type: string
+    type: object
+  server.grantRoleRequest:
+    properties:
+      reason:
+        type: string
+      roleID:
+        type: string
+    type: object
+  server.mergeUsersRequest:
+    properties:
+      retiring_user_id:
+        type: string
+      surviving_user_id:
+        type: string
+    type: object
+  server.revokePrivilegeRequest:
+    properties:
+      reason:
+        type: string
+    type: object
+  server.revokeRoleRequest:
+    properties:
+      reason:
+        type: string
+    type: object
+  server.rolePrivilegeRequest:
+    properties:
+      privilege:
+        $ref: '#/definitions/models.PrivilegeKey'
+    type: object
+  server.updateAllocationResourceMappingRequest:
+    properties:
+      resource_amount:
+        type: integer
+      resource_time:
+        type: integer
+    type: object
+  server.updateRoleRequest:
+    properties:
+      description:
+        type: string
+      name:
+        type: string
+    type: object
+host: localhost:8080
+info:
+  contact: {}
+  description: 'REST API for Apache Custos: organizations, users, projects, compute
+    clusters and allocations, audit traces, and the privilege/role authorization layer.
+    Authenticate every request with the X-Custos-User-Id header. Connector endpoints
+    (/connectors/<name>/...) are documented in separate specs.'
+  title: Apache Custos Core API
+  version: 0.2.0
+paths:
+  /audit/events:
+    get:
+      parameters:
+      - description: 32-char lowercase hex
+        in: query
+        name: trace_id
+        required: true
+        type: string
+      - description: 16-char lowercase hex
+        in: query
+        name: span_id
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            properties:
+              events:
+                items:
+                  $ref: '#/definitions/models.TraceEvent'
+                type: array
+            type: object
+        "400":
+          description: Malformed trace_id or span_id
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "503":
+          description: Audit trace store not configured
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List audit events for a trace (optionally one span)
+      tags:
+      - Audit
+  /audit/sources:
+    get:
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            properties:
+              sources:
+                items:
+                  type: string
+                type: array
+            type: object
+        "503":
+          description: Audit trace store not configured
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List distinct audit-event sources
+      tags:
+      - Audit
+  /audit/traces:
+    get:
+      description: One row per distinct trace_id. `q` matches trace_id hex prefix
+        or root-operation substring.
+      parameters:
+      - collectionFormat: multi
+        description: Restrict to listed sources (repeatable)
+        in: query
+        items:
+          type: string
+        name: source
+        type: array
+      - collectionFormat: multi
+        description: ok | error | in_progress (repeatable)
+        in: query
+        items:
+          type: string
+        name: status
+        type: array
+      - description: RFC3339 lower bound on started_at
+        in: query
+        name: from
+        type: string
+      - description: RFC3339 upper bound on started_at
+        in: query
+        name: to
+        type: string
+      - description: Substring match on trace_id hex or root operation
+        in: query
+        name: q
+        type: string
+      - description: Page size (default 50, max 200)
+        in: query
+        name: limit
+        type: integer
+      - description: Page offset (default 0)
+        in: query
+        name: offset
+        type: integer
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            properties:
+              limit:
+                type: integer
+              offset:
+                type: integer
+              total:
+                type: integer
+              traces:
+                items:
+                  $ref: '#/definitions/models.TraceSummary'
+                type: array
+            type: object
+        "400":
+          description: Invalid filter value
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "503":
+          description: Audit trace store not configured
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List audit traces with filters
+      tags:
+      - Audit
+  /audit/traces/{trace_id}:
+    get:
+      parameters:
+      - description: 32-char lowercase hex
+        in: path
+        name: trace_id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            properties:
+              trace_id:
+                type: string
+              tree:
+                items:
+                  $ref: '#/definitions/models.TraceNode'
+                type: array
+              truncated:
+                type: boolean
+            type: object
+        "400":
+          description: Malformed trace_id
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Trace not found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "503":
+          description: Audit trace store not configured
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a trace's nested span tree
+      tags:
+      - Audit
+  /compute-allocation-change-request-events:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Event payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationChangeRequestEvent'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationChangeRequestEvent'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a change request event
+      tags:
+      - Change Request Events
+  /compute-allocation-change-request-events/{id}:
+    delete:
+      parameters:
+      - description: Event ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a change request event
+      tags:
+      - Change Request Events
+    get:
+      parameters:
+      - description: Event ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationChangeRequestEvent'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a change request event
+      tags:
+      - Change Request Events
+  /compute-allocation-change-requests:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Change request payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationChangeRequest'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationChangeRequest'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute allocation change request
+      tags:
+      - Compute Allocation Change Requests
+  /compute-allocation-change-requests/{id}:
+    delete:
+      parameters:
+      - description: Change request ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a compute allocation change request
+      tags:
+      - Compute Allocation Change Requests
+    get:
+      parameters:
+      - description: Change request ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationChangeRequest'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute allocation change request
+      tags:
+      - Compute Allocation Change Requests
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Change request ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Change request payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationChangeRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationChangeRequest'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a compute allocation change request
+      tags:
+      - Compute Allocation Change Requests
+  /compute-allocation-change-requests/{id}/events:
+    get:
+      parameters:
+      - description: Change request ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationChangeRequestEvent'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List events for a change request
+      tags:
+      - Change Request Events
+  /compute-allocation-change-requests/{id}/events/latest:
+    get:
+      parameters:
+      - description: Change request ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationChangeRequestEvent'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get the most recent event for a change request
+      tags:
+      - Change Request Events
+  /compute-allocation-diffs:
+    post:
+      consumes:
+      - application/json
+      description: Records a discrete change against a compute allocation (e.g. USAGE_UPDATE
+        or ALLOCATION_STATUS_CHANGE).
+      parameters:
+      - description: Diff payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationDiff'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationDiff'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute allocation diff
+      tags:
+      - Compute Allocation Diffs
+  /compute-allocation-diffs/{id}:
+    delete:
+      parameters:
+      - description: Diff ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a compute allocation diff
+      tags:
+      - Compute Allocation Diffs
+    get:
+      parameters:
+      - description: Diff ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationDiff'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute allocation diff
+      tags:
+      - Compute Allocation Diffs
+  /compute-allocation-membership-resource-overrides:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Override payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationMembershipResourceOverride'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationMembershipResourceOverride'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a membership resource override
+      tags:
+      - Membership Resource Overrides
+  /compute-allocation-membership-resource-overrides/{id}:
+    delete:
+      parameters:
+      - description: Override ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a membership resource override
+      tags:
+      - Membership Resource Overrides
+    get:
+      parameters:
+      - description: Override ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationMembershipResourceOverride'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a membership resource override
+      tags:
+      - Membership Resource Overrides
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Override ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Override payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationMembershipResourceOverride'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationMembershipResourceOverride'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a membership resource override
+      tags:
+      - Membership Resource Overrides
+  /compute-allocation-memberships:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Membership payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationMembership'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationMembership'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute allocation membership
+      tags:
+      - Compute Allocation Memberships
+  /compute-allocation-memberships/{id}:
+    delete:
+      parameters:
+      - description: Membership ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a compute allocation membership
+      tags:
+      - Compute Allocation Memberships
+    get:
+      parameters:
+      - description: Membership ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationMembership'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute allocation membership
+      tags:
+      - Compute Allocation Memberships
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Membership ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Membership payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationMembership'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationMembership'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a compute allocation membership
+      tags:
+      - Compute Allocation Memberships
+  /compute-allocation-memberships/{id}/resource-overrides:
+    get:
+      parameters:
+      - description: Membership ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationMembershipResourceOverride'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List resource overrides for a membership
+      tags:
+      - Membership Resource Overrides
+  /compute-allocation-memberships/{id}/status:
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Membership ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Status patch
+        in: body
+        name: request
+        required: true
+        schema:
+          properties:
+            membership_status:
+              $ref: '#/definitions/models.AllocationStatus'
+          type: object
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationMembership'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a membership's status
+      tags:
+      - Compute Allocation Memberships
+  /compute-allocation-resource-rates:
+    post:
+      consumes:
+      - application/json
+      description: Records the SU rate (e.g. SU per CPU-hour) for a resource over
+        a time window.
+      parameters:
+      - description: Rate payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationResourceRate'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationResourceRate'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute allocation resource rate
+      tags:
+      - Compute Allocation Resource Rates
+  /compute-allocation-resource-rates/{id}:
+    get:
+      parameters:
+      - description: Rate ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationResourceRate'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute allocation resource rate
+      tags:
+      - Compute Allocation Resource Rates
+  /compute-allocation-resources:
+    get:
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationResource'
+            type: array
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List all compute allocation resources
+      tags:
+      - Compute Allocation Resources
+    post:
+      consumes:
+      - application/json
+      description: Defines a resource (partition) — e.g. a CPU or GPU partition that
+        allocations can be attached to.
+      parameters:
+      - description: Resource payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationResource'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationResource'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute allocation resource
+      tags:
+      - Compute Allocation Resources
+  /compute-allocation-resources/{id}:
+    get:
+      parameters:
+      - description: Resource ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationResource'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute allocation resource
+      tags:
+      - Compute Allocation Resources
+  /compute-allocation-resources/{id}/allocations:
+    get:
+      parameters:
+      - description: Compute allocation resource ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationResourceMapping'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List compute allocations attached to a resource
+      tags:
+      - Compute Allocation Resources
+  /compute-allocation-resources/{id}/membership-overrides:
+    get:
+      parameters:
+      - description: Compute allocation resource ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationMembershipResourceOverride'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List membership overrides referencing a resource
+      tags:
+      - Membership Resource Overrides
+  /compute-allocation-resources/{id}/rates:
+    get:
+      parameters:
+      - description: Compute allocation resource ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationResourceRate'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List rate history for a resource
+      tags:
+      - Compute Allocation Resource Rates
+  /compute-allocation-resources/{id}/rates/effective:
+    get:
+      description: Returns the rate whose `[start_time, end_time)` window contains
+        the `at` timestamp. Defaults to now when `at` is omitted.
+      parameters:
+      - description: Compute allocation resource ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: RFC3339 time; defaults to now
+        in: query
+        name: at
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationResourceRate'
+        "400":
+          description: Invalid 'at' query parameter
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get the effective rate for a resource at a given time
+      tags:
+      - Compute Allocation Resource Rates
+  /compute-allocation-usages:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Usage payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocationUsage'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationUsage'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute allocation usage record
+      tags:
+      - Compute Allocation Usages
+  /compute-allocation-usages/{id}:
+    delete:
+      parameters:
+      - description: Usage ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a compute allocation usage record
+      tags:
+      - Compute Allocation Usages
+    get:
+      parameters:
+      - description: Usage ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationUsage'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute allocation usage record
+      tags:
+      - Compute Allocation Usages
+  /compute-allocations:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Compute allocation payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeAllocation'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocation'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute allocation
+      tags:
+      - Compute Allocations
+  /compute-allocations/{id}:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocation'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute allocation by ID
+      tags:
+      - Compute Allocations
+  /compute-allocations/{id}/change-requests:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationChangeRequest'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List change requests for a compute allocation
+      tags:
+      - Compute Allocation Change Requests
+  /compute-allocations/{id}/diffs:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationDiff'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List diffs for a compute allocation
+      tags:
+      - Compute Allocation Diffs
+  /compute-allocations/{id}/diffs/latest:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationDiff'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get the most recent diff for a compute allocation
+      tags:
+      - Compute Allocation Diffs
+  /compute-allocations/{id}/memberships:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationMembership'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List members of a compute allocation
+      tags:
+      - Compute Allocation Memberships
+  /compute-allocations/{id}/resources:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationResourceMapping'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List resources attached to a compute allocation
+      tags:
+      - Compute Allocation Resources
+    post:
+      consumes:
+      - application/json
+      description: Creates a mapping between a compute allocation and a resource (partition)
+        with a specific amount and wall-clock time grant.
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Attach payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/server.attachResourceRequest'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationResourceMapping'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Attach a resource to a compute allocation
+      tags:
+      - Compute Allocation Resources
+  /compute-allocations/{id}/resources/{resourceId}:
+    delete:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Compute allocation resource ID
+        in: path
+        name: resourceId
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Detach a resource from a compute allocation
+      tags:
+      - Compute Allocation Resources
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Compute allocation resource ID
+        in: path
+        name: resourceId
+        required: true
+        type: string
+      - description: Mapping update payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/server.updateAllocationResourceMappingRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeAllocationResourceMapping'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a compute allocation -> resource mapping
+      tags:
+      - Compute Allocation Resources
+  /compute-allocations/{id}/usages:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationUsage'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List usages for a compute allocation
+      tags:
+      - Compute Allocation Usages
+  /compute-allocations/{id}/usages/total:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            properties:
+              compute_allocation_id:
+                type: string
+              total_su_amount:
+                type: integer
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get total SU usage for a compute allocation
+      tags:
+      - Compute Allocation Usages
+  /compute-allocations/{id}/users/{userId}/usages/total:
+    get:
+      parameters:
+      - description: Compute allocation ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: User ID
+        in: path
+        name: userId
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            properties:
+              compute_allocation_id:
+                type: string
+              total_su_amount:
+                type: integer
+              user_id:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get total SU usage for a user within a compute allocation
+      tags:
+      - Compute Allocation Usages
+  /compute-cluster-users:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Cluster user payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeClusterUser'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeClusterUser'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute cluster user
+      tags:
+      - Compute Cluster Users
+  /compute-cluster-users/{id}:
+    delete:
+      parameters:
+      - description: Compute cluster user ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a compute cluster user
+      tags:
+      - Compute Cluster Users
+    get:
+      parameters:
+      - description: Compute cluster user ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeClusterUser'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute cluster user by ID
+      tags:
+      - Compute Cluster Users
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Compute cluster user ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Cluster user payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeClusterUser'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeClusterUser'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a compute cluster user
+      tags:
+      - Compute Cluster Users
+  /compute-clusters:
+    get:
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeCluster'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List compute clusters
+      tags:
+      - Compute Clusters
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Cluster payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.ComputeCluster'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.ComputeCluster'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a compute cluster
+      tags:
+      - Compute Clusters
+  /compute-clusters/{id}:
+    get:
+      parameters:
+      - description: Compute cluster ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeCluster'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute cluster by ID
+      tags:
+      - Compute Clusters
+  /compute-clusters/{id}/users:
+    get:
+      parameters:
+      - description: Compute cluster ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeClusterUser'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List users on a compute cluster
+      tags:
+      - Compute Cluster Users
+  /compute-clusters/{id}/users/{userId}:
+    get:
+      parameters:
+      - description: Compute cluster ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: User ID
+        in: path
+        name: userId
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ComputeClusterUser'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a compute cluster user by (cluster, user) pair
+      tags:
+      - Compute Cluster Users
+  /organizations:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Organization payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.Organization'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.Organization'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create an organization
+      tags:
+      - Organizations
+  /organizations/{id}:
+    get:
+      parameters:
+      - description: Organization ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Organization'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get an organization by ID
+      tags:
+      - Organizations
+  /privileges/{key}/holders:
+    get:
+      description: Role-derived holders are NOT listed here.
+      parameters:
+      - description: Privilege key
+        enum:
+        - amie:read
+        - amie:write
+        - hpc:read
+        - hpc:write
+        - signer:read
+        - signer:write
+        - privileges:grant
+        - roles:manage
+        in: path
+        name: key
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.UserPrivilege'
+            type: array
+        "400":
+          description: Unknown privilege key
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List direct holders of a privilege
+      tags:
+      - Privileges
+  /privileges/catalog:
+    get:
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.PrivilegeKey'
+            type: array
+        "401":
+          description: Missing caller header
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "403":
+          description: Caller lacks privileges:grant
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List the declared privilege catalog
+      tags:
+      - Privileges
+  /projects:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Project payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.Project'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.Project'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a project
+      tags:
+      - Projects
+  /projects/{id}:
+    get:
+      parameters:
+      - description: Project ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Project'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a project by ID
+      tags:
+      - Projects
+  /projects/{id}/status:
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Project ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Status patch
+        in: body
+        name: request
+        required: true
+        schema:
+          properties:
+            status:
+              $ref: '#/definitions/models.ProjectStatus'
+          type: object
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Project'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a project's status
+      tags:
+      - Projects
+  /roles:
+    get:
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.Role'
+            type: array
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "403":
+          description: Caller lacks roles:manage
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List all roles
+      tags:
+      - Roles
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Role payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/server.createRoleRequest'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.Role'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "409":
+          description: Role name collides
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a role
+      tags:
+      - Roles
+  /roles/{id}:
+    delete:
+      description: System roles cannot be deleted. CASCADE drops every assignment
+        of this role.
+      parameters:
+      - description: Role ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "400":
+          description: System role / unknown role
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a role
+      tags:
+      - Roles
+    get:
+      parameters:
+      - description: Role ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            properties:
+              privileges:
+                items:
+                  $ref: '#/definitions/models.PrivilegeKey'
+                type: array
+              role:
+                $ref: '#/definitions/models.Role'
+            type: object
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a role with its privilege bundle
+      tags:
+      - Roles
+    put:
+      consumes:
+      - application/json
+      description: System roles cannot be renamed.
+      parameters:
+      - description: Role ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Role patch
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/server.updateRoleRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Role'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update role name / description
+      tags:
+      - Roles
+  /roles/{id}/holders:
+    get:
+      parameters:
+      - description: Role ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.UserRole'
+            type: array
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List users holding the role
+      tags:
+      - Roles
+  /roles/{id}/privileges:
+    post:
+      consumes:
+      - application/json
+      description: The new privilege propagates to every current holder.
+      parameters:
+      - description: Role ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Privilege key
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/server.rolePrivilegeRequest'
+      responses:
+        "204":
+          description: No Content
+        "400":
+          description: Unknown key
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "409":
+          description: Role already carries that privilege
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Add a privilege to a role
+      tags:
+      - Roles
+  /roles/{id}/privileges/{key}:
+    delete:
+      description: Removal propagates to every holder. Refuses to remove `privileges:grant`
+        or `roles:manage` if that would leave no role anywhere carrying it.
+      parameters:
+      - description: Role ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Privilege key
+        enum:
+        - amie:read
+        - amie:write
+        - hpc:read
+        - hpc:write
+        - signer:read
+        - signer:write
+        - privileges:grant
+        - roles:manage
+        in: path
+        name: key
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "400":
+          description: Last-source guard or unknown key
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Role does not carry that privilege
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Remove a privilege from a role
+      tags:
+      - Roles
+  /user-identities:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Identity payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.UserIdentity'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.UserIdentity'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a user identity
+      tags:
+      - User Identities
+  /user-identities/{id}:
+    delete:
+      parameters:
+      - description: Identity ID
+        in: path
+        name: id
+        required: true
+        type: string
+      responses:
+        "204":
+          description: No Content
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Delete a user identity
+      tags:
+      - User Identities
+    get:
+      parameters:
+      - description: Identity ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.UserIdentity'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a user identity
+      tags:
+      - User Identities
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Identity ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Identity payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.UserIdentity'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.UserIdentity'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a user identity
+      tags:
+      - User Identities
+  /user-identities/oidc-subjects/{oidcSub}:
+    get:
+      parameters:
+      - description: OIDC subject claim
+        in: path
+        name: oidcSub
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.UserIdentity'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a user identity by its OIDC subject claim
+      tags:
+      - User Identities
+  /user-identities/sources/{source}/external/{externalId}:
+    get:
+      parameters:
+      - description: Identity source
+        in: path
+        name: source
+        required: true
+        type: string
+      - description: Identity's external ID at that source
+        in: path
+        name: externalId
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.UserIdentity'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a user identity by source and external ID
+      tags:
+      - User Identities
+  /user/privileges:
+    get:
+      description: Effective set is direct grants UNION every privilege carried by
+        every role the caller holds.
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: Effective privilege set
+          schema:
+            properties:
+              privileges:
+                items:
+                  $ref: '#/definitions/models.PrivilegeKey'
+                type: array
+            type: object
+        "401":
+          description: Missing X-Custos-User-Id header
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "503":
+          description: Auth lookup failed
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get caller's effective privileges
+      tags:
+      - Caller
+  /users:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: User payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/models.User'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.User'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Create a user
+      tags:
+      - Users
+  /users/{id}:
+    get:
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.User'
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Get a user by ID
+      tags:
+      - Users
+  /users/{id}/change-requests:
+    get:
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationChangeRequest'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List change requests submitted by a user
+      tags:
+      - Compute Allocation Change Requests
+  /users/{id}/compute-allocation-memberships:
+    get:
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationMembership'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List a user's compute allocation memberships
+      tags:
+      - Compute Allocation Memberships
+  /users/{id}/compute-allocation-usages:
+    get:
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeAllocationUsage'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List usages submitted by a user
+      tags:
+      - Compute Allocation Usages
+  /users/{id}/compute-cluster-users:
+    get:
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ComputeClusterUser'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List compute cluster users for a user
+      tags:
+      - Compute Cluster Users
+  /users/{id}/privileges:
+    get:
+      description: Returns DIRECT grants only (not role-derived). Combine with `GET
+        /users/{id}/roles` for the full picture.
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.UserPrivilege'
+            type: array
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "403":
+          description: Caller lacks privileges:grant
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List a user's direct privilege grants
+      tags:
+      - Privileges
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Grant payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/server.grantPrivilegeRequest'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.UserPrivilege'
+        "400":
+          description: Bad request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Missing caller header
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "403":
+          description: Caller lacks privileges:grant
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "409":
+          description: Privilege already active for user
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Grant a direct privilege to a user
+      tags:
+      - Privileges
+  /users/{id}/privileges/{key}:
+    delete:
+      consumes:
+      - application/json
+      description: '`privileges:grant` itself cannot be self-revoked or revoked from
+        the last holder.'
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Privilege key
+        enum:
+        - amie:read
+        - amie:write
+        - hpc:read
+        - hpc:write
+        - signer:read
+        - signer:write
+        - privileges:grant
+        - roles:manage
+        in: path
+        name: key
+        required: true
+        type: string
+      - description: Optional reason
+        in: body
+        name: request
+        schema:
+          $ref: '#/definitions/server.revokePrivilegeRequest'
+      responses:
+        "204":
+          description: No Content
+        "400":
+          description: Self-revoke meta, last-holder, or unknown key
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Missing caller header
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "403":
+          description: Caller lacks privileges:grant
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: No active grant for that key
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Revoke a direct privilege from a user
+      tags:
+      - Privileges
+  /users/{id}/roles:
+    get:
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.UserRole'
+            type: array
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List roles a user holds
+      tags:
+      - Role Assignments
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Role assignment payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/server.grantRoleRequest'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/models.UserRole'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "409":
+          description: User already holds that role
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Grant a role to a user
+      tags:
+      - Role Assignments
+  /users/{id}/roles/{roleId}:
+    delete:
+      consumes:
+      - application/json
+      description: Refuses if revoking would leave no holder of `privileges:grant`
+        or `roles:manage` anywhere (last-meta-holder guard).
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Role ID
+        in: path
+        name: roleId
+        required: true
+        type: string
+      - description: Optional reason
+        in: body
+        name: request
+        schema:
+          $ref: '#/definitions/server.revokeRoleRequest'
+      responses:
+        "204":
+          description: No Content
+        "400":
+          description: Last-meta-holder guard
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "401":
+          description: Unauthorized
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: User does not hold that role
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Revoke a role from a user
+      tags:
+      - Role Assignments
+  /users/{id}/status:
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: Status patch
+        in: body
+        name: request
+        required: true
+        schema:
+          properties:
+            status:
+              $ref: '#/definitions/models.UserStatus'
+          type: object
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.User'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Update a user's status
+      tags:
+      - Users
+  /users/{id}/user-identities:
+    get:
+      parameters:
+      - description: User ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.UserIdentity'
+            type: array
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List a user's identities
+      tags:
+      - User Identities
+  /users/merge:
+    post:
+      consumes:
+      - application/json
+      description: Merges the retiring user into the surviving user; the surviving
+        record is returned.
+      parameters:
+      - description: Merge payload
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/server.mergeUsersRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.User'
+        "400":
+          description: Bad Request
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "404":
+          description: Not Found
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: Merge two users
+      tags:
+      - Users
+securityDefinitions:
+  CustosUserHeader:
+    in: header
+    name: X-Custos-User-Id
+    type: apiKey
+swagger: "2.0"
diff --git a/cmd/server/main.go b/cmd/server/main.go
index c1c94ea44..318dffcbb 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -18,6 +18,9 @@
 // Command server starts the Custos HTTP API.
 package main
 
+//go:generate go run github.com/swaggo/swag/cmd/swag init -g main.go -d .,../../internal/server,../../pkg/models -o ../../api --outputTypes yaml --parseDependency
+//go:generate mv ../../api/swagger.yaml ../../api/core.openapi.yaml
+
 import (
 	"context"
 	"errors"
@@ -39,6 +42,15 @@ import (
 	"github.com/apache/airavata-custos/pkg/service"
 )
 
+// @title	Apache Custos Core API
+// @version	0.2.0
+// @description	REST API for Apache Custos: organizations, users, projects, compute clusters and allocations, audit traces, and the privilege/role authorization layer. Authenticate every request with the X-Custos-User-Id header. Connector endpoints (/connectors/<name>/...) are documented in separate specs.
+// @host	localhost:8080
+// @BasePath	/
+// @securityDefinitions.apikey	CustosUserHeader
+// @in	header
+// @name	X-Custos-User-Id
+// @description.CustosUserHeader	Identifies the calling user. Dev: dev-admin, dev-operator, dev-auditor, dev-researcher (see dev-ops/compose/seeds/dev_users_and_roles.sql).
 func main() {
 	slog.SetDefault(slog.New(tracing.SlogHandler(slog.NewJSONHandler(os.Stdout, nil))))
 
diff --git a/connectors/ACCESS/AMIE-Processor/api/amie.openapi.yaml b/connectors/ACCESS/AMIE-Processor/api/amie.openapi.yaml
new file mode 100644
index 000000000..4828073c1
--- /dev/null
+++ b/connectors/ACCESS/AMIE-Processor/api/amie.openapi.yaml
@@ -0,0 +1,81 @@
+basePath: /
+host: localhost:8080
+info:
+  contact: {}
+  description: REST endpoints for the ACCESS-CI AMIE connector, all under /connectors/amie/.
+  title: AMIE Connector API
+  version: 0.1.0
+paths:
+  /connectors/amie/packets/{packet_id}/audits:
+    get:
+      parameters:
+      - description: AMIE packet ID
+        in: path
+        name: packet_id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            properties:
+              events:
+                items:
+                  properties:
+                    created_at:
+                      type: string
+                    description:
+                      type: string
+                    entity_id:
+                      type: string
+                    entity_type:
+                      type: string
+                    event_type:
+                      type: string
+                    parent_span_id:
+                      type: string
+                    source:
+                      type: string
+                    span_id:
+                      type: string
+                    status:
+                      type: string
+                  type: object
+                type: array
+              packet_id:
+                type: string
+            type: object
+        "400":
+          description: packet_id is required
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "500":
+          description: Store lookup failed
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+        "503":
+          description: AMIE packet audit store not configured
+          schema:
+            properties:
+              error:
+                type: string
+            type: object
+      security:
+      - CustosUserHeader: []
+      summary: List audit events for an AMIE packet
+      tags:
+      - AMIE Audit
+securityDefinitions:
+  CustosUserHeader:
+    in: header
+    name: X-Custos-User-Id
+    type: apiKey
+swagger: "2.0"
diff --git a/connectors/ACCESS/AMIE-Processor/pkg/amie/loader.go b/connectors/ACCESS/AMIE-Processor/pkg/amie/loader.go
index 03e35f7c7..7363a8bfa 100644
--- a/connectors/ACCESS/AMIE-Processor/pkg/amie/loader.go
+++ b/connectors/ACCESS/AMIE-Processor/pkg/amie/loader.go
@@ -18,6 +18,9 @@
 // Package amie wires the ACCESS-AMIE connector into the core binary.
 package amie
 
+//go:generate go run github.com/swaggo/swag/cmd/swag init -g loader.go -d .,../../server,../../store -o ../../api --outputTypes yaml --parseDependency
+//go:generate mv ../../api/swagger.yaml ../../api/amie.openapi.yaml
+
 import (
 	"context"
 	"log/slog"
@@ -54,6 +57,15 @@ const connectorName = "amie"
 // LoadConnector skips silently when AMIE_BASE_URL / AMIE_SITE_CODE /
 // AMIE_API_KEY are not all set. When enabled, it attaches /connectors/amie/*
 // endpoints to mux.
+//
+// @title	AMIE Connector API
+// @version	0.1.0
+// @description	REST endpoints for the ACCESS-CI AMIE connector, all under /connectors/amie/.
+// @host	localhost:8080
+// @BasePath	/
+// @securityDefinitions.apikey	CustosUserHeader
+// @in	header
+// @name	X-Custos-User-Id
 func LoadConnector(ctx context.Context, database *sqlx.DB, eventBus *events.Bus, coreService *coreservice.Service, wg *sync.WaitGroup, mux *http.ServeMux) error {
 	cfg := loadConfig()
 	if cfg.AMIE.APIKey == "" || cfg.AMIE.BaseURL == "" || cfg.AMIE.SiteCode == "" {
diff --git a/connectors/ACCESS/AMIE-Processor/server/handlers.go b/connectors/ACCESS/AMIE-Processor/server/handlers.go
index 5e675069d..408dda313 100644
--- a/connectors/ACCESS/AMIE-Processor/server/handlers.go
+++ b/connectors/ACCESS/AMIE-Processor/server/handlers.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-// Package server exposes the AMIE connector's HTTP read surface.
+// Package server exposes the AMIE connector's REST read endpoints.
 package server
 
 import (
@@ -40,8 +40,16 @@ func (h *Handlers) RegisterRoutes(mux *http.ServeMux) {
 	mux.HandleFunc("GET /connectors/amie/packets/{packet_id}/audits", h.listPacketAudits)
 }
 
-// listPacketAudits returns every audit_events row written for one AMIE packet,
-// joined through amie_audit_extras.
+// @Summary	List audit events for an AMIE packet
+// @Tags	AMIE Audit
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	packet_id	path	string	true	"AMIE packet ID"
+// @Success	200	{object}	object{packet_id=string,events=[]object{span_id=string,parent_span_id=string,source=string,event_type=string,entity_type=string,entity_id=string,description=string,status=string,created_at=string}}
+// @Failure	400	{object}	object{error=string}	"packet_id is required"
+// @Failure	500	{object}	object{error=string}	"Store lookup failed"
+// @Failure	503	{object}	object{error=string}	"AMIE packet audit store not configured"
+// @Router	/connectors/amie/packets/{packet_id}/audits [get]
 func (h *Handlers) listPacketAudits(w http.ResponseWriter, r *http.Request) {
 	if h.audits == nil {
 		writeError(w, http.StatusServiceUnavailable, errors.New("amie packet audit store not configured"))
diff --git a/go.mod b/go.mod
index dc178be85..9da28975c 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
 	github.com/prometheus/client_golang v1.23.2
 	github.com/prometheus/client_model v0.6.2
 	github.com/stretchr/testify v1.11.1
+	github.com/swaggo/swag v1.16.6
 	go.opentelemetry.io/otel v1.41.0
 	go.opentelemetry.io/otel/sdk v1.41.0
 	go.opentelemetry.io/otel/trace v1.41.0
@@ -19,18 +20,37 @@ require (
 
 require (
 	filippo.io/edwards25519 v1.1.1 // indirect
+	github.com/KyleBanks/depth v1.2.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.21.0 // indirect
+	github.com/go-openapi/spec v0.20.4 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/common v0.66.1 // indirect
 	github.com/prometheus/procfs v0.16.1 // indirect
+	github.com/russross/blackfriday/v2 v2.0.1 // indirect
+	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
+	github.com/urfave/cli/v2 v2.3.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
 	go.opentelemetry.io/otel/metric v1.41.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
+	golang.org/x/mod v0.29.0 // indirect
+	golang.org/x/sync v0.18.0 // indirect
 	golang.org/x/sys v0.41.0 // indirect
+	golang.org/x/text v0.31.0 // indirect
+	golang.org/x/tools v0.38.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	sigs.k8s.io/yaml v1.3.0 // indirect
 )
+
+tool github.com/swaggo/swag/cmd/swag
diff --git a/go.sum b/go.sum
index 34b9608f5..fc3195913 100644
--- a/go.sum
+++ b/go.sum
@@ -3,8 +3,13 @@ filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
 filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -13,6 +18,11 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
 github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
 github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
@@ -32,6 +42,19 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -44,12 +67,22 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
 github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -60,12 +93,14 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
 github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@@ -78,10 +113,21 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
 github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
+github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
+github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
+github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
@@ -100,12 +146,37 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
 go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
 golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
 google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
 google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/internal/server/audit_handlers.go b/internal/server/audit_handlers.go
index db884a966..e3ba7a450 100644
--- a/internal/server/audit_handlers.go
+++ b/internal/server/audit_handlers.go
@@ -45,8 +45,22 @@ func (s *Server) requireAuditStore(w http.ResponseWriter) (store.AuditTraceStore
 	return s.admin.AuditTraces, true
 }
 
-// `q` matches trace_id hex prefix OR action substring. `total` is
-// pre-status-filter so the response may have fewer rows than `total` indicates.
+// @Summary	List audit traces with filters
+// @Description	One row per distinct trace_id. `q` matches trace_id hex prefix or root-operation substring.
+// @Tags	Audit
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	source	query	[]string	false	"Restrict to listed sources (repeatable)"	collectionFormat(multi)
+// @Param	status	query	[]string	false	"ok | error | in_progress (repeatable)"	collectionFormat(multi)
+// @Param	from	query	string	false	"RFC3339 lower bound on started_at"
+// @Param	to	query	string	false	"RFC3339 upper bound on started_at"
+// @Param	q	query	string	false	"Substring match on trace_id hex or root operation"
+// @Param	limit	query	integer	false	"Page size (default 50, max 200)"
+// @Param	offset	query	integer	false	"Page offset (default 0)"
+// @Success	200	{object}	object{traces=[]models.TraceSummary,total=integer,limit=integer,offset=integer}
+// @Failure	400	{object}	object{error=string}	"Invalid filter value"
+// @Failure	503	{object}	object{error=string}	"Audit trace store not configured"
+// @Router	/audit/traces [get]
 func (s *Server) handleListTraces(w http.ResponseWriter, r *http.Request) {
 	ts, ok := s.requireAuditStore(w)
 	if !ok {
@@ -70,6 +84,16 @@ func (s *Server) handleListTraces(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
+// @Summary	Get a trace's nested span tree
+// @Tags	Audit
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	trace_id	path	string	true	"32-char lowercase hex"
+// @Success	200	{object}	object{trace_id=string,tree=[]models.TraceNode,truncated=boolean}
+// @Failure	400	{object}	object{error=string}	"Malformed trace_id"
+// @Failure	404	{object}	object{error=string}	"Trace not found"
+// @Failure	503	{object}	object{error=string}	"Audit trace store not configured"
+// @Router	/audit/traces/{trace_id} [get]
 func (s *Server) handleGetTrace(w http.ResponseWriter, r *http.Request) {
 	ts, ok := s.requireAuditStore(w)
 	if !ok {
@@ -98,6 +122,16 @@ func (s *Server) handleGetTrace(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, resp)
 }
 
+// @Summary	List audit events for a trace (optionally one span)
+// @Tags	Audit
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	trace_id	query	string	true	"32-char lowercase hex"
+// @Param	span_id	query	string	false	"16-char lowercase hex"
+// @Success	200	{object}	object{events=[]models.TraceEvent}
+// @Failure	400	{object}	object{error=string}	"Malformed trace_id or span_id"
+// @Failure	503	{object}	object{error=string}	"Audit trace store not configured"
+// @Router	/audit/events [get]
 func (s *Server) handleListEvents(w http.ResponseWriter, r *http.Request) {
 	ts, ok := s.requireAuditStore(w)
 	if !ok {
@@ -128,6 +162,13 @@ func (s *Server) handleListEvents(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, map[string]any{"events": events})
 }
 
+// @Summary	List distinct audit-event sources
+// @Tags	Audit
+// @Security	CustosUserHeader
+// @Produce	json
+// @Success	200	{object}	object{sources=[]string}
+// @Failure	503	{object}	object{error=string}	"Audit trace store not configured"
+// @Router	/audit/sources [get]
 func (s *Server) handleListSources(w http.ResponseWriter, r *http.Request) {
 	ts, ok := s.requireAuditStore(w)
 	if !ok {
diff --git a/internal/server/privilege.go b/internal/server/privilege.go
index b882e6932..e33b1acb1 100644
--- a/internal/server/privilege.go
+++ b/internal/server/privilege.go
@@ -24,8 +24,15 @@ import (
 	"github.com/apache/airavata-custos/pkg/models"
 )
 
-// getCallerPrivileges handles GET /user/privileges. Returns the
-// authenticated caller's effective privilege set.
+// @Summary	Get caller's effective privileges
+// @Description	Effective set is direct grants UNION every privilege carried by every role the caller holds.
+// @Tags	Caller
+// @Security	CustosUserHeader
+// @Produce	json
+// @Success	200	{object}	object{privileges=[]models.PrivilegeKey}	"Effective privilege set"
+// @Failure	401	{object}	object{error=string}	"Missing X-Custos-User-Id header"
+// @Failure	503	{object}	object{error=string}	"Auth lookup failed"
+// @Router	/user/privileges [get]
 func (s *Server) getCallerPrivileges(w http.ResponseWriter, r *http.Request) {
 	callerID := r.Header.Get(callerHeader)
 	if callerID == "" {
@@ -44,14 +51,28 @@ func (s *Server) getCallerPrivileges(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, map[string]any{"privileges": keys})
 }
 
-// getPrivilegeCatalog handles GET /privileges/catalog. Returns the static
-// catalog of declared privilege keys. Gated on privileges:grant.
+// @Summary	List the declared privilege catalog
+// @Tags	Privileges
+// @Security	CustosUserHeader
+// @Produce	json
+// @Success	200	{array}	models.PrivilegeKey
+// @Failure	401	{object}	object{error=string}	"Missing caller header"
+// @Failure	403	{object}	object{error=string}	"Caller lacks privileges:grant"
+// @Router	/privileges/catalog [get]
 func (s *Server) getPrivilegeCatalog(w http.ResponseWriter, _ *http.Request) {
 	writeJSON(w, http.StatusOK, s.svc.PrivilegeCatalog())
 }
 
-// listUserPrivileges handles GET /users/{id}/privileges. Returns the active
-// privileges of the target user. Gated on privileges:grant.
+// @Summary	List a user's direct privilege grants
+// @Description	Returns DIRECT grants only (not role-derived). Combine with `GET /users/{id}/roles` for the full picture.
+// @Tags	Privileges
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Success	200	{array}	models.UserPrivilege
+// @Failure	400	{object}	object{error=string}
+// @Failure	403	{object}	object{error=string}	"Caller lacks privileges:grant"
+// @Router	/users/{id}/privileges [get]
 func (s *Server) listUserPrivileges(w http.ResponseWriter, r *http.Request) {
 	userID := r.PathValue("id")
 	if userID == "" {
@@ -66,8 +87,15 @@ func (s *Server) listUserPrivileges(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, rows)
 }
 
-// listPrivilegeHolders handles GET /privileges/{key}/holders. Gated on
-// privileges:grant.
+// @Summary	List direct holders of a privilege
+// @Description	Role-derived holders are NOT listed here.
+// @Tags	Privileges
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	key	path	models.PrivilegeKey	true	"Privilege key"
+// @Success	200	{array}	models.UserPrivilege
+// @Failure	400	{object}	object{error=string}	"Unknown privilege key"
+// @Router	/privileges/{key}/holders [get]
 func (s *Server) listPrivilegeHolders(w http.ResponseWriter, r *http.Request) {
 	key := models.PrivilegeKey(r.PathValue("key"))
 	if !models.IsKnownPrivilege(key) {
@@ -87,7 +115,19 @@ type grantPrivilegeRequest struct {
 	Reason    string              `json:"reason"`
 }
 
-// grantPrivilege handles POST /users/{id}/privileges. Gated on privileges:grant.
+// @Summary	Grant a direct privilege to a user
+// @Tags	Privileges
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Param	request	body	grantPrivilegeRequest	true	"Grant payload"
+// @Success	201	{object}	models.UserPrivilege
+// @Failure	400	{object}	object{error=string}	"Bad request"
+// @Failure	401	{object}	object{error=string}	"Missing caller header"
+// @Failure	403	{object}	object{error=string}	"Caller lacks privileges:grant"
+// @Failure	409	{object}	object{error=string}	"Privilege already active for user"
+// @Router	/users/{id}/privileges [post]
 func (s *Server) grantPrivilege(w http.ResponseWriter, r *http.Request) {
 	userID := r.PathValue("id")
 	if userID == "" {
@@ -117,8 +157,20 @@ type revokePrivilegeRequest struct {
 	Reason string `json:"reason"`
 }
 
-// revokePrivilege handles DELETE /users/{id}/privileges/{key}. Gated on
-// privileges:grant.
+// @Summary	Revoke a direct privilege from a user
+// @Description	`privileges:grant` itself cannot be self-revoked or revoked from the last holder.
+// @Tags	Privileges
+// @Security	CustosUserHeader
+// @Accept	json
+// @Param	id	path	string	true	"User ID"
+// @Param	key	path	models.PrivilegeKey	true	"Privilege key"
+// @Param	request	body	revokePrivilegeRequest	false	"Optional reason"
+// @Success	204	"No Content"
+// @Failure	400	{object}	object{error=string}	"Self-revoke meta, last-holder, or unknown key"
+// @Failure	401	{object}	object{error=string}	"Missing caller header"
+// @Failure	403	{object}	object{error=string}	"Caller lacks privileges:grant"
+// @Failure	404	{object}	object{error=string}	"No active grant for that key"
+// @Router	/users/{id}/privileges/{key} [delete]
 func (s *Server) revokePrivilege(w http.ResponseWriter, r *http.Request) {
 	userID := r.PathValue("id")
 	key := models.PrivilegeKey(r.PathValue("key"))
diff --git a/internal/server/role.go b/internal/server/role.go
index 360763417..ee91ea7b7 100644
--- a/internal/server/role.go
+++ b/internal/server/role.go
@@ -24,6 +24,14 @@ import (
 	"github.com/apache/airavata-custos/pkg/models"
 )
 
+// @Summary	List all roles
+// @Tags	Roles
+// @Security	CustosUserHeader
+// @Produce	json
+// @Success	200	{array}	models.Role
+// @Failure	401	{object}	object{error=string}
+// @Failure	403	{object}	object{error=string}	"Caller lacks roles:manage"
+// @Router	/roles [get]
 func (s *Server) listRoles(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListRoles(r.Context())
 	if err != nil {
@@ -33,7 +41,15 @@ func (s *Server) listRoles(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, rows)
 }
 
-// getRole returns the role plus its privilege bundle in one response.
+// @Summary	Get a role with its privilege bundle
+// @Tags	Roles
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Role ID"
+// @Success	200	{object}	object{role=models.Role,privileges=[]models.PrivilegeKey}
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/roles/{id} [get]
 func (s *Server) getRole(w http.ResponseWriter, r *http.Request) {
 	roleID := r.PathValue("id")
 	if roleID == "" {
@@ -57,10 +73,21 @@ func (s *Server) getRole(w http.ResponseWriter, r *http.Request) {
 }
 
 type createRoleRequest struct {
-	Name        string `json:"name"`
-	Description string `json:"description"`
+	Name        string `JSON:"name"`
+	Description string `JSON:"description"`
 }
 
+// @Summary	Create a role
+// @Tags	Roles
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	createRoleRequest	true	"Role payload"
+// @Success	201	{object}	models.Role
+// @Failure	400	{object}	object{error=string}
+// @Failure	401	{object}	object{error=string}
+// @Failure	409	{object}	object{error=string}	"Role name collides"
+// @Router	/roles [post]
 func (s *Server) createRole(w http.ResponseWriter, r *http.Request) {
 	actorID := r.Header.Get(callerHeader)
 	if actorID == "" {
@@ -81,10 +108,22 @@ func (s *Server) createRole(w http.ResponseWriter, r *http.Request) {
 }
 
 type updateRoleRequest struct {
-	Name        string `json:"name"`
-	Description string `json:"description"`
+	Name        string `JSON:"name"`
+	Description string `JSON:"description"`
 }
 
+// @Summary	Update role name / description
+// @Description	System roles cannot be renamed.
+// @Tags	Roles
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Role ID"
+// @Param	request	body	updateRoleRequest	true	"Role patch"
+// @Success	200	{object}	models.Role
+// @Failure	400	{object}	object{error=string}
+// @Failure	401	{object}	object{error=string}
+// @Router	/roles/{id} [put]
 func (s *Server) updateRole(w http.ResponseWriter, r *http.Request) {
 	roleID := r.PathValue("id")
 	if roleID == "" {
@@ -110,6 +149,15 @@ func (s *Server) updateRole(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, role)
 }
 
+// @Summary	Delete a role
+// @Description	System roles cannot be deleted. CASCADE drops every assignment of this role.
+// @Tags	Roles
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Role ID"
+// @Success	204	"No Content"
+// @Failure	400	{object}	object{error=string}	"System role / unknown role"
+// @Failure	401	{object}	object{error=string}
+// @Router	/roles/{id} [delete]
 func (s *Server) deleteRole(w http.ResponseWriter, r *http.Request) {
 	roleID := r.PathValue("id")
 	if roleID == "" {
@@ -130,9 +178,21 @@ func (s *Server) deleteRole(w http.ResponseWriter, r *http.Request) {
 }
 
 type rolePrivilegeRequest struct {
-	Privilege models.PrivilegeKey `json:"privilege"`
+	Privilege models.PrivilegeKey `JSON:"privilege"`
 }
 
+// @Summary	Add a privilege to a role
+// @Description	The new privilege propagates to every current holder.
+// @Tags	Roles
+// @Security	CustosUserHeader
+// @Accept	json
+// @Param	id	path	string	true	"Role ID"
+// @Param	request	body	rolePrivilegeRequest	true	"Privilege key"
+// @Success	204	"No Content"
+// @Failure	400	{object}	object{error=string}	"Unknown key"
+// @Failure	401	{object}	object{error=string}
+// @Failure	409	{object}	object{error=string}	"Role already carries that privilege"
+// @Router	/roles/{id}/privileges [post]
 func (s *Server) addRolePrivilege(w http.ResponseWriter, r *http.Request) {
 	roleID := r.PathValue("id")
 	if roleID == "" {
@@ -157,6 +217,17 @@ func (s *Server) addRolePrivilege(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	Remove a privilege from a role
+// @Description	Removal propagates to every holder. Refuses to remove `privileges:grant` or `roles:manage` if that would leave no role anywhere carrying it.
+// @Tags	Roles
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Role ID"
+// @Param	key	path	models.PrivilegeKey	true	"Privilege key"
+// @Success	204	"No Content"
+// @Failure	400	{object}	object{error=string}	"Last-source guard or unknown key"
+// @Failure	401	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}	"Role does not carry that privilege"
+// @Router	/roles/{id}/privileges/{key} [delete]
 func (s *Server) removeRolePrivilege(w http.ResponseWriter, r *http.Request) {
 	roleID := r.PathValue("id")
 	key := models.PrivilegeKey(r.PathValue("key"))
@@ -177,6 +248,14 @@ func (s *Server) removeRolePrivilege(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List roles a user holds
+// @Tags	Role Assignments
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Success	200	{array}	models.UserRole
+// @Failure	400	{object}	object{error=string}
+// @Router	/users/{id}/roles [get]
 func (s *Server) listUserRoles(w http.ResponseWriter, r *http.Request) {
 	userID := r.PathValue("id")
 	if userID == "" {
@@ -191,6 +270,14 @@ func (s *Server) listUserRoles(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	List users holding the role
+// @Tags	Roles
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Role ID"
+// @Success	200	{array}	models.UserRole
+// @Failure	400	{object}	object{error=string}
+// @Router	/roles/{id}/holders [get]
 func (s *Server) listRoleHolders(w http.ResponseWriter, r *http.Request) {
 	roleID := r.PathValue("id")
 	if roleID == "" {
@@ -206,10 +293,22 @@ func (s *Server) listRoleHolders(w http.ResponseWriter, r *http.Request) {
 }
 
 type grantRoleRequest struct {
-	RoleID string `json:"role_id"`
-	Reason string `json:"reason"`
+	RoleID string `JSON:"role_id"`
+	Reason string `JSON:"reason"`
 }
 
+// @Summary	Grant a role to a user
+// @Tags	Role Assignments
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Param	request	body	grantRoleRequest	true	"Role assignment payload"
+// @Success	201	{object}	models.UserRole
+// @Failure	400	{object}	object{error=string}
+// @Failure	401	{object}	object{error=string}
+// @Failure	409	{object}	object{error=string}	"User already holds that role"
+// @Router	/users/{id}/roles [post]
 func (s *Server) grantRoleToUser(w http.ResponseWriter, r *http.Request) {
 	userID := r.PathValue("id")
 	if userID == "" {
@@ -236,9 +335,22 @@ func (s *Server) grantRoleToUser(w http.ResponseWriter, r *http.Request) {
 }
 
 type revokeRoleRequest struct {
-	Reason string `json:"reason"`
+	Reason string `JSON:"reason"`
 }
 
+// @Summary	Revoke a role from a user
+// @Description	Refuses if revoking would leave no holder of `privileges:grant` or `roles:manage` anywhere (last-meta-holder guard).
+// @Tags	Role Assignments
+// @Security	CustosUserHeader
+// @Accept	json
+// @Param	id	path	string	true	"User ID"
+// @Param	roleId	path	string	true	"Role ID"
+// @Param	request	body	revokeRoleRequest	false	"Optional reason"
+// @Success	204	"No Content"
+// @Failure	400	{object}	object{error=string}	"Last-meta-holder guard"
+// @Failure	401	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}	"User does not hold that role"
+// @Router	/users/{id}/roles/{roleId} [delete]
 func (s *Server) revokeRoleFromUser(w http.ResponseWriter, r *http.Request) {
 	userID := r.PathValue("id")
 	roleID := r.PathValue("roleId")
diff --git a/internal/server/server.go b/internal/server/server.go
index 2dace2e82..9dca02b27 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -191,6 +191,15 @@ func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
 	writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
 }
 
+// @Summary	Create an organization
+// @Tags	Organizations
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.Organization	true	"Organization payload"
+// @Success	201	{object}	models.Organization
+// @Failure	400	{object}	object{error=string}
+// @Router	/organizations [post]
 func (s *Server) createOrganization(w http.ResponseWriter, r *http.Request) {
 	var org models.Organization
 	if err := decodeJSON(r, &org); err != nil {
@@ -205,6 +214,14 @@ func (s *Server) createOrganization(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get an organization by ID
+// @Tags	Organizations
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Organization ID"
+// @Success	200	{object}	models.Organization
+// @Failure	404	{object}	object{error=string}
+// @Router	/organizations/{id} [get]
 func (s *Server) getOrganization(w http.ResponseWriter, r *http.Request) {
 	org, err := s.svc.GetOrganization(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -214,6 +231,15 @@ func (s *Server) getOrganization(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, org)
 }
 
+// @Summary	Create a user
+// @Tags	Users
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.User	true	"User payload"
+// @Success	201	{object}	models.User
+// @Failure	400	{object}	object{error=string}
+// @Router	/users [post]
 func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
 	var u models.User
 	if err := decodeJSON(r, &u); err != nil {
@@ -228,6 +254,14 @@ func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a user by ID
+// @Tags	Users
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Success	200	{object}	models.User
+// @Failure	404	{object}	object{error=string}
+// @Router	/users/{id} [get]
 func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
 	u, err := s.svc.GetUser(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -237,6 +271,15 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, u)
 }
 
+// @Summary	Create a project
+// @Tags	Projects
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.Project	true	"Project payload"
+// @Success	201	{object}	models.Project
+// @Failure	400	{object}	object{error=string}
+// @Router	/projects [post]
 func (s *Server) createProject(w http.ResponseWriter, r *http.Request) {
 	var p models.Project
 	if err := decodeJSON(r, &p); err != nil {
@@ -251,6 +294,14 @@ func (s *Server) createProject(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a project by ID
+// @Tags	Projects
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Project ID"
+// @Success	200	{object}	models.Project
+// @Failure	404	{object}	object{error=string}
+// @Router	/projects/{id} [get]
 func (s *Server) getProject(w http.ResponseWriter, r *http.Request) {
 	p, err := s.svc.GetProject(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -260,6 +311,15 @@ func (s *Server) getProject(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, p)
 }
 
+// @Summary	Create a compute cluster
+// @Tags	Compute Clusters
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeCluster	true	"Cluster payload"
+// @Success	201	{object}	models.ComputeCluster
+// @Failure	400	{object}	object{error=string}
+// @Router	/compute-clusters [post]
 func (s *Server) createComputeCluster(w http.ResponseWriter, r *http.Request) {
 	var c models.ComputeCluster
 	if err := decodeJSON(r, &c); err != nil {
@@ -274,6 +334,14 @@ func (s *Server) createComputeCluster(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute cluster by ID
+// @Tags	Compute Clusters
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute cluster ID"
+// @Success	200	{object}	models.ComputeCluster
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-clusters/{id} [get]
 func (s *Server) getComputeCluster(w http.ResponseWriter, r *http.Request) {
 	c, err := s.svc.GetComputeCluster(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -283,6 +351,13 @@ func (s *Server) getComputeCluster(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, c)
 }
 
+// @Summary	List compute clusters
+// @Tags	Compute Clusters
+// @Security	CustosUserHeader
+// @Produce	json
+// @Success	200	{array}	models.ComputeCluster
+// @Failure	500	{object}	object{error=string}
+// @Router	/compute-clusters [get]
 func (s *Server) listComputeClusters(w http.ResponseWriter, r *http.Request) {
 	clusters, err := s.svc.ListComputeClusters(r.Context())
 	if err != nil {
@@ -292,6 +367,15 @@ func (s *Server) listComputeClusters(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, clusters)
 }
 
+// @Summary	Create a compute cluster user
+// @Tags	Compute Cluster Users
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeClusterUser	true	"Cluster user payload"
+// @Success	201	{object}	models.ComputeClusterUser
+// @Failure	400	{object}	object{error=string}
+// @Router	/compute-cluster-users [post]
 func (s *Server) createComputeClusterUser(w http.ResponseWriter, r *http.Request) {
 	var cu models.ComputeClusterUser
 	if err := decodeJSON(r, &cu); err != nil {
@@ -306,6 +390,14 @@ func (s *Server) createComputeClusterUser(w http.ResponseWriter, r *http.Request
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute cluster user by ID
+// @Tags	Compute Cluster Users
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute cluster user ID"
+// @Success	200	{object}	models.ComputeClusterUser
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-cluster-users/{id} [get]
 func (s *Server) getComputeClusterUser(w http.ResponseWriter, r *http.Request) {
 	cu, err := s.svc.GetComputeClusterUser(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -315,6 +407,17 @@ func (s *Server) getComputeClusterUser(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, cu)
 }
 
+// @Summary	Update a compute cluster user
+// @Tags	Compute Cluster Users
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Compute cluster user ID"
+// @Param	request	body	models.ComputeClusterUser	true	"Cluster user payload"
+// @Success	200	{object}	models.ComputeClusterUser
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-cluster-users/{id} [put]
 func (s *Server) updateComputeClusterUser(w http.ResponseWriter, r *http.Request) {
 	var cu models.ComputeClusterUser
 	if err := decodeJSON(r, &cu); err != nil {
@@ -329,6 +432,13 @@ func (s *Server) updateComputeClusterUser(w http.ResponseWriter, r *http.Request
 	writeJSON(w, http.StatusOK, &cu)
 }
 
+// @Summary	Delete a compute cluster user
+// @Tags	Compute Cluster Users
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Compute cluster user ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-cluster-users/{id} [delete]
 func (s *Server) deleteComputeClusterUser(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DeleteComputeClusterUser(r.Context(), r.PathValue("id")); err != nil {
 		writeServiceError(w, err)
@@ -337,6 +447,14 @@ func (s *Server) deleteComputeClusterUser(w http.ResponseWriter, r *http.Request
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List users on a compute cluster
+// @Tags	Compute Cluster Users
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute cluster ID"
+// @Success	200	{array}	models.ComputeClusterUser
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-clusters/{id}/users [get]
 func (s *Server) listComputeClusterUsersByCluster(w http.ResponseWriter, r *http.Request) {
 	users, err := s.svc.ListComputeClusterUsersByCluster(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -346,6 +464,15 @@ func (s *Server) listComputeClusterUsersByCluster(w http.ResponseWriter, r *http
 	writeJSON(w, http.StatusOK, users)
 }
 
+// @Summary	Get a compute cluster user by (cluster, user) pair
+// @Tags	Compute Cluster Users
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute cluster ID"
+// @Param	userId	path	string	true	"User ID"
+// @Success	200	{object}	models.ComputeClusterUser
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-clusters/{id}/users/{userId} [get]
 func (s *Server) getComputeClusterUserByPair(w http.ResponseWriter, r *http.Request) {
 	cu, err := s.svc.GetComputeClusterUserByPair(r.Context(), r.PathValue("id"), r.PathValue("userId"))
 	if err != nil {
@@ -355,6 +482,14 @@ func (s *Server) getComputeClusterUserByPair(w http.ResponseWriter, r *http.Requ
 	writeJSON(w, http.StatusOK, cu)
 }
 
+// @Summary	List compute cluster users for a user
+// @Tags	Compute Cluster Users
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Success	200	{array}	models.ComputeClusterUser
+// @Failure	404	{object}	object{error=string}
+// @Router	/users/{id}/compute-cluster-users [get]
 func (s *Server) listComputeClusterUsersByUser(w http.ResponseWriter, r *http.Request) {
 	users, err := s.svc.ListComputeClusterUsersByUser(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -364,6 +499,15 @@ func (s *Server) listComputeClusterUsersByUser(w http.ResponseWriter, r *http.Re
 	writeJSON(w, http.StatusOK, users)
 }
 
+// @Summary	Create a compute allocation
+// @Tags	Compute Allocations
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocation	true	"Compute allocation payload"
+// @Success	201	{object}	models.ComputeAllocation
+// @Failure	400	{object}	object{error=string}
+// @Router	/compute-allocations [post]
 func (s *Server) createComputeAllocation(w http.ResponseWriter, r *http.Request) {
 	var a models.ComputeAllocation
 	if err := decodeJSON(r, &a); err != nil {
@@ -378,6 +522,14 @@ func (s *Server) createComputeAllocation(w http.ResponseWriter, r *http.Request)
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute allocation by ID
+// @Tags	Compute Allocations
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{object}	models.ComputeAllocation
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id} [get]
 func (s *Server) getComputeAllocation(w http.ResponseWriter, r *http.Request) {
 	a, err := s.svc.GetComputeAllocation(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -387,6 +539,17 @@ func (s *Server) getComputeAllocation(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, a)
 }
 
+// @Summary	Create a compute allocation resource
+// @Description	Defines a resource (partition) — e.g. a CPU or GPU partition that allocations can be attached to.
+// @Tags	Compute Allocation Resources
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocationResource	true	"Resource payload"
+// @Success	201	{object}	models.ComputeAllocationResource
+// @Failure	400	{object}	object{error=string}
+// @Failure	401	{object}	object{error=string}
+// @Router	/compute-allocation-resources [post]
 func (s *Server) createComputeAllocationResource(w http.ResponseWriter, r *http.Request) {
 	var res models.ComputeAllocationResource
 	if err := decodeJSON(r, &res); err != nil {
@@ -401,6 +564,14 @@ func (s *Server) createComputeAllocationResource(w http.ResponseWriter, r *http.
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute allocation resource
+// @Tags	Compute Allocation Resources
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Resource ID"
+// @Success	200	{object}	models.ComputeAllocationResource
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-resources/{id} [get]
 func (s *Server) getComputeAllocationResource(w http.ResponseWriter, r *http.Request) {
 	res, err := s.svc.GetComputeAllocationResource(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -410,6 +581,13 @@ func (s *Server) getComputeAllocationResource(w http.ResponseWriter, r *http.Req
 	writeJSON(w, http.StatusOK, res)
 }
 
+// @Summary	List all compute allocation resources
+// @Tags	Compute Allocation Resources
+// @Security	CustosUserHeader
+// @Produce	json
+// @Success	200	{array}	models.ComputeAllocationResource
+// @Failure	401	{object}	object{error=string}
+// @Router	/compute-allocation-resources [get]
 func (s *Server) listComputeAllocationResources(w http.ResponseWriter, r *http.Request) {
 	resources, err := s.svc.ListComputeAllocationResources(r.Context())
 	if err != nil {
@@ -425,6 +603,18 @@ type attachResourceRequest struct {
 	ResourceTime                int64  `json:"resource_time"`
 }
 
+// @Summary	Attach a resource to a compute allocation
+// @Description	Creates a mapping between a compute allocation and a resource (partition) with a specific amount and wall-clock time grant.
+// @Tags	Compute Allocation Resources
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Param	request	body	attachResourceRequest	true	"Attach payload"
+// @Success	201	{object}	models.ComputeAllocationResourceMapping
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/resources [post]
 func (s *Server) attachResourceToAllocation(w http.ResponseWriter, r *http.Request) {
 	var body attachResourceRequest
 	if err := decodeJSON(r, &body); err != nil {
@@ -444,6 +634,18 @@ type updateAllocationResourceMappingRequest struct {
 	ResourceTime   int64 `json:"resource_time"`
 }
 
+// @Summary	Update a compute allocation -> resource mapping
+// @Tags	Compute Allocation Resources
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Param	resourceId	path	string	true	"Compute allocation resource ID"
+// @Param	request	body	updateAllocationResourceMappingRequest	true	"Mapping update payload"
+// @Success	200	{object}	models.ComputeAllocationResourceMapping
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/resources/{resourceId} [put]
 func (s *Server) updateAllocationResourceMapping(w http.ResponseWriter, r *http.Request) {
 	var body updateAllocationResourceMappingRequest
 	if err := decodeJSON(r, &body); err != nil {
@@ -458,6 +660,14 @@ func (s *Server) updateAllocationResourceMapping(w http.ResponseWriter, r *http.
 	writeJSON(w, http.StatusOK, mapping)
 }
 
+// @Summary	Detach a resource from a compute allocation
+// @Tags	Compute Allocation Resources
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Param	resourceId	path	string	true	"Compute allocation resource ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/resources/{resourceId} [delete]
 func (s *Server) detachResourceFromAllocation(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DetachResourceFromAllocation(r.Context(), r.PathValue("id"), r.PathValue("resourceId")); err != nil {
 		writeServiceError(w, err)
@@ -466,6 +676,14 @@ func (s *Server) detachResourceFromAllocation(w http.ResponseWriter, r *http.Req
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List resources attached to a compute allocation
+// @Tags	Compute Allocation Resources
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{array}	models.ComputeAllocationResourceMapping
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/resources [get]
 func (s *Server) listResourcesForAllocation(w http.ResponseWriter, r *http.Request) {
 	resources, err := s.svc.ListResourcesForAllocation(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -475,6 +693,14 @@ func (s *Server) listResourcesForAllocation(w http.ResponseWriter, r *http.Reque
 	writeJSON(w, http.StatusOK, resources)
 }
 
+// @Summary	List compute allocations attached to a resource
+// @Tags	Compute Allocation Resources
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation resource ID"
+// @Success	200	{array}	models.ComputeAllocationResourceMapping
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-resources/{id}/allocations [get]
 func (s *Server) listAllocationsForResource(w http.ResponseWriter, r *http.Request) {
 	allocs, err := s.svc.ListAllocationsForResource(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -484,6 +710,17 @@ func (s *Server) listAllocationsForResource(w http.ResponseWriter, r *http.Reque
 	writeJSON(w, http.StatusOK, allocs)
 }
 
+// @Summary	Create a compute allocation resource rate
+// @Description	Records the SU rate (e.g. SU per CPU-hour) for a resource over a time window.
+// @Tags	Compute Allocation Resource Rates
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocationResourceRate	true	"Rate payload"
+// @Success	201	{object}	models.ComputeAllocationResourceRate
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-resource-rates [post]
 func (s *Server) createComputeAllocationResourceRate(w http.ResponseWriter, r *http.Request) {
 	var rate models.ComputeAllocationResourceRate
 	if err := decodeJSON(r, &rate); err != nil {
@@ -498,6 +735,14 @@ func (s *Server) createComputeAllocationResourceRate(w http.ResponseWriter, r *h
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute allocation resource rate
+// @Tags	Compute Allocation Resource Rates
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Rate ID"
+// @Success	200	{object}	models.ComputeAllocationResourceRate
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-resource-rates/{id} [get]
 func (s *Server) getComputeAllocationResourceRate(w http.ResponseWriter, r *http.Request) {
 	rate, err := s.svc.GetComputeAllocationResourceRate(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -507,6 +752,14 @@ func (s *Server) getComputeAllocationResourceRate(w http.ResponseWriter, r *http
 	writeJSON(w, http.StatusOK, rate)
 }
 
+// @Summary	List rate history for a resource
+// @Tags	Compute Allocation Resource Rates
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation resource ID"
+// @Success	200	{array}	models.ComputeAllocationResourceRate
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-resources/{id}/rates [get]
 func (s *Server) listRatesForResource(w http.ResponseWriter, r *http.Request) {
 	rates, err := s.svc.ListRatesForResource(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -516,6 +769,17 @@ func (s *Server) listRatesForResource(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, rates)
 }
 
+// @Summary	Get the effective rate for a resource at a given time
+// @Description	Returns the rate whose `[start_time, end_time)` window contains the `at` timestamp. Defaults to now when `at` is omitted.
+// @Tags	Compute Allocation Resource Rates
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation resource ID"
+// @Param	at	query	string	false	"RFC3339 time; defaults to now"
+// @Success	200	{object}	models.ComputeAllocationResourceRate
+// @Failure	400	{object}	object{error=string}	"Invalid 'at' query parameter"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-resources/{id}/rates/effective [get]
 func (s *Server) getEffectiveRateForResource(w http.ResponseWriter, r *http.Request) {
 	var at time.Time
 	if raw := r.URL.Query().Get("at"); raw != "" {
@@ -534,6 +798,17 @@ func (s *Server) getEffectiveRateForResource(w http.ResponseWriter, r *http.Requ
 	writeJSON(w, http.StatusOK, rate)
 }
 
+// @Summary	Create a compute allocation diff
+// @Description	Records a discrete change against a compute allocation (e.g. USAGE_UPDATE or ALLOCATION_STATUS_CHANGE).
+// @Tags	Compute Allocation Diffs
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocationDiff	true	"Diff payload"
+// @Success	201	{object}	models.ComputeAllocationDiff
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-diffs [post]
 func (s *Server) createComputeAllocationDiff(w http.ResponseWriter, r *http.Request) {
 	var diff models.ComputeAllocationDiff
 	if err := decodeJSON(r, &diff); err != nil {
@@ -548,6 +823,14 @@ func (s *Server) createComputeAllocationDiff(w http.ResponseWriter, r *http.Requ
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute allocation diff
+// @Tags	Compute Allocation Diffs
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Diff ID"
+// @Success	200	{object}	models.ComputeAllocationDiff
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-diffs/{id} [get]
 func (s *Server) getComputeAllocationDiff(w http.ResponseWriter, r *http.Request) {
 	diff, err := s.svc.GetComputeAllocationDiff(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -557,6 +840,13 @@ func (s *Server) getComputeAllocationDiff(w http.ResponseWriter, r *http.Request
 	writeJSON(w, http.StatusOK, diff)
 }
 
+// @Summary	Delete a compute allocation diff
+// @Tags	Compute Allocation Diffs
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Diff ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-diffs/{id} [delete]
 func (s *Server) deleteComputeAllocationDiff(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DeleteComputeAllocationDiff(r.Context(), r.PathValue("id")); err != nil {
 		writeServiceError(w, err)
@@ -565,6 +855,14 @@ func (s *Server) deleteComputeAllocationDiff(w http.ResponseWriter, r *http.Requ
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List diffs for a compute allocation
+// @Tags	Compute Allocation Diffs
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{array}	models.ComputeAllocationDiff
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/diffs [get]
 func (s *Server) listDiffsForAllocation(w http.ResponseWriter, r *http.Request) {
 	diffs, err := s.svc.ListDiffsForAllocation(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -574,6 +872,14 @@ func (s *Server) listDiffsForAllocation(w http.ResponseWriter, r *http.Request)
 	writeJSON(w, http.StatusOK, diffs)
 }
 
+// @Summary	Get the most recent diff for a compute allocation
+// @Tags	Compute Allocation Diffs
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{object}	models.ComputeAllocationDiff
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/diffs/latest [get]
 func (s *Server) getLatestDiffForAllocation(w http.ResponseWriter, r *http.Request) {
 	diff, err := s.svc.GetLatestDiffForAllocation(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -583,6 +889,15 @@ func (s *Server) getLatestDiffForAllocation(w http.ResponseWriter, r *http.Reque
 	writeJSON(w, http.StatusOK, diff)
 }
 
+// @Summary	Create a compute allocation change request
+// @Tags	Compute Allocation Change Requests
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocationChangeRequest	true	"Change request payload"
+// @Success	201	{object}	models.ComputeAllocationChangeRequest
+// @Failure	400	{object}	object{error=string}
+// @Router	/compute-allocation-change-requests [post]
 func (s *Server) createComputeAllocationChangeRequest(w http.ResponseWriter, r *http.Request) {
 	var req models.ComputeAllocationChangeRequest
 	if err := decodeJSON(r, &req); err != nil {
@@ -597,6 +912,14 @@ func (s *Server) createComputeAllocationChangeRequest(w http.ResponseWriter, r *
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute allocation change request
+// @Tags	Compute Allocation Change Requests
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Change request ID"
+// @Success	200	{object}	models.ComputeAllocationChangeRequest
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-change-requests/{id} [get]
 func (s *Server) getComputeAllocationChangeRequest(w http.ResponseWriter, r *http.Request) {
 	req, err := s.svc.GetComputeAllocationChangeRequest(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -606,6 +929,17 @@ func (s *Server) getComputeAllocationChangeRequest(w http.ResponseWriter, r *htt
 	writeJSON(w, http.StatusOK, req)
 }
 
+// @Summary	Update a compute allocation change request
+// @Tags	Compute Allocation Change Requests
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Change request ID"
+// @Param	request	body	models.ComputeAllocationChangeRequest	true	"Change request payload"
+// @Success	200	{object}	models.ComputeAllocationChangeRequest
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-change-requests/{id} [put]
 func (s *Server) updateComputeAllocationChangeRequest(w http.ResponseWriter, r *http.Request) {
 	var req models.ComputeAllocationChangeRequest
 	if err := decodeJSON(r, &req); err != nil {
@@ -621,6 +955,13 @@ func (s *Server) updateComputeAllocationChangeRequest(w http.ResponseWriter, r *
 	writeJSON(w, http.StatusOK, updated)
 }
 
+// @Summary	Delete a compute allocation change request
+// @Tags	Compute Allocation Change Requests
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Change request ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-change-requests/{id} [delete]
 func (s *Server) deleteComputeAllocationChangeRequest(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DeleteComputeAllocationChangeRequest(r.Context(), r.PathValue("id")); err != nil {
 		writeServiceError(w, err)
@@ -629,6 +970,14 @@ func (s *Server) deleteComputeAllocationChangeRequest(w http.ResponseWriter, r *
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List change requests for a compute allocation
+// @Tags	Compute Allocation Change Requests
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{array}	models.ComputeAllocationChangeRequest
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/change-requests [get]
 func (s *Server) listChangeRequestsForAllocation(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListChangeRequestsForAllocation(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -638,6 +987,14 @@ func (s *Server) listChangeRequestsForAllocation(w http.ResponseWriter, r *http.
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	List change requests submitted by a user
+// @Tags	Compute Allocation Change Requests
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Success	200	{array}	models.ComputeAllocationChangeRequest
+// @Failure	404	{object}	object{error=string}
+// @Router	/users/{id}/change-requests [get]
 func (s *Server) listChangeRequestsByRequester(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListChangeRequestsByRequester(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -647,6 +1004,15 @@ func (s *Server) listChangeRequestsByRequester(w http.ResponseWriter, r *http.Re
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	Create a change request event
+// @Tags	Change Request Events
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocationChangeRequestEvent	true	"Event payload"
+// @Success	201	{object}	models.ComputeAllocationChangeRequestEvent
+// @Failure	400	{object}	object{error=string}
+// @Router	/compute-allocation-change-request-events [post]
 func (s *Server) createComputeAllocationChangeRequestEvent(w http.ResponseWriter, r *http.Request) {
 	var evt models.ComputeAllocationChangeRequestEvent
 	if err := decodeJSON(r, &evt); err != nil {
@@ -661,6 +1027,14 @@ func (s *Server) createComputeAllocationChangeRequestEvent(w http.ResponseWriter
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a change request event
+// @Tags	Change Request Events
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Event ID"
+// @Success	200	{object}	models.ComputeAllocationChangeRequestEvent
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-change-request-events/{id} [get]
 func (s *Server) getComputeAllocationChangeRequestEvent(w http.ResponseWriter, r *http.Request) {
 	evt, err := s.svc.GetComputeAllocationChangeRequestEvent(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -670,6 +1044,13 @@ func (s *Server) getComputeAllocationChangeRequestEvent(w http.ResponseWriter, r
 	writeJSON(w, http.StatusOK, evt)
 }
 
+// @Summary	Delete a change request event
+// @Tags	Change Request Events
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Event ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-change-request-events/{id} [delete]
 func (s *Server) deleteComputeAllocationChangeRequestEvent(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DeleteComputeAllocationChangeRequestEvent(r.Context(), r.PathValue("id")); err != nil {
 		writeServiceError(w, err)
@@ -678,6 +1059,14 @@ func (s *Server) deleteComputeAllocationChangeRequestEvent(w http.ResponseWriter
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List events for a change request
+// @Tags	Change Request Events
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Change request ID"
+// @Success	200	{array}	models.ComputeAllocationChangeRequestEvent
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-change-requests/{id}/events [get]
 func (s *Server) listEventsForChangeRequest(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListEventsForChangeRequest(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -687,6 +1076,14 @@ func (s *Server) listEventsForChangeRequest(w http.ResponseWriter, r *http.Reque
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	Get the most recent event for a change request
+// @Tags	Change Request Events
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Change request ID"
+// @Success	200	{object}	models.ComputeAllocationChangeRequestEvent
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-change-requests/{id}/events/latest [get]
 func (s *Server) getLatestEventForChangeRequest(w http.ResponseWriter, r *http.Request) {
 	evt, err := s.svc.GetLatestEventForChangeRequest(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -696,6 +1093,15 @@ func (s *Server) getLatestEventForChangeRequest(w http.ResponseWriter, r *http.R
 	writeJSON(w, http.StatusOK, evt)
 }
 
+// @Summary	Create a compute allocation membership
+// @Tags	Compute Allocation Memberships
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocationMembership	true	"Membership payload"
+// @Success	201	{object}	models.ComputeAllocationMembership
+// @Failure	400	{object}	object{error=string}
+// @Router	/compute-allocation-memberships [post]
 func (s *Server) createComputeAllocationMembership(w http.ResponseWriter, r *http.Request) {
 	var m models.ComputeAllocationMembership
 	if err := decodeJSON(r, &m); err != nil {
@@ -710,6 +1116,14 @@ func (s *Server) createComputeAllocationMembership(w http.ResponseWriter, r *htt
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute allocation membership
+// @Tags	Compute Allocation Memberships
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Membership ID"
+// @Success	200	{object}	models.ComputeAllocationMembership
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-memberships/{id} [get]
 func (s *Server) getComputeAllocationMembership(w http.ResponseWriter, r *http.Request) {
 	m, err := s.svc.GetComputeAllocationMembership(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -719,6 +1133,17 @@ func (s *Server) getComputeAllocationMembership(w http.ResponseWriter, r *http.R
 	writeJSON(w, http.StatusOK, m)
 }
 
+// @Summary	Update a compute allocation membership
+// @Tags	Compute Allocation Memberships
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Membership ID"
+// @Param	request	body	models.ComputeAllocationMembership	true	"Membership payload"
+// @Success	200	{object}	models.ComputeAllocationMembership
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-memberships/{id} [put]
 func (s *Server) updateComputeAllocationMembership(w http.ResponseWriter, r *http.Request) {
 	var m models.ComputeAllocationMembership
 	if err := decodeJSON(r, &m); err != nil {
@@ -734,6 +1159,17 @@ func (s *Server) updateComputeAllocationMembership(w http.ResponseWriter, r *htt
 	writeJSON(w, http.StatusOK, updated)
 }
 
+// @Summary	Update a membership's status
+// @Tags	Compute Allocation Memberships
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Membership ID"
+// @Param	request	body	object{membership_status=models.AllocationStatus}	true	"Status patch"
+// @Success	200	{object}	models.ComputeAllocationMembership
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-memberships/{id}/status [put]
 func (s *Server) updateMembershipStatus(w http.ResponseWriter, r *http.Request) {
 	var body struct {
 		MembershipStatus models.AllocationStatus `json:"membership_status"`
@@ -750,6 +1186,13 @@ func (s *Server) updateMembershipStatus(w http.ResponseWriter, r *http.Request)
 	writeJSON(w, http.StatusOK, updated)
 }
 
+// @Summary	Delete a compute allocation membership
+// @Tags	Compute Allocation Memberships
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Membership ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-memberships/{id} [delete]
 func (s *Server) deleteComputeAllocationMembership(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DeleteComputeAllocationMembership(r.Context(), r.PathValue("id")); err != nil {
 		writeServiceError(w, err)
@@ -758,6 +1201,14 @@ func (s *Server) deleteComputeAllocationMembership(w http.ResponseWriter, r *htt
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List members of a compute allocation
+// @Tags	Compute Allocation Memberships
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{array}	models.ComputeAllocationMembership
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/memberships [get]
 func (s *Server) listMembersForAllocation(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListMembersForAllocation(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -767,6 +1218,14 @@ func (s *Server) listMembersForAllocation(w http.ResponseWriter, r *http.Request
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	List a user's compute allocation memberships
+// @Tags	Compute Allocation Memberships
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Success	200	{array}	models.ComputeAllocationMembership
+// @Failure	404	{object}	object{error=string}
+// @Router	/users/{id}/compute-allocation-memberships [get]
 func (s *Server) listAllocationsForUser(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListAllocationsForUser(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -776,6 +1235,15 @@ func (s *Server) listAllocationsForUser(w http.ResponseWriter, r *http.Request)
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	Create a compute allocation usage record
+// @Tags	Compute Allocation Usages
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocationUsage	true	"Usage payload"
+// @Success	201	{object}	models.ComputeAllocationUsage
+// @Failure	400	{object}	object{error=string}
+// @Router	/compute-allocation-usages [post]
 func (s *Server) createComputeAllocationUsage(w http.ResponseWriter, r *http.Request) {
 	var u models.ComputeAllocationUsage
 	if err := decodeJSON(r, &u); err != nil {
@@ -790,6 +1258,14 @@ func (s *Server) createComputeAllocationUsage(w http.ResponseWriter, r *http.Req
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a compute allocation usage record
+// @Tags	Compute Allocation Usages
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Usage ID"
+// @Success	200	{object}	models.ComputeAllocationUsage
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-usages/{id} [get]
 func (s *Server) getComputeAllocationUsage(w http.ResponseWriter, r *http.Request) {
 	u, err := s.svc.GetComputeAllocationUsage(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -799,6 +1275,13 @@ func (s *Server) getComputeAllocationUsage(w http.ResponseWriter, r *http.Reques
 	writeJSON(w, http.StatusOK, u)
 }
 
+// @Summary	Delete a compute allocation usage record
+// @Tags	Compute Allocation Usages
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Usage ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-usages/{id} [delete]
 func (s *Server) deleteComputeAllocationUsage(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DeleteComputeAllocationUsage(r.Context(), r.PathValue("id")); err != nil {
 		writeServiceError(w, err)
@@ -807,6 +1290,14 @@ func (s *Server) deleteComputeAllocationUsage(w http.ResponseWriter, r *http.Req
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List usages for a compute allocation
+// @Tags	Compute Allocation Usages
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{array}	models.ComputeAllocationUsage
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/usages [get]
 func (s *Server) listUsagesForAllocation(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListUsagesForAllocation(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -816,6 +1307,14 @@ func (s *Server) listUsagesForAllocation(w http.ResponseWriter, r *http.Request)
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	List usages submitted by a user
+// @Tags	Compute Allocation Usages
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Success	200	{array}	models.ComputeAllocationUsage
+// @Failure	404	{object}	object{error=string}
+// @Router	/users/{id}/compute-allocation-usages [get]
 func (s *Server) listUsagesByUser(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListUsagesByUser(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -825,6 +1324,15 @@ func (s *Server) listUsagesByUser(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	Create a membership resource override
+// @Tags	Membership Resource Overrides
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.ComputeAllocationMembershipResourceOverride	true	"Override payload"
+// @Success	201	{object}	models.ComputeAllocationMembershipResourceOverride
+// @Failure	400	{object}	object{error=string}
+// @Router	/compute-allocation-membership-resource-overrides [post]
 func (s *Server) createComputeAllocationMembershipResourceOverride(w http.ResponseWriter, r *http.Request) {
 	var o models.ComputeAllocationMembershipResourceOverride
 	if err := decodeJSON(r, &o); err != nil {
@@ -839,6 +1347,14 @@ func (s *Server) createComputeAllocationMembershipResourceOverride(w http.Respon
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a membership resource override
+// @Tags	Membership Resource Overrides
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Override ID"
+// @Success	200	{object}	models.ComputeAllocationMembershipResourceOverride
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-membership-resource-overrides/{id} [get]
 func (s *Server) getComputeAllocationMembershipResourceOverride(w http.ResponseWriter, r *http.Request) {
 	o, err := s.svc.GetComputeAllocationMembershipResourceOverride(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -848,6 +1364,17 @@ func (s *Server) getComputeAllocationMembershipResourceOverride(w http.ResponseW
 	writeJSON(w, http.StatusOK, o)
 }
 
+// @Summary	Update a membership resource override
+// @Tags	Membership Resource Overrides
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Override ID"
+// @Param	request	body	models.ComputeAllocationMembershipResourceOverride	true	"Override payload"
+// @Success	200	{object}	models.ComputeAllocationMembershipResourceOverride
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-membership-resource-overrides/{id} [put]
 func (s *Server) updateComputeAllocationMembershipResourceOverride(w http.ResponseWriter, r *http.Request) {
 	var o models.ComputeAllocationMembershipResourceOverride
 	if err := decodeJSON(r, &o); err != nil {
@@ -863,6 +1390,13 @@ func (s *Server) updateComputeAllocationMembershipResourceOverride(w http.Respon
 	writeJSON(w, http.StatusOK, updated)
 }
 
+// @Summary	Delete a membership resource override
+// @Tags	Membership Resource Overrides
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Override ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-membership-resource-overrides/{id} [delete]
 func (s *Server) deleteComputeAllocationMembershipResourceOverride(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DeleteComputeAllocationMembershipResourceOverride(r.Context(), r.PathValue("id")); err != nil {
 		writeServiceError(w, err)
@@ -871,6 +1405,14 @@ func (s *Server) deleteComputeAllocationMembershipResourceOverride(w http.Respon
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// @Summary	List resource overrides for a membership
+// @Tags	Membership Resource Overrides
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Membership ID"
+// @Success	200	{array}	models.ComputeAllocationMembershipResourceOverride
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-memberships/{id}/resource-overrides [get]
 func (s *Server) listOverridesForMembership(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListOverridesForMembership(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -880,6 +1422,14 @@ func (s *Server) listOverridesForMembership(w http.ResponseWriter, r *http.Reque
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	List membership overrides referencing a resource
+// @Tags	Membership Resource Overrides
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation resource ID"
+// @Success	200	{array}	models.ComputeAllocationMembershipResourceOverride
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocation-resources/{id}/membership-overrides [get]
 func (s *Server) listOverridesForResource(w http.ResponseWriter, r *http.Request) {
 	rows, err := s.svc.ListOverridesForResource(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -889,6 +1439,14 @@ func (s *Server) listOverridesForResource(w http.ResponseWriter, r *http.Request
 	writeJSON(w, http.StatusOK, rows)
 }
 
+// @Summary	Get total SU usage for a compute allocation
+// @Tags	Compute Allocation Usages
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{object}	object{compute_allocation_id=string,total_su_amount=integer}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/usages/total [get]
 func (s *Server) getTotalSUUsageForAllocation(w http.ResponseWriter, r *http.Request) {
 	total, err := s.svc.GetTotalSUUsageForAllocation(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -901,6 +1459,15 @@ func (s *Server) getTotalSUUsageForAllocation(w http.ResponseWriter, r *http.Req
 	})
 }
 
+// @Summary	Get total SU usage for a user within a compute allocation
+// @Tags	Compute Allocation Usages
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Param	userId	path	string	true	"User ID"
+// @Success	200	{object}	object{compute_allocation_id=string,user_id=string,total_su_amount=integer}
+// @Failure	404	{object}	object{error=string}
+// @Router	/compute-allocations/{id}/users/{userId}/usages/total [get]
 func (s *Server) getTotalSUUsageForUserInAllocation(w http.ResponseWriter, r *http.Request) {
 	allocationID := r.PathValue("id")
 	userID := r.PathValue("userId")
@@ -920,6 +1487,17 @@ type statusUpdateRequest struct {
 	Status string `json:"status"`
 }
 
+// @Summary	Update a user's status
+// @Tags	Users
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Param	request	body	object{status=models.UserStatus}	true	"Status patch"
+// @Success	200	{object}	models.User
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/users/{id}/status [put]
 func (s *Server) updateUserStatus(w http.ResponseWriter, r *http.Request) {
 	var req statusUpdateRequest
 	if err := decodeJSON(r, &req); err != nil {
@@ -934,6 +1512,17 @@ func (s *Server) updateUserStatus(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, u)
 }
 
+// @Summary	Update a project's status
+// @Tags	Projects
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Project ID"
+// @Param	request	body	object{status=models.ProjectStatus}	true	"Status patch"
+// @Success	200	{object}	models.Project
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/projects/{id}/status [put]
 func (s *Server) updateProjectStatus(w http.ResponseWriter, r *http.Request) {
 	var req statusUpdateRequest
 	if err := decodeJSON(r, &req); err != nil {
@@ -948,6 +1537,15 @@ func (s *Server) updateProjectStatus(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, p)
 }
 
+// @Summary	Create a user identity
+// @Tags	User Identities
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	models.UserIdentity	true	"Identity payload"
+// @Success	201	{object}	models.UserIdentity
+// @Failure	400	{object}	object{error=string}
+// @Router	/user-identities [post]
 func (s *Server) createUserIdentity(w http.ResponseWriter, r *http.Request) {
 	var e models.UserIdentity
 	if err := decodeJSON(r, &e); err != nil {
@@ -962,6 +1560,14 @@ func (s *Server) createUserIdentity(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusCreated, created)
 }
 
+// @Summary	Get a user identity
+// @Tags	User Identities
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"Identity ID"
+// @Success	200	{object}	models.UserIdentity
+// @Failure	404	{object}	object{error=string}
+// @Router	/user-identities/{id} [get]
 func (s *Server) getUserIdentity(w http.ResponseWriter, r *http.Request) {
 	e, err := s.svc.GetUserIdentity(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -971,6 +1577,15 @@ func (s *Server) getUserIdentity(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, e)
 }
 
+// @Summary	Get a user identity by source and external ID
+// @Tags	User Identities
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	source	path	string	true	"Identity source"
+// @Param	externalId	path	string	true	"Identity's external ID at that source"
+// @Success	200	{object}	models.UserIdentity
+// @Failure	404	{object}	object{error=string}
+// @Router	/user-identities/sources/{source}/external/{externalId} [get]
 func (s *Server) getUserIdentityBySourceAndExternalID(w http.ResponseWriter, r *http.Request) {
 	e, err := s.svc.GetUserIdentityBySourceAndExternalID(r.Context(), r.PathValue("source"), r.PathValue("externalId"))
 	if err != nil {
@@ -980,6 +1595,14 @@ func (s *Server) getUserIdentityBySourceAndExternalID(w http.ResponseWriter, r *
 	writeJSON(w, http.StatusOK, e)
 }
 
+// @Summary	Get a user identity by its OIDC subject claim
+// @Tags	User Identities
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	oidcSub	path	string	true	"OIDC subject claim"
+// @Success	200	{object}	models.UserIdentity
+// @Failure	404	{object}	object{error=string}
+// @Router	/user-identities/oidc-subjects/{oidcSub} [get]
 func (s *Server) getUserIdentityByOIDCSub(w http.ResponseWriter, r *http.Request) {
 	e, err := s.svc.GetUserIdentityByOIDCSub(r.Context(), r.PathValue("oidcSub"))
 	if err != nil {
@@ -989,6 +1612,14 @@ func (s *Server) getUserIdentityByOIDCSub(w http.ResponseWriter, r *http.Request
 	writeJSON(w, http.StatusOK, e)
 }
 
+// @Summary	List a user's identities
+// @Tags	User Identities
+// @Security	CustosUserHeader
+// @Produce	json
+// @Param	id	path	string	true	"User ID"
+// @Success	200	{array}	models.UserIdentity
+// @Failure	404	{object}	object{error=string}
+// @Router	/users/{id}/user-identities [get]
 func (s *Server) listUserIdentitiesForUser(w http.ResponseWriter, r *http.Request) {
 	out, err := s.svc.ListUserIdentitiesForUser(r.Context(), r.PathValue("id"))
 	if err != nil {
@@ -998,6 +1629,17 @@ func (s *Server) listUserIdentitiesForUser(w http.ResponseWriter, r *http.Reques
 	writeJSON(w, http.StatusOK, out)
 }
 
+// @Summary	Update a user identity
+// @Tags	User Identities
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	id	path	string	true	"Identity ID"
+// @Param	request	body	models.UserIdentity	true	"Identity payload"
+// @Success	200	{object}	models.UserIdentity
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/user-identities/{id} [put]
 func (s *Server) updateUserIdentity(w http.ResponseWriter, r *http.Request) {
 	var e models.UserIdentity
 	if err := decodeJSON(r, &e); err != nil {
@@ -1012,6 +1654,13 @@ func (s *Server) updateUserIdentity(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, &e)
 }
 
+// @Summary	Delete a user identity
+// @Tags	User Identities
+// @Security	CustosUserHeader
+// @Param	id	path	string	true	"Identity ID"
+// @Success	204	"No Content"
+// @Failure	404	{object}	object{error=string}
+// @Router	/user-identities/{id} [delete]
 func (s *Server) deleteUserIdentity(w http.ResponseWriter, r *http.Request) {
 	if err := s.svc.DeleteUserIdentity(r.Context(), r.PathValue("id")); err != nil {
 		writeServiceError(w, err)
@@ -1025,6 +1674,17 @@ type mergeUsersRequest struct {
 	RetiringUserID  string `json:"retiring_user_id"`
 }
 
+// @Summary	Merge two users
+// @Description	Merges the retiring user into the surviving user; the surviving record is returned.
+// @Tags	Users
+// @Security	CustosUserHeader
+// @Accept	json
+// @Produce	json
+// @Param	request	body	mergeUsersRequest	true	"Merge payload"
+// @Success	200	{object}	models.User
+// @Failure	400	{object}	object{error=string}
+// @Failure	404	{object}	object{error=string}
+// @Router	/users/merge [post]
 func (s *Server) mergeUsers(w http.ResponseWriter, r *http.Request) {
 	var req mergeUsersRequest
 	if err := decodeJSON(r, &req); err != nil {