The Merch system supports per-item fulfillment routing — each order item can be assigned to a different supplier (e.g. a t-shirt to IMAP Japan, a plaque to a US manufacturer). Suppliers are modeled as merch_fulfillment_provider rows in the database.
Supplier users are people who can log in and see orders assigned to their supplier. They are linked by email via the merch_fulfillment_provider_partner table (a mapping of email → provider id).
Supplier access is not based on a Cognito/DynamoDB role. Instead:
merch_fulfillment_provider_partnerThis avoids the complexity of managing roles during signup — an email can be linked before the user even has a Zooly account.
User visits / (root page)
→ getVerifiedUserInfo() to check session
→ If admin role → redirect to /admin/merch (full dashboard)
→ If email in merch_fulfillment_provider_partner → redirect to /admin/merch/orders
→ Otherwise → redirect to /merch (consumer storefront)
Once on /admin/merch, the merch-admin SPA calls GET /api/merch/admin/me which returns:
{ "role": "supplier", "supplierId": "supplier-id-here", "user": {...} }
The client stores this in AdminAuthContext and:
/orders route (other routes bounce back)| Table | Purpose |
|---|---|
merch_fulfillment_provider | Supplier entity (code, display name, kind, driver, active flag) |
merch_fulfillment_provider_partner | Email → provider mapping (who can access this supplier's orders) |
merch_item.supplier_id | Which supplier is assigned to fulfill this item (null means unassigned) |
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/merch/admin/me | Any authenticated user | Returns viewer role (admin/supplier/none) + provider scope |
GET /api/merch/admin/orders | Admin or supplier | Lists orders; suppliers see only their items |
PATCH /api/merch/admin/orders/[id] | Admin or supplier | Update order; suppliers limited to fulfillmentStatus and adminNotes |
POST /fulfillment-providers/[id]/partners | Admin only | Link an email to a supplier |
DELETE /fulfillment-providers/[id]/partners/[id] | Admin only | Unlink an email from a supplier |
requireSupplierScope)The requireSupplierScope function in @zooly/merch-admin-srv gates all order endpoints:
admin role → returns { user, providerId: null } (no scope filter)merch_fulfillment_provider_partner → returns { user, providerId } (scoped)All order queries filter by providerId when non-null, ensuring suppliers only see their own items.
merch_fulfillment_provider_partner/ → root page detects supplier status, redirects to /admin/merch/orders| Package | File | Responsibility |
|---|---|---|
@zooly/db | access/merch/merch-fulfillment-provider-partner.ts | DB access: link/unlink/list/lookup supplier users |
@zooly/merch-admin-srv | require-admin.ts | requireSupplierScope() auth gate |
@zooly/merch-admin-client | context/admin-auth-context.tsx | Client-side role/scope state |
@zooly/merch-admin-client | components/layout/admin-layout.tsx | Route gating (suppliers → /orders only) |
@zooly/merch-fulfillment-srv | send-email.ts | sendSupplierLinkedEmail(), sendOrderAssignedToSupplierEmail() |