Product Catalog Schema: RUO Research Chemicals¶
This document defines the complete product data model for Research Relay's RUO (Research Use Only) research chemical catalog in MedusaJS v2. It maps standard Medusa fields, defines custom RUO compliance fields, establishes the category taxonomy, and documents all business rules that affect products at checkout and fulfillment.
1. Standard MedusaJS v2 Product Fields¶
1.1 Product-Level Fields¶
These are built-in fields on the Medusa Product data model. Every product in the catalog uses these directly.
| Field | Type | Required | RR Usage | Example |
|---|---|---|---|---|
id |
string (auto) | Auto | Primary key | prod_01H... |
title |
string | Yes | Chemical common name | "BPC-157 (Body Protection Compound)" |
subtitle |
string | No | IUPAC name or alternate name | "Pentadecapeptide BPC 157" |
handle |
string (auto) | Auto | URL slug, auto-generated from title | "bpc-157-body-protection-compound" |
description |
string | Yes | Detailed product description (plain text or HTML) | Chemical description, applications, handling notes |
status |
enum | Yes | Product lifecycle status | draft / proposed / published / rejected |
thumbnail |
string | No | Primary product image URL | "https://cdn.research-relay.com/products/bpc-157-thumb.jpg" |
images |
Image[] | No | Additional product images (vial, packaging, molecular structure) | Array of image URLs |
is_giftcard |
boolean | Auto | Always false for chemicals |
false |
discountable |
boolean | Yes | Whether discount codes apply | true (most products) |
type |
ProductType | Yes | Product type classification | "Peptide", "Reagent", "Solvent" |
collection |
ProductCollection | No | Marketing groupings | "Best Sellers", "New Arrivals" |
categories |
ProductCategory[] | Yes | Taxonomy categories (see Section 3) | ["Peptides", "Peptides > Protective"] |
tags |
ProductTag[] | No | Search/filter tags | ["cold-chain", "lyophilized", "popular"] |
weight |
string | No | Product weight (set at variant level instead) | -- |
length |
string | No | Product length (set at variant level instead) | -- |
width |
string | No | Product width (set at variant level instead) | -- |
height |
string | No | Product height (set at variant level instead) | -- |
hs_code |
string | No | Harmonized System code (set at variant level instead) | -- |
origin_country |
string | No | Country of manufacture (set at variant level if varies) | "US" |
material |
string | No | Not used for chemicals | -- |
external_id |
string | No | ID in external system (supplier catalog number) | "SUP-BPC157-001" |
metadata |
JSON | Yes | Custom RUO fields stored here (see Section 2) | { "cas_number": "137317-11-0", ... } |
1.2 Product Variant Fields¶
Each product has one or more variants representing different sizes/quantities. Variants carry the shipping-relevant physical attributes.
| Field | Type | Required | RR Usage | Example |
|---|---|---|---|---|
id |
string (auto) | Auto | Primary key | variant_01H... |
title |
string | Yes | Size description | "5mg Lyophilized Powder" |
sku |
string | Yes | Stock keeping unit | "BPC157-5MG-LYO" |
barcode |
string | No | Barcode if applicable | -- |
ean |
string | No | EAN code | -- |
upc |
string | No | UPC code | -- |
hs_code |
string | Yes | Harmonized System tariff code | "2933.99.9750" |
origin_country |
string | Yes | Country of manufacture | "US" |
mid_code |
string | No | Manufacturer ID code | -- |
material |
string | No | Not used | -- |
weight |
number | Yes | Weight in grams (for shipping calc) | 15 |
length |
number | Yes | Length in mm (packaged) | 120 |
width |
number | Yes | Width in mm (packaged) | 40 |
height |
number | Yes | Height in mm (packaged) | 40 |
allow_backorder |
boolean | Yes | Whether to accept orders when out of stock | false |
manage_inventory |
boolean | Yes | Whether Medusa tracks inventory | true |
metadata |
JSON | No | Variant-specific overrides | {} |
options |
OptionValue[] | Yes | Variant option values | { "Size": "5mg" } |
1.3 Product Options¶
Product options define the axes of variation. For research chemicals, the primary option is size/quantity.
| Option Name | Values (examples) | Applies To |
|---|---|---|
Size |
1mg, 5mg, 10mg, 25mg, 50mg, 100mg, 1g, 5g, 10g, 25g |
Peptides, amino acids, reference standards |
Volume |
10mL, 25mL, 50mL, 100mL, 500mL, 1L |
Solvents, solutions, buffers |
Concentration |
1mg/mL, 5mg/mL, 10mg/mL |
Pre-mixed solutions |
1.4 Pricing¶
Prices are set per variant. Research Relay operates in USD only at launch (US domestic only per Decision D7/D11).
| Field | Type | Example |
|---|---|---|
currency_code |
string | "usd" |
amount |
number | 3999 (represents $39.99, stored in cents) |
Pricing is configured through Medusa's Pricing Module and linked to variants via the standard ProductVariant <-> Price link.
2. Custom RUO Metadata Fields¶
Research chemicals require domain-specific data beyond what Medusa provides out of the box. These fields are stored in the product's metadata JSON field for the initial implementation, with a migration path to a dedicated custom module for structured querying (see Section 8).
2.1 Product Metadata Schema¶
All custom fields are stored under product.metadata. Every product in the catalog must have these fields populated before being set to published status.
| Field | Type | Required | Example | Purpose |
|---|---|---|---|---|
cas_number |
string | Yes | "137317-11-0" |
Chemical Abstracts Service registry number. Unique chemical identifier. |
molecular_formula |
string | Yes | "C62H98N16O22" |
Molecular formula per IUPAC convention. |
molecular_weight |
number | Yes | 1419.53 |
Molecular weight in g/mol. |
purity_percent |
number | Yes | 98.0 |
Minimum guaranteed purity percentage. |
form |
enum | Yes | "powder" |
Physical form. One of: powder, liquid, crystal, solution, lyophilized. |
grade |
enum | Yes | "research" |
Product grade. One of: research, analytical, reagent, reference. |
sds_url |
string | Yes | "https://cdn.research-relay.com/sds/bpc-157.pdf" |
URL to Safety Data Sheet PDF. Required by OSHA/GHS. |
storage_class |
enum | Yes | "frozen" |
Storage temperature classification. One of: ambient, refrigerated, frozen. |
storage_temp_min |
number | No | -20 |
Minimum recommended storage temperature in degrees Celsius. |
storage_temp_max |
number | No | -15 |
Maximum recommended storage temperature in degrees Celsius. |
is_cold_chain |
boolean | Yes | true |
Whether the product requires cold-chain shipping. Triggers $15 surcharge (Decision D15). |
is_hazmat |
boolean | Yes | false |
Whether the product is classified as hazardous material under DOT 49 CFR. |
hazmat_class |
string | No | "6.1" |
UN hazmat class. Required if is_hazmat is true. |
un_number |
string | No | "UN2811" |
UN identification number. Required if is_hazmat is true. |
packaging_group |
string | No | "III" |
Packing group (I, II, or III). Required if is_hazmat is true. |
shelf_life_days |
number | No | 730 |
Shelf life in days from date of manufacture. |
restricted_states |
string[] | No | ["CA", "NY"] |
US state postal codes where this product cannot be shipped. Checked at checkout. |
requires_signature |
boolean | Auto | true |
Auto-calculated: true when any variant price >= $150 (Decision D14). Can be overridden to true for high-value chemicals regardless of price. |
ruo_only |
boolean | Yes | true |
Always true. Marks product as Research Use Only. Enforces RUO checkout flow. |
lot_trackable |
boolean | Yes | true |
Whether lot numbers are tracked for this product. true for all chemicals, false for lab supplies. |
2.2 Storage Class Definitions¶
| Storage Class | Temperature Range | Shipping Requirement | Examples |
|---|---|---|---|
ambient |
15-25 C | Standard packaging | Most reagents, solvents, dry amino acids |
refrigerated |
2-8 C | Cold-chain required (is_cold_chain: true) |
Certain peptide solutions, enzymes |
frozen |
-20 C or below | Cold-chain required (is_cold_chain: true) |
Lyophilized peptides, sensitive compounds |
2.3 Hazmat Field Requirements¶
When is_hazmat is true, the following fields become required:
| Field | Validation |
|---|---|
hazmat_class |
Must be a valid DOT hazmat class (e.g., "3", "6.1", "8", "9") |
un_number |
Must match pattern UN\d{4} (e.g., "UN2811") |
packaging_group |
Must be one of "I", "II", "III" |
Hazmat products are subject to carrier-specific shipping restrictions. The specific product-by-product hazmat review is deferred to catalog finalization (Decision D16).
2.4 Metadata Validation Rules¶
Before a product can transition from draft to published:
- All
Required: Yesfields must be populated cas_numbermust be a valid CAS format (\d{2,7}-\d{2}-\d)purity_percentmust be between 0 and 100molecular_weightmust be a positive numbersds_urlmust be a valid URL pointing to an accessible PDF- If
is_hazmatistrue, all hazmat sub-fields must be populated - If
storage_classisrefrigeratedorfrozen,is_cold_chainmust betrue ruo_onlymust betrue(enforced, cannot be set tofalse)
3. Product Categories and Collections¶
3.1 Category Taxonomy¶
Categories use Medusa's built-in ProductCategory model, which supports hierarchical nesting. The taxonomy is organized by chemical/product type.
Research Chemicals (root)
├── Peptides
│ ├── Protective Peptides
│ ├── Growth Factor Peptides
│ ├── Antimicrobial Peptides
│ ├── Metabolic Peptides
│ └── Custom / Specialty Peptides
├── Amino Acids
│ ├── Standard Amino Acids
│ ├── Non-Standard Amino Acids
│ └── Modified Amino Acids
├── Reference Standards
│ ├── Analytical Reference Standards
│ └── Pharmacopeial Reference Standards
├── Reagents
│ ├── Organic Reagents
│ ├── Inorganic Reagents
│ └── Biochemical Reagents
├── Solvents
│ ├── HPLC-Grade Solvents
│ ├── ACS-Grade Solvents
│ └── General-Purpose Solvents
├── Buffers & Solutions
│ ├── Buffer Solutions
│ ├── Reconstitution Solutions
│ └── Bacteriostatic Water
└── Lab Supplies
├── Vials & Containers
├── Syringes & Needles
└── Mixing Supplies
Notes:
- Categories are used for storefront navigation and filtering.
- A product can belong to multiple categories (many-to-many).
- The
Lab Suppliescategory contains non-chemical products that do not require RUO metadata fields likecas_numberormolecular_formula. These products still carryruo_only: falseandlot_trackable: false.
3.2 Product Types¶
Medusa's ProductType is a flat classification (not hierarchical). Use it for the top-level chemical type:
| Product Type | Description |
|---|---|
Peptide |
Synthetic peptides for research |
Amino Acid |
Individual amino acids |
Reference Standard |
Certified reference materials |
Reagent |
Chemical reagents |
Solvent |
Laboratory solvents |
Buffer |
Buffer solutions and reconstitution media |
Lab Supply |
Non-chemical laboratory consumables |
3.3 Collections¶
Collections are marketing-driven groupings managed via the Medusa admin. Planned collections:
| Collection | Purpose |
|---|---|
Best Sellers |
High-volume products for homepage display |
New Arrivals |
Recently added products |
Starter Kits |
Curated bundles for new researchers |
Cold-Chain Products |
All products requiring cold-chain shipping |
Sale |
Products with active promotions |
3.4 Product Tags¶
Tags provide flexible, non-hierarchical labeling for filtering and internal organization.
| Tag Category | Example Tags |
|---|---|
| Shipping | cold-chain, hazmat, signature-required, oversized |
| Form | lyophilized, powder, solution, crystal |
| Storage | ambient, refrigerated, frozen |
| Popularity | popular, trending, staff-pick |
| Compliance | state-restricted, new-formula |
4. Variant Strategy¶
4.1 Variant Structure¶
Each product has variants representing different purchasable sizes/quantities. Every variant is independently priced, stocked, and has its own shipping dimensions.
Peptide example (BPC-157):
| Variant Title | SKU | Option (Size) | Price (USD) | Weight (g) |
|---|---|---|---|---|
| 5mg Lyophilized Powder | BPC157-5MG-LYO |
5mg |
$39.99 | 12 |
| 10mg Lyophilized Powder | BPC157-10MG-LYO |
10mg |
$69.99 | 14 |
| 25mg Lyophilized Powder | BPC157-25MG-LYO |
25mg |
$149.99 | 18 |
| 50mg Lyophilized Powder | BPC157-50MG-LYO |
50mg |
$259.99 | 22 |
Solvent example (Bacteriostatic Water):
| Variant Title | SKU | Option (Volume) | Price (USD) | Weight (g) |
|---|---|---|---|---|
| 10mL Vial | BAC-WATER-10ML |
10mL |
$8.99 | 25 |
| 30mL Vial | BAC-WATER-30ML |
30mL |
$12.99 | 55 |
4.2 SKU Naming Convention¶
| Component | Description | Examples |
|---|---|---|
PRODUCT_CODE |
Short product identifier | BPC157, BAC-WATER, TB500 |
SIZE |
Numeric quantity | 5, 10, 25, 100 |
UNIT |
Unit of measure | MG, G, ML, L |
FORM_CODE |
Physical form abbreviation | LYO (lyophilized), PWD (powder), SOL (solution), CRY (crystal) |
4.3 Inventory Per Variant¶
Each variant independently tracks:
- Inventory quantity via Medusa's Inventory Module
- Lot assignments via the Product Compliance Module (lot-to-variant link)
- Pricing via Medusa's Pricing Module
- Shipping dimensions (weight, length, width, height) on the variant record
4.4 When to Create Separate Products vs. Variants¶
| Scenario | Approach |
|---|---|
| Same chemical, different sizes | Variants of one product |
| Same chemical, different purities (98% vs 99.5%) | Separate products (different metadata) |
| Same chemical, different forms (powder vs solution) | Separate products (different handling/storage) |
| Same chemical, different grades (research vs analytical) | Separate products (different metadata, pricing) |
| Completely different chemicals | Separate products |
5. Shipping Classification Logic¶
These business rules are derived from the decisions documented in docs/compliance/business-decisions.md. They affect how products are handled during checkout and fulfillment.
5.1 Cold-Chain Products (Decision D15)¶
IF any item in cart has metadata.is_cold_chain == true:
ADD $15.00 flat surcharge to shipping cost (once per order, not per item)
RESTRICT shipping methods to Monday-Wednesday dispatch
ADD cold-chain packaging (insulated container + gel packs or dry ice)
- The $15 surcharge is a flat fee per order, not per item.
- Products flagged
is_cold_chain: truemust also havestorage_classset torefrigeratedorfrozen. - Expedited or overnight shipping may be required during warm months (June-September). This is handled operationally, not enforced in the system at launch.
5.2 Hazmat Products (Decision D16)¶
IF any item in cart has metadata.is_hazmat == true:
RESTRICT shipping methods to hazmat-approved carriers/services
APPLY carrier-specific hazmat surcharge (passed through at cost)
REQUIRE proper DOT 49 CFR labeling on package
LOG hazmat_class, un_number, packaging_group for shipping label generation
- Specific hazmat products will be identified during catalog finalization.
- Carrier agreements for hazmat shipping must be in place before listing hazmat products.
- Some carriers (USPS) do not accept certain hazmat classes. The fulfillment module must filter available shipping methods accordingly.
5.3 Signature Requirement (Decision D14)¶
IF order total >= $150.00:
SET requires_signature = true on shipment
REQUIRE carrier signature confirmation service
ELSE IF any item has metadata.requires_signature == true:
SET requires_signature = true on shipment
- The $150 threshold is on the order total (after discounts, before tax and shipping).
- Individual products can override this with
requires_signature: truein metadata for high-value or sensitive chemicals regardless of price.
5.4 Free Shipping (Decision D11)¶
IF order subtotal >= $200.00:
OFFER free standard shipping (USPS Priority Mail)
Customer may upgrade to expedited/overnight at additional cost
- Free shipping applies to standard shipping only. Expedited and overnight options remain at calculated rates.
- Cold-chain surcharge ($15) still applies even with free shipping.
5.5 State Restriction Checking¶
AT CHECKOUT (before payment):
FOR each item in cart:
IF item.product.metadata.restricted_states CONTAINS shipping_address.state:
BLOCK checkout
DISPLAY: "One or more items in your cart cannot be shipped to {state}."
IDENTIFY restricted items for the customer
- State restrictions are checked at the cart/checkout level, not at the product browsing level. Customers can view all products but cannot complete checkout if restricted.
- The
restricted_statesarray is populated per product during the regulatory review before catalog launch (Decision D8). - The restriction check must happen server-side (in a workflow step or API middleware) to prevent bypass.
5.6 Shipping Method Selection¶
| Shipping Method | Carrier | Estimated Transit | Hazmat Allowed | P.O. Box (D9) |
|---|---|---|---|---|
| Standard | USPS Priority Mail | 3-5 business days | Limited | Yes |
| Expedited | UPS 2-Day / FedEx 2Day | 2 business days | Yes (with agreement) | No |
| Overnight | UPS Next Day Air / FedEx Overnight | 1 business day | Yes (with agreement) | No |
Rates are calculated in real-time via ShipStation integration (Decision D10).
6. Checkout Compliance Fields¶
The checkout flow must capture compliance data before an order can be completed. This is enforced by the Compliance Checkout Module (see project plan, Section 2.4).
6.1 Required Checkout Attestation¶
Before the cart can be completed, the customer must submit a checkout attestation with the following fields:
| Field | Type | Required | Validation | Source |
|---|---|---|---|---|
ruo_disclaimer_accepted |
boolean | Yes | Must be true |
Checkbox: "I acknowledge these products are for Research Use Only" |
ruo_disclaimer_version |
string | Auto | Current active disclaimer version | System-provided from RuoDisclaimer model |
age_verified |
boolean | Yes | Must be true |
Checkbox: "I am at least 18 years of age" (Decision D1) |
research_use_attested |
boolean | Yes | Must be true |
Checkbox: "I am purchasing for legitimate research purposes" |
institution_name |
string | No | Optional free text | Text field: research institution or lab name |
researcher_name |
string | No | Optional free text | Text field: name of responsible researcher |
6.2 Checkout Attestation Text¶
The exact text displayed at checkout (from docs/compliance/ruo-disclosure.md):
By completing this purchase, I confirm that:
- I am at least 18 years of age.
- I am purchasing these products for legitimate research use only.
- I understand these products are not for human or animal consumption.
- I will handle, store, and dispose of these products in accordance with all applicable laws and safety standards.
- I have read and agree to Research Relay's Terms of Service and RUO Disclosure.
All five points must be acknowledged via a single checkbox (clickwrap agreement).
6.3 Attestation Data Captured Automatically¶
These fields are captured server-side and stored with the attestation record:
| Field | Type | Purpose |
|---|---|---|
cart_id |
string | Links attestation to the cart |
order_id |
string | Populated when order is placed (via order.placed subscriber) |
customer_id |
string | Linked customer account (if logged in) |
ip_address |
string | Customer's IP address for audit trail |
user_agent |
string | Browser user agent for audit trail |
attested_at |
datetime | Timestamp of attestation submission |
6.4 Checkout Flow Sequence¶
1. Customer adds items to cart
2. Customer enters shipping address
└── State restriction check runs (Section 5.5)
3. Customer selects shipping method
└── Cold-chain surcharge applied if applicable (Section 5.1)
4. Customer submits compliance attestation (Section 6.1)
└── POST /store/compliance/attest { cart_id, attestation fields }
5. Customer selects payment method (BTC/Lightning or ACH)
6. Cart completion workflow runs
└── validate-compliance step checks attestation is complete
└── If attestation incomplete, cart completion is blocked
7. Payment is processed
8. Order is created
└── order.placed subscriber archives attestation with order_id
7. Sample Product Entry¶
BPC-157 (Body Protection Compound-157)¶
A common research peptide used as a reference example. This shows all fields fully populated.
Product Record¶
{
"title": "BPC-157 (Body Protection Compound-157)",
"subtitle": "Pentadecapeptide BPC 157",
"handle": "bpc-157-body-protection-compound-157",
"description": "BPC-157 is a synthetic pentadecapeptide consisting of 15 amino acids (Gly-Glu-Pro-Pro-Pro-Gly-Lys-Pro-Ala-Asp-Asp-Ala-Gly-Leu-Val) with a molecular weight of 1419.53 g/mol. This research-grade peptide is supplied as a lyophilized powder for reconstitution. Suitable for in vitro research applications. Not for human consumption.",
"status": "published",
"thumbnail": "https://cdn.research-relay.com/products/bpc-157/thumb.jpg",
"images": [
{ "url": "https://cdn.research-relay.com/products/bpc-157/vial-5mg.jpg" },
{ "url": "https://cdn.research-relay.com/products/bpc-157/molecular-structure.png" },
{ "url": "https://cdn.research-relay.com/products/bpc-157/hplc-chromatogram.png" }
],
"is_giftcard": false,
"discountable": true,
"type": { "value": "Peptide" },
"collection": { "title": "Best Sellers" },
"categories": [
{ "name": "Peptides" },
{ "name": "Protective Peptides" }
],
"tags": [
{ "value": "cold-chain" },
{ "value": "lyophilized" },
{ "value": "popular" }
],
"origin_country": "US",
"metadata": {
"cas_number": "137317-11-0",
"molecular_formula": "C62H98N16O22",
"molecular_weight": 1419.53,
"purity_percent": 98.0,
"form": "lyophilized",
"grade": "research",
"sds_url": "https://cdn.research-relay.com/sds/bpc-157-sds.pdf",
"storage_class": "frozen",
"storage_temp_min": -20,
"storage_temp_max": -15,
"is_cold_chain": true,
"is_hazmat": false,
"hazmat_class": null,
"un_number": null,
"packaging_group": null,
"shelf_life_days": 730,
"restricted_states": [],
"requires_signature": false,
"ruo_only": true,
"lot_trackable": true
}
}
Product Options¶
Variants¶
{
"variants": [
{
"title": "5mg Lyophilized Powder",
"sku": "BPC157-5MG-LYO",
"hs_code": "2933.99.9750",
"origin_country": "US",
"weight": 12,
"length": 100,
"width": 35,
"height": 35,
"allow_backorder": false,
"manage_inventory": true,
"options": { "Size": "5mg" },
"prices": [
{ "currency_code": "usd", "amount": 3999 }
]
},
{
"title": "10mg Lyophilized Powder",
"sku": "BPC157-10MG-LYO",
"hs_code": "2933.99.9750",
"origin_country": "US",
"weight": 14,
"length": 100,
"width": 35,
"height": 35,
"allow_backorder": false,
"manage_inventory": true,
"options": { "Size": "10mg" },
"prices": [
{ "currency_code": "usd", "amount": 6999 }
]
},
{
"title": "25mg Lyophilized Powder",
"sku": "BPC157-25MG-LYO",
"hs_code": "2933.99.9750",
"origin_country": "US",
"weight": 18,
"length": 110,
"width": 40,
"height": 40,
"allow_backorder": false,
"manage_inventory": true,
"options": { "Size": "25mg" },
"prices": [
{ "currency_code": "usd", "amount": 14999 }
]
},
{
"title": "50mg Lyophilized Powder",
"sku": "BPC157-50MG-LYO",
"hs_code": "2933.99.9750",
"origin_country": "US",
"weight": 22,
"length": 120,
"width": 45,
"height": 45,
"allow_backorder": false,
"manage_inventory": true,
"options": { "Size": "50mg" },
"prices": [
{ "currency_code": "usd", "amount": 25999 }
]
}
]
}
Linked Compliance Data (via Product Compliance Module)¶
{
"lot": {
"lot_number": "BPC157-2026-001",
"manufacture_date": "2026-01-15T00:00:00Z",
"expiration_date": "2028-01-15T00:00:00Z",
"quantity_produced": 500,
"quantity_remaining": 342,
"status": "active",
"coa": {
"file_url": "https://cdn.research-relay.com/coa/BPC157-2026-001.pdf",
"purity_percentage": 98.7,
"analysis_date": "2026-01-18T00:00:00Z",
"lab_name": "Analytical Testing Services Inc.",
"lab_reference": "ATS-2026-00142",
"verified": true,
"verified_by": "admin_01H...",
"purity_records": [
{
"test_method": "HPLC",
"parameter": "purity",
"result_value": "98.7",
"unit": "%",
"specification": ">98%",
"passes": true
},
{
"test_method": "MS",
"parameter": "identity",
"result_value": "1419.5",
"unit": "m/z",
"specification": "1419.53 +/- 0.5",
"passes": true
},
{
"test_method": "LAL",
"parameter": "endotoxin",
"result_value": "0.05",
"unit": "EU/mg",
"specification": "<0.1 EU/mg",
"passes": true
}
]
}
}
}
8. MedusaJS v2 Implementation Notes¶
8.1 Metadata vs. Custom Module: Decision Framework¶
There are two approaches for storing the custom RUO fields defined in Section 2.
Approach A: Product Metadata (Recommended for Phase A)
Store all custom fields in the product's built-in metadata JSON field.
// Creating a product with RUO metadata via Admin API
const product = await medusa.admin.products.create({
title: "BPC-157",
// ... standard fields
metadata: {
cas_number: "137317-11-0",
molecular_formula: "C62H98N16O22",
molecular_weight: 1419.53,
purity_percent: 98.0,
form: "lyophilized",
grade: "research",
sds_url: "https://cdn.research-relay.com/sds/bpc-157-sds.pdf",
storage_class: "frozen",
storage_temp_min: -20,
storage_temp_max: -15,
is_cold_chain: true,
is_hazmat: false,
ruo_only: true,
lot_trackable: true,
restricted_states: [],
},
})
Pros:
- No custom module needed. Works with vanilla Medusa immediately.
- Product metadata is included in standard API responses.
- Fastest path to a working catalog (Phase A goal).
Cons:
- No database-level validation or typing on metadata fields.
- Cannot query/filter by metadata fields efficiently (no indexes).
- No admin UI form fields without a custom widget.
Approach B: Custom Product Data Module (Recommended for Phase B+)
Create a dedicated module with a typed data model and link it to the Product.
// src/modules/product-ruo-data/models/ruo-product-data.ts
import { model } from "@medusajs/framework/utils"
const RuoProductData = model.define("ruo_product_data", {
id: model.id().primaryKey(),
cas_number: model.text(),
molecular_formula: model.text(),
molecular_weight: model.float(),
purity_percent: model.float(),
form: model.enum(["powder", "liquid", "crystal", "solution", "lyophilized"]),
grade: model.enum(["research", "analytical", "reagent", "reference"]),
sds_url: model.text(),
storage_class: model.enum(["ambient", "refrigerated", "frozen"]),
storage_temp_min: model.number().nullable(),
storage_temp_max: model.number().nullable(),
is_cold_chain: model.boolean().default(false),
is_hazmat: model.boolean().default(false),
hazmat_class: model.text().nullable(),
un_number: model.text().nullable(),
packaging_group: model.text().nullable(),
shelf_life_days: model.number().nullable(),
restricted_states: model.json().default([]),
requires_signature: model.boolean().default(false),
ruo_only: model.boolean().default(true),
lot_trackable: model.boolean().default(true),
metadata: model.json().nullable(),
})
export default RuoProductData
// src/links/product-ruo-data.ts
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import RuoDataModule from "../modules/product-ruo-data"
export default defineLink(
ProductModule.linkable.product,
RuoDataModule.linkable.ruoProductData
)
Pros:
- Full database-level type safety and validation.
- Indexed columns for efficient querying (e.g., find all cold-chain products).
- Clean separation of concerns.
- Admin widget can use typed form fields.
Cons:
- Requires module development, migration generation, and link setup.
- Requires custom API routes or workflow steps to populate data.
- Additional query joins to fetch product + RUO data together.
Recommended Path:
- Phase A: Use
metadatato get the catalog live quickly. Validate metadata in the admin widget and seed script. - Phase B: Build the
product-ruo-datamodule alongside the Product Compliance Module. Migrate data from metadata to the structured module.
8.2 State Restriction Checking at Checkout¶
State restrictions must be enforced server-side during cart completion. Implement as a workflow step that runs before payment authorization.
// src/workflows/steps/validate-state-restrictions.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
type ValidateStateRestrictionsInput = {
cart_id: string
}
export const validateStateRestrictionsStep = createStep(
"validate-state-restrictions",
async ({ cart_id }: ValidateStateRestrictionsInput, { container }) => {
const cartService = container.resolve("cartModuleService")
const productService = container.resolve("productModuleService")
// Retrieve cart with items and shipping address
const cart = await cartService.retrieveCart(cart_id, {
relations: ["items", "shipping_address"],
})
if (!cart.shipping_address?.province) {
throw new Error("Shipping address with state is required.")
}
const state = cart.shipping_address.province.toUpperCase()
// Check each item's product for state restrictions
for (const item of cart.items) {
const product = await productService.retrieveProduct(item.product_id)
const restrictedStates = product.metadata?.restricted_states as string[] || []
if (restrictedStates.includes(state)) {
throw new Error(
`"${product.title}" cannot be shipped to ${state} due to regulatory restrictions. ` +
`Please remove this item from your cart to continue.`
)
}
}
return new StepResponse({ valid: true, state })
}
)
8.3 Cold-Chain Surcharge Logic¶
The $15 cold-chain surcharge is applied as a shipping adjustment. Implement in the shipping option calculation or as a cart middleware.
// src/workflows/steps/apply-cold-chain-surcharge.ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
const COLD_CHAIN_SURCHARGE_CENTS = 1500 // $15.00
export const applyColdChainSurchargeStep = createStep(
"apply-cold-chain-surcharge",
async ({ cart_id }: { cart_id: string }, { container }) => {
const cartService = container.resolve("cartModuleService")
const productService = container.resolve("productModuleService")
const cart = await cartService.retrieveCart(cart_id, {
relations: ["items"],
})
// Check if any item requires cold-chain
let requiresColdChain = false
for (const item of cart.items) {
const product = await productService.retrieveProduct(item.product_id)
if (product.metadata?.is_cold_chain === true) {
requiresColdChain = true
break
}
}
return new StepResponse({
requires_cold_chain: requiresColdChain,
surcharge_amount: requiresColdChain ? COLD_CHAIN_SURCHARGE_CENTS : 0,
})
}
)
The surcharge can be applied via:
- A custom shipping price calculation in the ShipStation fulfillment module
- A cart adjustment/line item added during shipping method selection
- A middleware on the shipping option retrieval endpoint
The exact integration point will be determined during Phase F (ShipStation integration).
8.4 Admin Widget for RUO Product Data¶
A product detail widget injects a custom form into the admin product edit page, providing structured input for RUO metadata fields.
// src/admin/widgets/product-ruo-data-widget.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types"
const RuoProductDataWidget = ({ data }: DetailWidgetProps<AdminProduct>) => {
const metadata = data.metadata || {}
return (
<div>
<h2>RUO Chemical Data</h2>
{/* Form fields for CAS number, molecular formula, etc. */}
{/* Read metadata values for display, POST to update */}
<dl>
<dt>CAS Number</dt>
<dd>{metadata.cas_number || "Not set"}</dd>
<dt>Molecular Formula</dt>
<dd>{metadata.molecular_formula || "Not set"}</dd>
<dt>Purity</dt>
<dd>{metadata.purity_percent ? `${metadata.purity_percent}%` : "Not set"}</dd>
<dt>Storage Class</dt>
<dd>{metadata.storage_class || "Not set"}</dd>
<dt>Cold Chain Required</dt>
<dd>{metadata.is_cold_chain ? "Yes" : "No"}</dd>
<dt>Hazmat</dt>
<dd>{metadata.is_hazmat ? "Yes" : "No"}</dd>
</dl>
</div>
)
}
export const config = defineWidgetConfig({
zone: "product.details.after",
})
export default RuoProductDataWidget
8.5 Seed Script Structure¶
The seed script should create sample products with all fields populated for development and testing.
// src/scripts/seed.ts (simplified structure)
import { ExecArgs } from "@medusajs/framework/types"
export default async function seed({ container }: ExecArgs) {
const productService = container.resolve("productModuleService")
// Create product types
const peptideType = await productService.createProductTypes([
{ value: "Peptide" },
{ value: "Amino Acid" },
{ value: "Reagent" },
{ value: "Solvent" },
{ value: "Buffer" },
{ value: "Lab Supply" },
])
// Create categories
const categories = await productService.createProductCategories([
{ name: "Research Chemicals", is_internal: false },
{ name: "Peptides", parent_category_id: "..." },
{ name: "Protective Peptides", parent_category_id: "..." },
// ... etc
])
// Create BPC-157 with full metadata
await productService.createProducts([{
title: "BPC-157 (Body Protection Compound-157)",
handle: "bpc-157",
status: "published",
// ... all fields from Section 7
}])
}
8.6 Implementation Phasing¶
| Phase | What Gets Built | Product Schema Impact |
|---|---|---|
| Phase A | Vanilla Medusa + basic catalog | Products created with metadata for RUO fields. Manual validation. |
| Phase B | Product Compliance Module | Lot tracking and COA management linked to variants. RUO data optionally migrated to custom module. |
| Phase E | Compliance Checkout Module | Attestation flow enforced. State restrictions checked. Cold-chain surcharge applied. |
| Phase F | ShipStation Integration | Shipping calculations use variant weight/dimensions. Hazmat flags affect carrier selection. |
| Phase H | Admin UI Customizations | Product widget provides structured form for RUO metadata. Validation enforced in admin. |
Appendix A: Decision References¶
All business decisions referenced in this document come from docs/compliance/business-decisions.md. Key decisions affecting the product catalog:
| Decision | Summary | Impact on Catalog |
|---|---|---|
| D1 | Age requirement: 18+ | Checkout attestation requires age verification |
| D8 | State restrictions: defer to product review | restricted_states field populated per product before launch |
| D9 | P.O. Box: allow for USPS | No product-level impact, shipping method selection |
| D10 | Carriers: real-time calculated rates | Variant weight/dimensions required for rate calculation |
| D11 | Free shipping: $200+ | No product-level impact, cart-level rule |
| D14 | Signature: $150+ | requires_signature auto-calculated from variant price |
| D15 | Cold-chain: $15 flat surcharge | is_cold_chain flag on product metadata |
| D16 | Hazmat: defer to product review | is_hazmat and related fields populated per product before launch |
Appendix B: HS Code Reference¶
Common HS codes for research chemical products:
| Product Type | HS Code | Description |
|---|---|---|
| Synthetic peptides | 2933.99.9750 |
Other heterocyclic compounds, other |
| Amino acids | 2922.49.4300 |
Amino-acids and their esters, other |
| Organic reagents | 2942.00.5000 |
Other organic compounds, other |
| Solvents (DMSO) | 2930.90.4600 |
Organo-sulfur compounds, other |
| Bacteriostatic water | 3004.90.9290 |
Medicaments, other (for research use) |
| Lab supplies (vials) | 7010.90.5000 |
Glass containers |
These HS codes are set at the variant level. Confirm with a customs broker before using for any future international shipping.