Design Management
Bounded context for designer-curated artifacts in the design base. Owns ProjectCollection, collection items, ProjectProductVariant, product references, PartType, and Slides.com deck jobs. Product catalog records stay in product-marketplace.
Overview
Design Management owns designer-curated artifacts in apps/api-v1/src/modules/design-management. A ProjectCollection (short name: Collection) is a designer-created collection or favorites list of products for a project or design workflow. Catalog records remain owned by Product Marketplace; this module references them through ProductReference and read ports.
The module follows api-v1 hexagonal structure. Collections, items, part types, and deck jobs persist in the Airtable design base (appDr2xVXbdJ5b08R, AIRTABLE_DESIGN_BASE_ID). Portal publish writes a Postgres snapshot via PrismaPortalSyncAdapter; the client portal reads that snapshot from api-v2. There is no nested System: api-v1 and api-v2 already collaborate under Backend Platform.
This domain emits integration events for the aggregates it owns. ProjectProductVariant records ProjectProductVariantProjected when a catalog variant is written onto project_product_variants, and ProjectProductVariantReleased when that row is destroyed because it is no longer referenced by any collection on the project. A made-to-order row records ProjectProductVariantMaterialized, ProjectProductVariantConfigurationRevised, and ProjectProductVariantWithdrawn. Those three are dispatched after the Airtable write succeeds and have no handler. Project variant list, detail, and PDF reads are design queries; stock catalog fields and PDF bytes come from product marketplace. Made-to-order detail returns the stored configuration snapshot. Document reads of a made-to-order id stay not implemented.
It receives product-marketplace ProductVariantCatalogChanged and RelatedProductAdded and refreshes Design catalog fields already stored on existing ProjectProductVariant rows. Those events do not create rows and do not publish or delete Supabase product-image objects.
ProjectCollection use cases still return application results. Earlier collection event types were removed because no consumer needed them. Collection commands are not a substitute for the ProjectProductVariant events above.
Implementation Status
Status: Implemented in api-v1
Portal reads already live on api-v2 (portal-read + GET /api/v2/client-portal/...). That is a BFF over published snapshots, not a replacement for Airtable collection curation. See Backend Platform — Portal read path.
Aggregates and supporting types
| Concept | Persistence today | Badge |
|---|---|---|
ProjectProductVariant | Airtable project_product_variants (ADR-0083); referenced by PDG product assignment | Implemented in api-v1 |
ProjectCollection | Airtable user_product_collections; portal snapshot in Postgres | Implemented in api-v1 |
ProjectCollectionItem | Airtable user_product_collection_items | Implemented in api-v1 |
ProductReference | Value object on each item (PM catalog product / variant / MTO id — not a Design product_variants rec) | Implemented in api-v1 |
PartType | Airtable part types, generated from an MTO template | Implemented in api-v1 |
ProductCollectionDeckJob | Airtable deck jobs; Slides.com is an external publisher | Implemented in api-v1 |
ProjectCollection is the aggregate root. Collection item add/remove/select and portal publish/unpublish run on that aggregate. PartType and ProductCollectionDeckJob are separate persisted types in this module.
Boundary with Product Marketplace
Product browse, search, filter, refresh/reload/delete, image publishing, onboarding, and dimension migration stay in product-marketplace. Manufacturer or vendor product lines are VendorProductCollection, not ProjectCollection.
This module looks up MTO templates through AirtableMtoTemplateAdapter. Add to Project is a Product Marketplace steward action. It calls this module’s POST /projects/:projectId/actions/add-variants write port. Design Management owns the resulting Project Product Variant row and the default Project Collection; it does not own the Add to Project trigger or the core stock catalog.
Delete from the project is Design Management. Removing a product so it is no longer on any collection for that project runs releaseIfUnreferenced and destroys the project_product_variants row, which emits ProjectProductVariantReleased. Soft-remove from a curated list that re-homes the item onto {projectName}-default does not release the row.
Graph product assignment is owned by project-delivery-graph and references variant domain_id. Extra collection membership uses POST /project-collections/:id/add-items. This module does not own catalog images rows or the product-images bucket.
Portal publish
Publish Collection To Portal is the write path:
- api-v1
PublishToPortalUseCase/UnpublishFromPortalUseCase(ForPublishingToPortal,ForUnpublishingFromPortal) - Aggregate
publishToPortal/unpublishFromPortal(draft↔published) ForSyncingPortalDataimplemented byPrismaPortalSyncAdapter(upsert or unpublish in Postgres)- Airtable save of collection portal fields
Reads stay on api-v2: apps/api-v2/src/modules/portal-read and GET /api/v2/client-portal/projects/:projectId/project-collections. Documented on Backend Platform. Do not treat this split as the product-marketplace or BIM migration.
Slides.com
ProductCollectionDeckJob is a publication job that calls Slides.com through SlidesComProductCollectionDeckAdapter. Slides.com is an external publisher, not a catalog service. Do not add a Slides.com EventCatalog service.
Primary Code References
- Module root:
apps/api-v1/src/modules/design-management - Glossary:
apps/api-v1/src/modules/design-management/GLOSSARY.md - Composition:
apps/api-v1/src/modules/design-management/composition/design-management.composition.ts - REST:
apps/api-v1/src/modules/design-management/adapters/inbound/rest/design - Airtable base id:
appDr2xVXbdJ5b08R(AIRTABLE_DESIGN_BASE_ID) - Portal write adapter:
apps/api-v1/src/modules/design-management/adapters/outbound/persistence/portal/PrismaPortalSyncAdapter.ts
HTTP surface
Canonical mount: /api/v1/design.
| Method | Path | Behavior |
|---|---|---|
POST | /project-collections/:id/add-items | Add existing collection items to a collection |
POST | /project-collections/:id/remove-items | Soft-delete items on the aggregate |
POST | /project-collections/:id/select-items | Script-flag select; propagates to active items |
POST | /project-collections/delete | Destroy collections and/or items in Airtable |
POST | /projects/:projectId/actions/add-variants | Add catalog variants to {projectName}-default |
POST | /projects/:projectId/actions/publish-to-portal | Publish collections to the portal snapshot |
POST | /projects/:projectId/actions/unpublish-from-portal | Move collections back to draft |
POST | /projects/:projectId/project-product-variants | Materialize a made-to-order project variant. 201 and Location |
POST | /projects/:projectId/project-product-variants/:id/actions/update | Replace made-to-order selections. 200 |
POST | /projects/:projectId/project-product-variants/:id/actions/delete | Withdraw a made-to-order project variant. 200 |
GET | /projects/:projectId/project-product-variants | List project variants. Stock rows are hydrated from product marketplace |
GET | /projects/:projectId/project-product-variants/:id | Detail. Made-to-order returns the snapshot and selections. An unreadable snapshot is 500 |
POST | /product-mtos/:productMtoId/generate-part | Materialize a PartType from an MTO template |
POST | /project-collections/:productCollectionId/publish-slides | Sync Slides.com deck publish |
POST | /slides/publish-collection | Async deck job (ProductCollectionDeckJob) |
GET | /slides/jobs/:jobId/status | Deck job status |
GET | /slides/launch | Blind Slides.com launcher |
Project product variant commands and queries are in the generated api-v1 OpenAPI: materializeMadeToOrderProjectVariant, reviseMadeToOrderProjectVariant, withdrawMadeToOrderProjectVariant, listProjectProductVariants, and getProjectProductVariant. Legacy /api/app/design/* redirects to /api/v1/design/*.
Documentation Links
- api-v1 service:
/docs/services/api-v1 - api-v2 service:
/docs/services/api-v2 - Ubiquitous language:
/docs/domains/design-management/language - Product Marketplace:
/docs/domains/product-marketplace - Backend platform (portal read path):
/docs/systems/backend-platform - ADRs:
docs/adr/README.md
Open Questions
- When collection curation moves to api-v2, which Airtable tables remain a steward write model versus a read model?
- Whether portal publish/unpublish should also emit a collection event.
ProjectProductVariantalready emits for project-catalog membership; that is separate from portal snapshot sync.