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/07/02 20:46:12 UTC

(airavata-custos) branch server-refactoring created (now 2e8e78bec)

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

lahirujayathilake pushed a change to branch server-refactoring
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git


      at 2e8e78bec Split server handlers into per-resource files

This branch includes the following new commits:

     new 2e8e78bec Split server handlers into per-resource files

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(airavata-custos) 01/01: Split server handlers into per-resource files

Posted by la...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 2e8e78bec689a0f5b1e47dfde2c139bc04b43c56
Author: lahiruj <la...@gmail.com>
AuthorDate: Thu Jul 2 16:46:02 2026 -0400

    Split server handlers into per-resource files
---
 internal/server/allocation.go                |  188 +++
 internal/server/allocation_change_request.go |  247 ++++
 internal/server/allocation_membership.go     |  291 +++++
 internal/server/allocation_resource.go       |  286 +++++
 internal/server/allocation_usage.go          |  158 +++
 internal/server/compute_cluster.go           |  213 ++++
 internal/server/organization.go              |   65 +
 internal/server/project.go                   |  191 +++
 internal/server/server.go                    | 1662 --------------------------
 internal/server/user.go                      |  120 ++
 internal/server/user_identity.go             |  157 +++
 11 files changed, 1916 insertions(+), 1662 deletions(-)

diff --git a/internal/server/allocation.go b/internal/server/allocation.go
new file mode 100644
index 000000000..265c3cbbf
--- /dev/null
+++ b/internal/server/allocation.go
@@ -0,0 +1,188 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+
+	"github.com/apache/airavata-custos/internal/store"
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create a compute allocation
+// @Tags	Compute Allocations
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &a); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocation(r.Context(), &a)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute allocation by ID
+// @Tags	Compute Allocations
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, a)
+}
+
+// @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	BearerAuth
+// @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 := common.DecodeJSON(r, &diff); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationDiff(r.Context(), &diff)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute allocation diff
+// @Tags	Compute Allocation Diffs
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, diff)
+}
+
+// @Summary	Delete a compute allocation diff
+// @Tags	Compute Allocation Diffs
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// @Summary	List diffs for a compute allocation
+// @Tags	Compute Allocation Diffs
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, diffs)
+}
+
+// @Summary	Get the most recent diff for a compute allocation
+// @Tags	Compute Allocation Diffs
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, diff)
+}
+
+// @Summary	List compute allocations (filtered + paginated)
+// @Tags	Compute Allocations
+// @Security	BearerAuth
+// @Produce	json
+// @Param	project_id	query	string	false	"Filter by project ID"
+// @Param	status	query	string	false	"Filter by status"
+// @Param	q	query	string	false	"Search name"
+// @Param	limit	query	integer	false	"Page size"
+// @Param	offset	query	integer	false	"Page offset"
+// @Success	200	{object}	ComputeAllocationListResponse
+// @Router	/compute-allocations [get]
+func (s *Server) listComputeAllocations(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	f := store.AllocationListFilter{
+		ProjectID: q.Get("project_id"),
+		Status:    q.Get("status"),
+		Query:     q.Get("q"),
+		Limit:     atoiOr(q.Get("limit"), 50),
+		Offset:    atoiOr(q.Get("offset"), 0),
+	}
+	rows, total, err := s.svc.ListComputeAllocations(r.Context(), f)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	if rows == nil {
+		rows = []models.ComputeAllocation{}
+	}
+	common.WriteJSON(w, http.StatusOK, ComputeAllocationListResponse{Items: rows, Total: total})
+}
diff --git a/internal/server/allocation_change_request.go b/internal/server/allocation_change_request.go
new file mode 100644
index 000000000..9267ffad4
--- /dev/null
+++ b/internal/server/allocation_change_request.go
@@ -0,0 +1,247 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+
+	"github.com/apache/airavata-custos/internal/store"
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create a compute allocation change request
+// @Tags	Compute Allocation Change Requests
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &req); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationChangeRequest(r.Context(), &req)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute allocation change request
+// @Tags	Compute Allocation Change Requests
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, req)
+}
+
+// @Summary	Update a compute allocation change request
+// @Tags	Compute Allocation Change Requests
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &req); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	req.ID = r.PathValue("id")
+	updated, err := s.svc.UpdateComputeAllocationChangeRequest(r.Context(), &req)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, updated)
+}
+
+// @Summary	Delete a compute allocation change request
+// @Tags	Compute Allocation Change Requests
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// @Summary	List change requests for a compute allocation
+// @Tags	Compute Allocation Change Requests
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
+
+// @Summary	List change requests submitted by a user
+// @Tags	Compute Allocation Change Requests
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
+
+// @Summary	Create a change request event
+// @Tags	Change Request Events
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &evt); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationChangeRequestEvent(r.Context(), &evt)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a change request event
+// @Tags	Change Request Events
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, evt)
+}
+
+// @Summary	Delete a change request event
+// @Tags	Change Request Events
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// @Summary	List events for a change request
+// @Tags	Change Request Events
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
+
+// @Summary	Get the most recent event for a change request
+// @Tags	Change Request Events
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, evt)
+}
+
+func (s *Server) listChangeRequests(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	f := store.ChangeRequestListFilter{
+		Status: q.Get("status"),
+		Limit:  atoiOr(q.Get("limit"), 50),
+	}
+	rows, err := s.svc.ListChangeRequests(r.Context(), f)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	if rows == nil {
+		rows = []models.ComputeAllocationChangeRequest{}
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
diff --git a/internal/server/allocation_membership.go b/internal/server/allocation_membership.go
new file mode 100644
index 000000000..62e5e819c
--- /dev/null
+++ b/internal/server/allocation_membership.go
@@ -0,0 +1,291 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create a compute allocation membership
+// @Tags	Compute Allocation Memberships
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &m); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationMembership(r.Context(), &m)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute allocation membership
+// @Tags	Compute Allocation Memberships
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, m)
+}
+
+// @Summary	Update a compute allocation membership
+// @Tags	Compute Allocation Memberships
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &m); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	m.ID = r.PathValue("id")
+	updated, err := s.svc.UpdateComputeAllocationMembership(r.Context(), &m)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, updated)
+}
+
+// @Summary	Update a membership's status
+// @Tags	Compute Allocation Memberships
+// @Security	BearerAuth
+// @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"`
+	}
+	if err := common.DecodeJSON(r, &body); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	updated, err := s.svc.UpdateMembershipStatus(r.Context(), r.PathValue("id"), body.MembershipStatus)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, updated)
+}
+
+// @Summary	Delete a compute allocation membership
+// @Tags	Compute Allocation Memberships
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// @Summary	List members of a compute allocation
+// @Tags	Compute Allocation Memberships
+// @Security	BearerAuth
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{array}	AllocationMembershipResponse
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	out := make([]AllocationMembershipResponse, 0, len(rows))
+	for _, m := range rows {
+		out = append(out, AllocationMembershipResponse{
+			ComputeAllocationMembership: m.ComputeAllocationMembership,
+			Role:                        m.Role,
+			DisplayName:                 m.DisplayName,
+			Email:                       m.Email,
+		})
+	}
+	common.WriteJSON(w, http.StatusOK, out)
+}
+
+// @Summary	List a user's compute allocation memberships
+// @Tags	Compute Allocation Memberships
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
+
+// @Summary	Create a membership resource override
+// @Tags	Membership Resource Overrides
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &o); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationMembershipResourceOverride(r.Context(), &o)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a membership resource override
+// @Tags	Membership Resource Overrides
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, o)
+}
+
+// @Summary	Update a membership resource override
+// @Tags	Membership Resource Overrides
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &o); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	o.ID = r.PathValue("id")
+	updated, err := s.svc.UpdateComputeAllocationMembershipResourceOverride(r.Context(), &o)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, updated)
+}
+
+// @Summary	Delete a membership resource override
+// @Tags	Membership Resource Overrides
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// @Summary	List resource overrides for a membership
+// @Tags	Membership Resource Overrides
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
+
+// @Summary	List membership overrides referencing a resource
+// @Tags	Membership Resource Overrides
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
diff --git a/internal/server/allocation_resource.go b/internal/server/allocation_resource.go
new file mode 100644
index 000000000..2849ad89f
--- /dev/null
+++ b/internal/server/allocation_resource.go
@@ -0,0 +1,286 @@
+// 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.
+
+package server
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @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	BearerAuth
+// @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 := common.DecodeJSON(r, &res); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationResource(r.Context(), &res)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute allocation resource
+// @Tags	Compute Allocation Resources
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, res)
+}
+
+// @Summary	List all compute allocation resources
+// @Tags	Compute Allocation Resources
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, resources)
+}
+
+type attachResourceRequest struct {
+	ComputeAllocationResourceID string `json:"compute_allocation_resource_id"`
+	ResourceAmount              int64  `json:"resource_amount"`
+	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	BearerAuth
+// @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 := common.DecodeJSON(r, &body); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	mapping, err := s.svc.AttachResourceToAllocation(r.Context(), r.PathValue("id"), body.ComputeAllocationResourceID, body.ResourceAmount, body.ResourceTime)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, mapping)
+}
+
+type updateAllocationResourceMappingRequest struct {
+	ResourceAmount int64 `json:"resource_amount"`
+	ResourceTime   int64 `json:"resource_time"`
+}
+
+// @Summary	Update a compute allocation -> resource mapping
+// @Tags	Compute Allocation Resources
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &body); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	mapping, err := s.svc.UpdateAllocationResourceMapping(r.Context(), r.PathValue("id"), r.PathValue("resourceId"), body.ResourceAmount, body.ResourceTime)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, mapping)
+}
+
+// @Summary	Detach a resource from a compute allocation
+// @Tags	Compute Allocation Resources
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// @Summary	List resources attached to a compute allocation
+// @Tags	Compute Allocation Resources
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, resources)
+}
+
+// @Summary	List compute allocations attached to a resource
+// @Tags	Compute Allocation Resources
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.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	BearerAuth
+// @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 := common.DecodeJSON(r, &rate); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationResourceRate(r.Context(), &rate)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute allocation resource rate
+// @Tags	Compute Allocation Resource Rates
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rate)
+}
+
+// @Summary	List rate history for a resource
+// @Tags	Compute Allocation Resource Rates
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.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	BearerAuth
+// @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 != "" {
+		parsed, err := time.Parse(time.RFC3339Nano, raw)
+		if err != nil {
+			common.WriteError(w, http.StatusBadRequest, errors.New("invalid 'at' query parameter; expected RFC 3339"))
+			return
+		}
+		at = parsed
+	}
+	rate, err := s.svc.GetEffectiveRateForResource(r.Context(), r.PathValue("id"), at)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rate)
+}
diff --git a/internal/server/allocation_usage.go b/internal/server/allocation_usage.go
new file mode 100644
index 000000000..25f6c1f97
--- /dev/null
+++ b/internal/server/allocation_usage.go
@@ -0,0 +1,158 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create a compute allocation usage record
+// @Tags	Compute Allocation Usages
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &u); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationUsage(r.Context(), &u)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute allocation usage record
+// @Tags	Compute Allocation Usages
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, u)
+}
+
+// @Summary	Delete a compute allocation usage record
+// @Tags	Compute Allocation Usages
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// @Summary	List usages for a compute allocation
+// @Tags	Compute Allocation Usages
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
+
+// @Summary	List usages submitted by a user
+// @Tags	Compute Allocation Usages
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, rows)
+}
+
+// @Summary	Get total SU usage for a compute allocation
+// @Tags	Compute Allocation Usages
+// @Security	BearerAuth
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Success	200	{object}	AllocationSUTotalResponse
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, AllocationSUTotalResponse{
+		ComputeAllocationID: r.PathValue("id"),
+		TotalSUAmount:       total,
+	})
+}
+
+// @Summary	Get total SU usage for a user within a compute allocation
+// @Tags	Compute Allocation Usages
+// @Security	BearerAuth
+// @Produce	json
+// @Param	id	path	string	true	"Compute allocation ID"
+// @Param	userId	path	string	true	"User ID"
+// @Success	200	{object}	UserAllocationSUTotalResponse
+// @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")
+	total, err := s.svc.GetTotalSUUsageForUserInAllocation(r.Context(), allocationID, userID)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, UserAllocationSUTotalResponse{
+		ComputeAllocationID: allocationID,
+		UserID:              userID,
+		TotalSUAmount:       total,
+	})
+}
diff --git a/internal/server/compute_cluster.go b/internal/server/compute_cluster.go
new file mode 100644
index 000000000..fbee95f91
--- /dev/null
+++ b/internal/server/compute_cluster.go
@@ -0,0 +1,213 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create a compute cluster
+// @Tags	Compute Clusters
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &c); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeCluster(r.Context(), &c)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute cluster by ID
+// @Tags	Compute Clusters
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, c)
+}
+
+// @Summary	List compute clusters
+// @Tags	Compute Clusters
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, clusters)
+}
+
+// @Summary	Create a compute cluster user
+// @Tags	Compute Cluster Users
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &cu); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeClusterUser(r.Context(), &cu)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a compute cluster user by ID
+// @Tags	Compute Cluster Users
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, cu)
+}
+
+// @Summary	Update a compute cluster user
+// @Tags	Compute Cluster Users
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &cu); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	cu.ID = r.PathValue("id")
+	if err := s.svc.UpdateComputeClusterUser(r.Context(), &cu); err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, &cu)
+}
+
+// @Summary	Delete a compute cluster user
+// @Tags	Compute Cluster Users
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// @Summary	List users on a compute cluster
+// @Tags	Compute Cluster Users
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, users)
+}
+
+// @Summary	Get a compute cluster user by (cluster, user) pair
+// @Tags	Compute Cluster Users
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, cu)
+}
+
+// @Summary	List compute cluster users for a user
+// @Tags	Compute Cluster Users
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, users)
+}
diff --git a/internal/server/organization.go b/internal/server/organization.go
new file mode 100644
index 000000000..ae9068847
--- /dev/null
+++ b/internal/server/organization.go
@@ -0,0 +1,65 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create an organization
+// @Tags	Organizations
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &org); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateOrganization(r.Context(), &org)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get an organization by ID
+// @Tags	Organizations
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, org)
+}
diff --git a/internal/server/project.go b/internal/server/project.go
new file mode 100644
index 000000000..cfd4ad459
--- /dev/null
+++ b/internal/server/project.go
@@ -0,0 +1,191 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/apache/airavata-custos/internal/store"
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create a project
+// @Tags	Projects
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &p); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateProject(r.Context(), &p)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a project by ID
+// @Tags	Projects
+// @Security	BearerAuth
+// @Produce	json
+// @Param	id	path	string	true	"Project ID"
+// @Success	200	{object}	ProjectResponse
+// @Failure	404	{object}	object{error=string}
+// @Router	/projects/{id} [get]
+func (s *Server) getProject(w http.ResponseWriter, r *http.Request) {
+	p, err := s.svc.GetProjectWithPI(r.Context(), r.PathValue("id"))
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, projectResponseFrom(p))
+}
+
+// projectResponseFrom builds the API response shape from a JOIN-fetched row.
+func projectResponseFrom(p *store.ProjectWithPI) ProjectResponse {
+	v := ProjectResponse{Project: p.Project, ProjectPIEmail: p.PIEmail}
+	if full := strings.TrimSpace(p.PIFirstName + " " + p.PILastName); full != "" {
+		v.ProjectPIDisplayName = full
+	} else if p.PIEmail != "" {
+		v.ProjectPIDisplayName = p.PIEmail
+	}
+	return v
+}
+
+// @Summary	Update a project's status
+// @Tags	Projects
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &req); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	p, err := s.svc.UpdateProjectStatus(r.Context(), r.PathValue("id"), models.ProjectStatus(req.Status))
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, p)
+}
+
+// @Summary	List projects (filtered + paginated, PI joined)
+// @Tags	Projects
+// @Security	BearerAuth
+// @Produce	json
+// @Param	pi_id	query	string	false	"Filter by PI user ID"
+// @Param	status	query	string	false	"Filter by status"
+// @Param	q	query	string	false	"Search title / originated_id"
+// @Param	limit	query	integer	false	"Page size"
+// @Param	offset	query	integer	false	"Page offset"
+// @Success	200	{object}	ProjectListResponse
+// @Router	/projects [get]
+func (s *Server) listProjects(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	f := store.ProjectListFilter{
+		PIID:   q.Get("pi_id"),
+		Status: q.Get("status"),
+		Query:  q.Get("q"),
+		Limit:  atoiOr(q.Get("limit"), 50),
+		Offset: atoiOr(q.Get("offset"), 0),
+	}
+	rows, total, err := s.svc.ListProjectsWithPI(r.Context(), f)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	items := make([]ProjectResponse, 0, len(rows))
+	for i := range rows {
+		items = append(items, projectResponseFrom(&rows[i]))
+	}
+	common.WriteJSON(w, http.StatusOK, ProjectListResponse{Items: items, Total: total})
+}
+
+// @Summary	List project members (one row per distinct user, with allocations)
+// @Tags	Projects
+// @Security	BearerAuth
+// @Produce	json
+// @Param	id	path	string	true	"Project ID"
+// @Success	200	{array}	ProjectMemberResponse
+// @Failure	404	{object}	object{error=string}
+// @Router	/projects/{id}/members [get]
+func (s *Server) listProjectMembers(w http.ResponseWriter, r *http.Request) {
+	projectID := r.PathValue("id")
+	rows, err := s.svc.ListMembersForProject(r.Context(), projectID)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+
+	// Role is project-level (single value per user), so the dedup just
+	// collects the user's allocations.
+	type aggregate struct {
+		base        store.MembershipWithUser
+		allocations []ProjectMemberAllocationRef
+	}
+	byUser := map[string]*aggregate{}
+	order := []string{}
+	for _, m := range rows {
+		agg, ok := byUser[m.UserID]
+		if !ok {
+			agg = &aggregate{base: m}
+			byUser[m.UserID] = agg
+			order = append(order, m.UserID)
+		}
+		agg.allocations = append(agg.allocations, ProjectMemberAllocationRef{
+			ID:   m.ComputeAllocationID,
+			Name: m.AllocationName,
+			Role: m.Role,
+		})
+	}
+
+	out := make([]ProjectMemberResponse, 0, len(order))
+	for _, uid := range order {
+		agg := byUser[uid]
+		out = append(out, ProjectMemberResponse{
+			ID:          agg.base.ID,
+			ProjectID:   projectID,
+			UserID:      agg.base.UserID,
+			Email:       agg.base.Email,
+			DisplayName: agg.base.DisplayName,
+			Role:        agg.base.Role,
+			Status:      string(agg.base.MembershipStatus),
+			AddedTime:   agg.base.StartTime.UTC(),
+			Allocations: agg.allocations,
+		})
+	}
+	common.WriteJSON(w, http.StatusOK, out)
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index f8d96e9e3..7285a21e8 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -23,11 +23,9 @@ import (
 	"log/slog"
 	"net/http"
 	"strconv"
-	"strings"
 	"time"
 
 	"github.com/apache/airavata-custos/internal/httputil"
-	"github.com/apache/airavata-custos/internal/store"
 	"github.com/apache/airavata-custos/pkg/common"
 	"github.com/apache/airavata-custos/pkg/identity"
 	"github.com/apache/airavata-custos/pkg/models"
@@ -196,1534 +194,10 @@ func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
 	common.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
 }
 
-// @Summary	Create an organization
-// @Tags	Organizations
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &org); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateOrganization(r.Context(), &org)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get an organization by ID
-// @Tags	Organizations
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, org)
-}
-
-// @Summary	Create a user
-// @Tags	Users
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &u); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateUser(r.Context(), &u)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a user by ID
-// @Tags	Users
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, u)
-}
-
-// @Summary	Create a project
-// @Tags	Projects
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &p); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateProject(r.Context(), &p)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a project by ID
-// @Tags	Projects
-// @Security	BearerAuth
-// @Produce	json
-// @Param	id	path	string	true	"Project ID"
-// @Success	200	{object}	ProjectResponse
-// @Failure	404	{object}	object{error=string}
-// @Router	/projects/{id} [get]
-func (s *Server) getProject(w http.ResponseWriter, r *http.Request) {
-	p, err := s.svc.GetProjectWithPI(r.Context(), r.PathValue("id"))
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, projectResponseFrom(p))
-}
-
-// projectResponseFrom builds the API response shape from a JOIN-fetched row.
-func projectResponseFrom(p *store.ProjectWithPI) ProjectResponse {
-	v := ProjectResponse{Project: p.Project, ProjectPIEmail: p.PIEmail}
-	if full := strings.TrimSpace(p.PIFirstName + " " + p.PILastName); full != "" {
-		v.ProjectPIDisplayName = full
-	} else if p.PIEmail != "" {
-		v.ProjectPIDisplayName = p.PIEmail
-	}
-	return v
-}
-
-// @Summary	Create a compute cluster
-// @Tags	Compute Clusters
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &c); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeCluster(r.Context(), &c)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute cluster by ID
-// @Tags	Compute Clusters
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, c)
-}
-
-// @Summary	List compute clusters
-// @Tags	Compute Clusters
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, clusters)
-}
-
-// @Summary	Create a compute cluster user
-// @Tags	Compute Cluster Users
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &cu); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeClusterUser(r.Context(), &cu)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute cluster user by ID
-// @Tags	Compute Cluster Users
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, cu)
-}
-
-// @Summary	Update a compute cluster user
-// @Tags	Compute Cluster Users
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &cu); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	cu.ID = r.PathValue("id")
-	if err := s.svc.UpdateComputeClusterUser(r.Context(), &cu); err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, &cu)
-}
-
-// @Summary	Delete a compute cluster user
-// @Tags	Compute Cluster Users
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-// @Summary	List users on a compute cluster
-// @Tags	Compute Cluster Users
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, users)
-}
-
-// @Summary	Get a compute cluster user by (cluster, user) pair
-// @Tags	Compute Cluster Users
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, cu)
-}
-
-// @Summary	List compute cluster users for a user
-// @Tags	Compute Cluster Users
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, users)
-}
-
-// @Summary	Create a compute allocation
-// @Tags	Compute Allocations
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &a); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocation(r.Context(), &a)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute allocation by ID
-// @Tags	Compute Allocations
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.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	BearerAuth
-// @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 := common.DecodeJSON(r, &res); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocationResource(r.Context(), &res)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute allocation resource
-// @Tags	Compute Allocation Resources
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, res)
-}
-
-// @Summary	List all compute allocation resources
-// @Tags	Compute Allocation Resources
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, resources)
-}
-
-type attachResourceRequest struct {
-	ComputeAllocationResourceID string `json:"compute_allocation_resource_id"`
-	ResourceAmount              int64  `json:"resource_amount"`
-	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	BearerAuth
-// @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 := common.DecodeJSON(r, &body); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	mapping, err := s.svc.AttachResourceToAllocation(r.Context(), r.PathValue("id"), body.ComputeAllocationResourceID, body.ResourceAmount, body.ResourceTime)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, mapping)
-}
-
-type updateAllocationResourceMappingRequest struct {
-	ResourceAmount int64 `json:"resource_amount"`
-	ResourceTime   int64 `json:"resource_time"`
-}
-
-// @Summary	Update a compute allocation -> resource mapping
-// @Tags	Compute Allocation Resources
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &body); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	mapping, err := s.svc.UpdateAllocationResourceMapping(r.Context(), r.PathValue("id"), r.PathValue("resourceId"), body.ResourceAmount, body.ResourceTime)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, mapping)
-}
-
-// @Summary	Detach a resource from a compute allocation
-// @Tags	Compute Allocation Resources
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-// @Summary	List resources attached to a compute allocation
-// @Tags	Compute Allocation Resources
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, resources)
-}
-
-// @Summary	List compute allocations attached to a resource
-// @Tags	Compute Allocation Resources
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.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	BearerAuth
-// @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 := common.DecodeJSON(r, &rate); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocationResourceRate(r.Context(), &rate)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute allocation resource rate
-// @Tags	Compute Allocation Resource Rates
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rate)
-}
-
-// @Summary	List rate history for a resource
-// @Tags	Compute Allocation Resource Rates
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.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	BearerAuth
-// @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 != "" {
-		parsed, err := time.Parse(time.RFC3339Nano, raw)
-		if err != nil {
-			common.WriteError(w, http.StatusBadRequest, errors.New("invalid 'at' query parameter; expected RFC 3339"))
-			return
-		}
-		at = parsed
-	}
-	rate, err := s.svc.GetEffectiveRateForResource(r.Context(), r.PathValue("id"), at)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.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	BearerAuth
-// @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 := common.DecodeJSON(r, &diff); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocationDiff(r.Context(), &diff)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute allocation diff
-// @Tags	Compute Allocation Diffs
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, diff)
-}
-
-// @Summary	Delete a compute allocation diff
-// @Tags	Compute Allocation Diffs
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-// @Summary	List diffs for a compute allocation
-// @Tags	Compute Allocation Diffs
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, diffs)
-}
-
-// @Summary	Get the most recent diff for a compute allocation
-// @Tags	Compute Allocation Diffs
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, diff)
-}
-
-// @Summary	Create a compute allocation change request
-// @Tags	Compute Allocation Change Requests
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &req); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocationChangeRequest(r.Context(), &req)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute allocation change request
-// @Tags	Compute Allocation Change Requests
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, req)
-}
-
-// @Summary	Update a compute allocation change request
-// @Tags	Compute Allocation Change Requests
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &req); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	req.ID = r.PathValue("id")
-	updated, err := s.svc.UpdateComputeAllocationChangeRequest(r.Context(), &req)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, updated)
-}
-
-// @Summary	Delete a compute allocation change request
-// @Tags	Compute Allocation Change Requests
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-// @Summary	List change requests for a compute allocation
-// @Tags	Compute Allocation Change Requests
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
-// @Summary	List change requests submitted by a user
-// @Tags	Compute Allocation Change Requests
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
-// @Summary	Create a change request event
-// @Tags	Change Request Events
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &evt); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocationChangeRequestEvent(r.Context(), &evt)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a change request event
-// @Tags	Change Request Events
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, evt)
-}
-
-// @Summary	Delete a change request event
-// @Tags	Change Request Events
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-// @Summary	List events for a change request
-// @Tags	Change Request Events
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
-// @Summary	Get the most recent event for a change request
-// @Tags	Change Request Events
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, evt)
-}
-
-// @Summary	Create a compute allocation membership
-// @Tags	Compute Allocation Memberships
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &m); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocationMembership(r.Context(), &m)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute allocation membership
-// @Tags	Compute Allocation Memberships
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, m)
-}
-
-// @Summary	Update a compute allocation membership
-// @Tags	Compute Allocation Memberships
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &m); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	m.ID = r.PathValue("id")
-	updated, err := s.svc.UpdateComputeAllocationMembership(r.Context(), &m)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, updated)
-}
-
-// @Summary	Update a membership's status
-// @Tags	Compute Allocation Memberships
-// @Security	BearerAuth
-// @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"`
-	}
-	if err := common.DecodeJSON(r, &body); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	updated, err := s.svc.UpdateMembershipStatus(r.Context(), r.PathValue("id"), body.MembershipStatus)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, updated)
-}
-
-// @Summary	Delete a compute allocation membership
-// @Tags	Compute Allocation Memberships
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-// @Summary	List members of a compute allocation
-// @Tags	Compute Allocation Memberships
-// @Security	BearerAuth
-// @Produce	json
-// @Param	id	path	string	true	"Compute allocation ID"
-// @Success	200	{array}	AllocationMembershipResponse
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	out := make([]AllocationMembershipResponse, 0, len(rows))
-	for _, m := range rows {
-		out = append(out, AllocationMembershipResponse{
-			ComputeAllocationMembership: m.ComputeAllocationMembership,
-			Role:                        m.Role,
-			DisplayName:                 m.DisplayName,
-			Email:                       m.Email,
-		})
-	}
-	common.WriteJSON(w, http.StatusOK, out)
-}
-
-// @Summary	List a user's compute allocation memberships
-// @Tags	Compute Allocation Memberships
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
-// @Summary	Create a compute allocation usage record
-// @Tags	Compute Allocation Usages
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &u); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocationUsage(r.Context(), &u)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a compute allocation usage record
-// @Tags	Compute Allocation Usages
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, u)
-}
-
-// @Summary	Delete a compute allocation usage record
-// @Tags	Compute Allocation Usages
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-// @Summary	List usages for a compute allocation
-// @Tags	Compute Allocation Usages
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
-// @Summary	List usages submitted by a user
-// @Tags	Compute Allocation Usages
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
-// @Summary	Create a membership resource override
-// @Tags	Membership Resource Overrides
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &o); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateComputeAllocationMembershipResourceOverride(r.Context(), &o)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a membership resource override
-// @Tags	Membership Resource Overrides
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, o)
-}
-
-// @Summary	Update a membership resource override
-// @Tags	Membership Resource Overrides
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &o); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	o.ID = r.PathValue("id")
-	updated, err := s.svc.UpdateComputeAllocationMembershipResourceOverride(r.Context(), &o)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, updated)
-}
-
-// @Summary	Delete a membership resource override
-// @Tags	Membership Resource Overrides
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-// @Summary	List resource overrides for a membership
-// @Tags	Membership Resource Overrides
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
-// @Summary	List membership overrides referencing a resource
-// @Tags	Membership Resource Overrides
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
-// @Summary	Get total SU usage for a compute allocation
-// @Tags	Compute Allocation Usages
-// @Security	BearerAuth
-// @Produce	json
-// @Param	id	path	string	true	"Compute allocation ID"
-// @Success	200	{object}	AllocationSUTotalResponse
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, AllocationSUTotalResponse{
-		ComputeAllocationID: r.PathValue("id"),
-		TotalSUAmount:       total,
-	})
-}
-
-// @Summary	Get total SU usage for a user within a compute allocation
-// @Tags	Compute Allocation Usages
-// @Security	BearerAuth
-// @Produce	json
-// @Param	id	path	string	true	"Compute allocation ID"
-// @Param	userId	path	string	true	"User ID"
-// @Success	200	{object}	UserAllocationSUTotalResponse
-// @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")
-	total, err := s.svc.GetTotalSUUsageForUserInAllocation(r.Context(), allocationID, userID)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, UserAllocationSUTotalResponse{
-		ComputeAllocationID: allocationID,
-		UserID:              userID,
-		TotalSUAmount:       total,
-	})
-}
-
 type statusUpdateRequest struct {
 	Status string `json:"status"`
 }
 
-// @Summary	Update a user's status
-// @Tags	Users
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &req); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	u, err := s.svc.UpdateUserStatus(r.Context(), r.PathValue("id"), models.UserStatus(req.Status))
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, u)
-}
-
-// @Summary	Update a project's status
-// @Tags	Projects
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &req); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	p, err := s.svc.UpdateProjectStatus(r.Context(), r.PathValue("id"), models.ProjectStatus(req.Status))
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, p)
-}
-
-// @Summary	Create a user identity
-// @Tags	User Identities
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &e); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	created, err := s.svc.CreateUserIdentity(r.Context(), &e)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusCreated, created)
-}
-
-// @Summary	Get a user identity
-// @Tags	User Identities
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, e)
-}
-
-// @Summary	Get a user identity by source and external ID
-// @Tags	User Identities
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, e)
-}
-
-// @Summary	Get a user identity by its OIDC subject claim
-// @Tags	User Identities
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, e)
-}
-
-// @Summary	List a user's identities
-// @Tags	User Identities
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, out)
-}
-
-// @Summary	Update a user identity
-// @Tags	User Identities
-// @Security	BearerAuth
-// @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 := common.DecodeJSON(r, &e); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	e.ID = r.PathValue("id")
-	if err := s.svc.UpdateUserIdentity(r.Context(), &e); err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, &e)
-}
-
-// @Summary	Delete a user identity
-// @Tags	User Identities
-// @Security	BearerAuth
-// @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 {
-		common.WriteServiceError(w, err)
-		return
-	}
-	w.WriteHeader(http.StatusNoContent)
-}
-
-type mergeUsersRequest struct {
-	SurvivingUserID string `json:"surviving_user_id"`
-	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	BearerAuth
-// @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 := common.DecodeJSON(r, &req); err != nil {
-		common.WriteError(w, http.StatusBadRequest, err)
-		return
-	}
-	survivor, err := s.svc.MergeUsers(r.Context(), req.SurvivingUserID, req.RetiringUserID)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	common.WriteJSON(w, http.StatusOK, survivor)
-}
-
 // LoggingMiddleware logs every request once it completes. It wraps
 // tracing.Middleware (logging outer, tracing inner) and reads the trace_id
 // from the X-Trace-Id response header the inner span set, since the inner
@@ -1746,142 +220,6 @@ func LoggingMiddleware(next http.Handler) http.Handler {
 	})
 }
 
-// @Summary	List projects (filtered + paginated, PI joined)
-// @Tags	Projects
-// @Security	BearerAuth
-// @Produce	json
-// @Param	pi_id	query	string	false	"Filter by PI user ID"
-// @Param	status	query	string	false	"Filter by status"
-// @Param	q	query	string	false	"Search title / originated_id"
-// @Param	limit	query	integer	false	"Page size"
-// @Param	offset	query	integer	false	"Page offset"
-// @Success	200	{object}	ProjectListResponse
-// @Router	/projects [get]
-func (s *Server) listProjects(w http.ResponseWriter, r *http.Request) {
-	q := r.URL.Query()
-	f := store.ProjectListFilter{
-		PIID:   q.Get("pi_id"),
-		Status: q.Get("status"),
-		Query:  q.Get("q"),
-		Limit:  atoiOr(q.Get("limit"), 50),
-		Offset: atoiOr(q.Get("offset"), 0),
-	}
-	rows, total, err := s.svc.ListProjectsWithPI(r.Context(), f)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	items := make([]ProjectResponse, 0, len(rows))
-	for i := range rows {
-		items = append(items, projectResponseFrom(&rows[i]))
-	}
-	common.WriteJSON(w, http.StatusOK, ProjectListResponse{Items: items, Total: total})
-}
-
-// @Summary	List project members (one row per distinct user, with allocations)
-// @Tags	Projects
-// @Security	BearerAuth
-// @Produce	json
-// @Param	id	path	string	true	"Project ID"
-// @Success	200	{array}	ProjectMemberResponse
-// @Failure	404	{object}	object{error=string}
-// @Router	/projects/{id}/members [get]
-func (s *Server) listProjectMembers(w http.ResponseWriter, r *http.Request) {
-	projectID := r.PathValue("id")
-	rows, err := s.svc.ListMembersForProject(r.Context(), projectID)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-
-	// Role is project-level (single value per user), so the dedup just
-	// collects the user's allocations.
-	type aggregate struct {
-		base        store.MembershipWithUser
-		allocations []ProjectMemberAllocationRef
-	}
-	byUser := map[string]*aggregate{}
-	order := []string{}
-	for _, m := range rows {
-		agg, ok := byUser[m.UserID]
-		if !ok {
-			agg = &aggregate{base: m}
-			byUser[m.UserID] = agg
-			order = append(order, m.UserID)
-		}
-		agg.allocations = append(agg.allocations, ProjectMemberAllocationRef{
-			ID:   m.ComputeAllocationID,
-			Name: m.AllocationName,
-			Role: m.Role,
-		})
-	}
-
-	out := make([]ProjectMemberResponse, 0, len(order))
-	for _, uid := range order {
-		agg := byUser[uid]
-		out = append(out, ProjectMemberResponse{
-			ID:          agg.base.ID,
-			ProjectID:   projectID,
-			UserID:      agg.base.UserID,
-			Email:       agg.base.Email,
-			DisplayName: agg.base.DisplayName,
-			Role:        agg.base.Role,
-			Status:      string(agg.base.MembershipStatus),
-			AddedTime:   agg.base.StartTime.UTC(),
-			Allocations: agg.allocations,
-		})
-	}
-	common.WriteJSON(w, http.StatusOK, out)
-}
-
-// @Summary	List compute allocations (filtered + paginated)
-// @Tags	Compute Allocations
-// @Security	BearerAuth
-// @Produce	json
-// @Param	project_id	query	string	false	"Filter by project ID"
-// @Param	status	query	string	false	"Filter by status"
-// @Param	q	query	string	false	"Search name"
-// @Param	limit	query	integer	false	"Page size"
-// @Param	offset	query	integer	false	"Page offset"
-// @Success	200	{object}	ComputeAllocationListResponse
-// @Router	/compute-allocations [get]
-func (s *Server) listComputeAllocations(w http.ResponseWriter, r *http.Request) {
-	q := r.URL.Query()
-	f := store.AllocationListFilter{
-		ProjectID: q.Get("project_id"),
-		Status:    q.Get("status"),
-		Query:     q.Get("q"),
-		Limit:     atoiOr(q.Get("limit"), 50),
-		Offset:    atoiOr(q.Get("offset"), 0),
-	}
-	rows, total, err := s.svc.ListComputeAllocations(r.Context(), f)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	if rows == nil {
-		rows = []models.ComputeAllocation{}
-	}
-	common.WriteJSON(w, http.StatusOK, ComputeAllocationListResponse{Items: rows, Total: total})
-}
-
-func (s *Server) listChangeRequests(w http.ResponseWriter, r *http.Request) {
-	q := r.URL.Query()
-	f := store.ChangeRequestListFilter{
-		Status: q.Get("status"),
-		Limit:  atoiOr(q.Get("limit"), 50),
-	}
-	rows, err := s.svc.ListChangeRequests(r.Context(), f)
-	if err != nil {
-		common.WriteServiceError(w, err)
-		return
-	}
-	if rows == nil {
-		rows = []models.ComputeAllocationChangeRequest{}
-	}
-	common.WriteJSON(w, http.StatusOK, rows)
-}
-
 func atoiOr(s string, def int) int {
 	if s == "" {
 		return def
diff --git a/internal/server/user.go b/internal/server/user.go
new file mode 100644
index 000000000..fa01da0bc
--- /dev/null
+++ b/internal/server/user.go
@@ -0,0 +1,120 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create a user
+// @Tags	Users
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &u); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateUser(r.Context(), &u)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a user by ID
+// @Tags	Users
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, u)
+}
+
+// @Summary	Update a user's status
+// @Tags	Users
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &req); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	u, err := s.svc.UpdateUserStatus(r.Context(), r.PathValue("id"), models.UserStatus(req.Status))
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, u)
+}
+
+type mergeUsersRequest struct {
+	SurvivingUserID string `json:"surviving_user_id"`
+	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	BearerAuth
+// @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 := common.DecodeJSON(r, &req); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	survivor, err := s.svc.MergeUsers(r.Context(), req.SurvivingUserID, req.RetiringUserID)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, survivor)
+}
diff --git a/internal/server/user_identity.go b/internal/server/user_identity.go
new file mode 100644
index 000000000..a79d08030
--- /dev/null
+++ b/internal/server/user_identity.go
@@ -0,0 +1,157 @@
+// 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.
+
+package server
+
+import (
+	"net/http"
+
+	"github.com/apache/airavata-custos/pkg/common"
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// @Summary	Create a user identity
+// @Tags	User Identities
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &e); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateUserIdentity(r.Context(), &e)
+	if err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusCreated, created)
+}
+
+// @Summary	Get a user identity
+// @Tags	User Identities
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, e)
+}
+
+// @Summary	Get a user identity by source and external ID
+// @Tags	User Identities
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, e)
+}
+
+// @Summary	Get a user identity by its OIDC subject claim
+// @Tags	User Identities
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, e)
+}
+
+// @Summary	List a user's identities
+// @Tags	User Identities
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, out)
+}
+
+// @Summary	Update a user identity
+// @Tags	User Identities
+// @Security	BearerAuth
+// @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 := common.DecodeJSON(r, &e); err != nil {
+		common.WriteError(w, http.StatusBadRequest, err)
+		return
+	}
+	e.ID = r.PathValue("id")
+	if err := s.svc.UpdateUserIdentity(r.Context(), &e); err != nil {
+		common.WriteServiceError(w, err)
+		return
+	}
+	common.WriteJSON(w, http.StatusOK, &e)
+}
+
+// @Summary	Delete a user identity
+// @Tags	User Identities
+// @Security	BearerAuth
+// @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 {
+		common.WriteServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}