Supercommerce API Docs
Full Module Docs

Product Attribute Module

HTTP surface for product attributes (descriptive fields attached to products — material, country of origin, ingredient list, etc.) and attribute groups (reusable bundles of…

HTTP surface for product attributes (descriptive fields attached to products — material, country of origin, ingredient list, etc.) and attribute groups (reusable bundles of attributes). Admin owns full CRUD; vendor reads from a picker.

Source: api-modules/product-attribute (registered via ProductAttributeModule.forRoot() in apps/api/src/app.module.ts).

Attribute groups let the platform define a bundle once ("Skincare attributes": ingredients + skin type + finish + …) and reuse it across products. The vendor product form picks an attribute group, then fills the per-attribute values.


Conventions

Authentication

Endpoint groupAuthPermission
GET /admin/product-attributes/**requiredproductAttribute: read
POST /admin/product-attributesrequiredproductAttribute: create
PUT /admin/product-attributes/:id, POST /admin/product-attributes/:id/restorerequiredproductAttribute: update
DELETE /admin/product-attributes/:idrequiredproductAttribute: delete
/admin/product-attribute-groups/**requiredproductAttribute: <action> (same productAttribute:* resource)
GET /vendor/product-attributes/**, GET /vendor/product-attribute-groups/**required (vendor session)active vendor in session

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409UNIQUE_VIOLATION (code already taken)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle / soft-delete

Both attributes and groups soft-delete via deletedAt. DELETE flips it, POST /:id/restore clears it. code is unique among non-deleted rows.


Domain types

ProductAttribute

type ProductAttributeType = "multi_select" | "text" | "boolean" | "number" | "select";

type ProductAttribute = {
  id: string;
  title: string;                          // 1..255 chars (display label)
  code: string;                           // 1..255 chars; /^[a-z0-9]+(?:-[a-z0-9]+)*$/; unique
  type: ProductAttributeType;
  isRequired: boolean;                    // product form refuses to submit when missing
  isUnique: boolean;                      // value must be unique across products that use this attribute
  values: ProductAttributeValue[];        // populated for "select" / "multi_select" only
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
};

type ProductAttributeValue = {
  id: string;
  attributeId: string;
  value: string;                          // 1..255 chars
  sortOrder: number;
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
};

ProductAttributeGroup

type ProductAttributeGroup = {
  id: string;
  title: string;
  code: string;                           // unique
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
  attributes: Array<ProductAttribute & { sortOrder: number }>;  // members in order
};

Type semantics

typeStored asNotes
textsingle stringFree-form, no values[]
numbernumeric stringFree-form, no values[]
boolean"true" / "false"No values[]
selectone values[].idSingle-pick from a predefined list
multi_selectarray of values[].idMulti-pick from a predefined list

The values list is sortable via sortOrder and ignored for text / number / boolean.


Admin — attributes

Base path: /admin/product-attributes. Permissions on productAttribute:*.

GET /admin/product-attributes — List

Required permission: productAttribute: read. Standard QueryDto.

Response 200 — paginated envelope of ProductAttribute[].


GET /admin/product-attributes/:id

Required permission: productAttribute: read. Returns ProductAttribute with full values[].


POST /admin/product-attributes — Create

Required permission: productAttribute: create.

Body

{
  "title": "Skin Type",
  "code": "skin-type",
  "type": "multi_select",
  "isRequired": false,
  "isUnique": false,
  "values": [
    { "value": "Oily",   "sortOrder": 0 },
    { "value": "Dry",    "sortOrder": 1 },
    { "value": "Normal", "sortOrder": 2 }
  ]
}
FieldConstraints
title1..255 chars
code1..255 chars, regex ^[a-z0-9]+(?:-[a-z0-9]+)*$. Unique among non-deleted
typeone of multi_select / text / boolean / number / select
isRequireddefault false
isUniquedefault false
valuesrequired for select / multi_select; ignored otherwise

Response 201ProductAttribute.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
409UNIQUE_VIOLATIONcode collides with a non-deleted attribute

PUT /admin/product-attributes/:id — Update

Required permission: productAttribute: update. Partial update. When values[] is sent, it replaces the value list wholesale (existing value ids are dropped — products referencing the old ids must be re-saved; the platform doesn't migrate them automatically).


DELETE /admin/product-attributes/:id — Soft delete

Required permission: productAttribute: delete. POST /:id/restore reverses.


Admin — attribute groups

Base path: /admin/product-attribute-groups. Same productAttribute:* permission resource.

GET /admin/product-attribute-groups — List

Required permission: productAttribute: read. Standard QueryDto. Returns groups with their member attributes resolved (including the sortOrder per member).


GET /admin/product-attribute-groups/:id — Detail

Required permission: productAttribute: read. Returns ProductAttributeGroup with full attributes[].


POST /admin/product-attribute-groups — Create

Required permission: productAttribute: create.

Body

{
  "title": "Skincare attributes",
  "code": "skincare",
  "attributes": [
    { "attributeId": "01J9...", "sortOrder": 0 },
    { "attributeId": "01J9...", "sortOrder": 1 }
  ]
}
FieldConstraints
title1..255 chars
coderegex ^[a-z0-9]+(?:-[a-z0-9]+)*$. Unique among non-deleted groups
attributes[].attributeIdmust reference a non-deleted attribute
attributes[].sortOrder>= 0, default 0

Response 201ProductAttributeGroup.


PUT /admin/product-attribute-groups/:id — Update

Required permission: productAttribute: update. Partial update. attributes[] (when sent) replaces the membership wholesale.


DELETE /admin/product-attribute-groups/:id — Soft delete

Required permission: productAttribute: delete. POST /:id/restore reverses.


Vendor — read-only picker

Base path: /vendor/product-attributes and /vendor/product-attribute-groups. Auth: vendor session with active vendor. Read-only — vendors cannot create attributes (request the platform admin to create them).

Method + pathNotes
GET /vendor/product-attributesSearch attributes for the product form. Standard QueryDto
GET /vendor/product-attributes/:idSingle attribute with values[]
GET /vendor/product-attribute-groupsSearch groups
GET /vendor/product-attribute-groups/:idGroup with member attributes resolved

Both reads filter out soft-deleted rows.


  • catalog — products attach to one attribute group; the per-product attribute values live in catalog tables (product_attribute_value). See catalog.md.
  • admin-rbacproductAttribute:* permission resource. See admin-rbac.md.

On this page