Reviews Module — Admin
HTTP surface for platform-admin moderation of product reviews: cross-vendor list with filters, full-field edit, lifecycle transitions (approve / reject / mark-spam), admin-create…
HTTP surface for platform-admin moderation of product reviews: cross-vendor list with filters, full-field edit, lifecycle transitions (approve / reject / mark-spam), admin-create on behalf of a customer, soft-delete + restore.
Source:
api-modules/reviews/src/controllers/admin-reviews.controller.ts.A review attaches to a product and is owned by either a real Better-Auth user (
userId) or an admin-entered author name pair (authorFirstName+authorLastName) — XOR enforced at zod and at the DB CHECK constraint.
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching review:* permission.
| Endpoint group | Permission |
|---|---|
GET /admin/reviews, GET /admin/reviews/:id | review: read |
POST /admin/reviews | review: create |
PATCH /admin/reviews/:id, POST /admin/reviews/:id/restore | review: update |
POST /admin/reviews/:id/approve | review: approve |
POST /admin/reviews/:id/reject | review: reject |
POST /admin/reviews/:id/mark-spam, POST /admin/reviews/:id/unmark-spam | review: mark-spam |
DELETE /admin/reviews/:id | review: delete |
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Lifecycle
status is one of pending / approved / rejected. Approve / reject endpoints stamp approvedAt / approvedBy / rejectedAt / rejectedBy and emit events. isSpam is an orthogonal flag (a spam review can also be approved/rejected/pending). Soft-delete sets deletedAt; restore clears it.
Domain types
ReviewResponse
type ReviewStatus = "pending" | "approved" | "rejected";
type ReviewImageResponse = {
id: string;
reviewId: string;
url: string;
sortOrder: number;
};
type ReviewResponse = {
id: string;
productId: string;
userId: string | null;
authorFirstName: string | null;
authorLastName: string | null;
title: string | null;
content: string; // 1..5000, trimmed
stars: number; // 1..5
recommended: boolean | null;
isVerifiedPurchase: boolean;
isSpam: boolean;
status: ReviewStatus;
approvedAt: string | null;
approvedBy: string | null; // admin user id
rejectedAt: string | null;
rejectedBy: string | null;
createdBy: string | null; // admin user id when admin-created
createdAt: string;
updatedAt: string;
deletedAt: string | null;
images: ReviewImageResponse[];
};Endpoints
Base path: /admin/reviews.
GET /admin/reviews — List reviews
Required permission: review: read. Full moderator filter set including spam and deleted.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
productId | string? | — | Filter to one product |
vendorId | string? | — | Filter to reviews of products owned by one vendor |
userId | string? | — | Filter to one author |
status | ReviewStatus? | — | — |
isSpam | "true" | "false"? | — | Coerced to boolean |
includeDeleted | "true" | "false"? | — | Coerced — when true, soft-deleted rows are returned |
page | int | 1 | — |
limit | int | DEFAULT_REVIEW_PAGE_SIZE | Bounded by MAX_REVIEW_PAGE_SIZE |
orderBy | "newest" | "oldest" | "stars-desc" | "stars-asc"? | — | — |
Response 200 — paginated envelope of ReviewResponse[].
GET /admin/reviews/:id — Get one review
Required permission: review: read. Returns the review with its images.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/reviews — Create a review on behalf of a customer
Required permission: review: create. Either userId (attach to a real customer) or both authorFirstName + authorLastName (anonymous customer the admin is recording for) — XOR.
Body
{
"productId": "01J9...",
"userId": "01J9...", // OR omit and supply authorFirstName/authorLastName
"title": "Great product", // optional, trimmed, 1..200
"content": "Loved it", // trimmed, 1..5000
"stars": 5, // 1..5
"recommended": true, // optional
"isVerifiedPurchase": false, // optional
"status": "approved", // optional; defaults to service default
"images": [
{ "url": "https://cdn.example/r/1.jpg", "sortOrder": 0 }
]
}| Field | Type | Constraints |
|---|---|---|
productId | string | Required |
userId | string? | XOR with author name pair |
authorFirstName / authorLastName | string? | Trimmed, 1..100. Required together when userId absent |
title | string? | Trimmed, 1..200 |
content | string | Trimmed, 1..5000 |
stars | int | 1..5 |
recommended | boolean? | null | — |
isVerifiedPurchase | boolean? | — |
status | ReviewStatus? | — |
images[] | Array<{url, sortOrder}>? | url: valid URL; sortOrder >= 0 |
Response 201 — ReviewResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod (including the XOR rule) |
PATCH /admin/reviews/:id — Edit any field
Required permission: review: update. PATCH semantics — only supplied keys are applied. When images is sent, the entire image set is replaced wholesale; omit it to leave images untouched. Retroactively attaching a userId (or swapping in author names) is supported and validated against the same XOR rule.
Body
{
"title": "Updated title",
"content": "Edited content...",
"stars": 4,
"recommended": true,
"authorFirstName": null,
"authorLastName": null,
"images": [
{ "url": "https://cdn.example/r/1.jpg", "sortOrder": 0 }
]
}Response 200 — updated ReviewResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 404 | NOT_FOUND | Unknown id |
POST /admin/reviews/:id/approve — Approve
Required permission: review: approve. Stamps approvedAt / approvedBy = current admin user id. Resets rejectedAt / rejectedBy to null.
Response 200 — ReviewResponse.
POST /admin/reviews/:id/reject — Reject
Required permission: review: reject. Stamps rejectedAt / rejectedBy.
Response 200 — ReviewResponse.
POST /admin/reviews/:id/mark-spam — Mark as spam
Required permission: review: mark-spam. Sets isSpam = true. Does not change status.
Response 200 — ReviewResponse.
POST /admin/reviews/:id/unmark-spam — Clear spam flag
Required permission: review: mark-spam. Sets isSpam = false.
Response 200 — ReviewResponse.
DELETE /admin/reviews/:id — Soft-delete
Required permission: review: delete. Stamps deletedAt. Hidden from default lists; visible when includeDeleted=true.
Response 200 — deleted ReviewResponse.
POST /admin/reviews/:id/restore — Restore
Required permission: review: update. Clears deletedAt.
Response 200 — restored ReviewResponse.
Related modules
admin-rbac— gates every endpoint viareview:*. Seeadmin-rbac.md.catalog—productIdreferences a non-deleted product.customer— admin-create can attach to a customer via the customer picker. Seecustomer.md.auth—userId,approvedBy,rejectedBy,createdByare Better-Auth user ids.
Product Attribute Module — Admin
HTTP surface for managing the platform-wide catalogue of product attributes (typed metadata fields — Material, Capacity, Heel Height, etc.) and attribute groups that bundle…
Rewards Module — Admin
HTTP surface for platform staff managing the rewards/loyalty plugin: per-customer search, balance + history inspection, and manual credit/debit adjustments. All endpoints sit…