domain

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.

DomainStatus: Implemented in api-v1Airtable-backed

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

ConceptPersistence todayBadge
ProjectProductVariantAirtable project_product_variants (ADR-0083); referenced by PDG product assignmentImplemented in api-v1
ProjectCollectionAirtable user_product_collections; portal snapshot in PostgresImplemented in api-v1
ProjectCollectionItemAirtable user_product_collection_itemsImplemented in api-v1
ProductReferenceValue object on each item (PM catalog product / variant / MTO id — not a Design product_variants rec)Implemented in api-v1
PartTypeAirtable part types, generated from an MTO templateImplemented in api-v1
ProductCollectionDeckJobAirtable deck jobs; Slides.com is an external publisherImplemented 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:

  1. api-v1 PublishToPortalUseCase / UnpublishFromPortalUseCase (ForPublishingToPortal, ForUnpublishingFromPortal)
  2. Aggregate publishToPortal / unpublishFromPortal (draft ↔ published)
  3. ForSyncingPortalData implemented by PrismaPortalSyncAdapter (upsert or unpublish in Postgres)
  4. 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.

MethodPathBehavior
POST/project-collections/:id/add-itemsAdd existing collection items to a collection
POST/project-collections/:id/remove-itemsSoft-delete items on the aggregate
POST/project-collections/:id/select-itemsScript-flag select; propagates to active items
POST/project-collections/deleteDestroy collections and/or items in Airtable
POST/projects/:projectId/actions/add-variantsAdd catalog variants to {projectName}-default
POST/projects/:projectId/actions/publish-to-portalPublish collections to the portal snapshot
POST/projects/:projectId/actions/unpublish-from-portalMove collections back to draft
POST/projects/:projectId/project-product-variantsMaterialize a made-to-order project variant. 201 and Location
POST/projects/:projectId/project-product-variants/:id/actions/updateReplace made-to-order selections. 200
POST/projects/:projectId/project-product-variants/:id/actions/deleteWithdraw a made-to-order project variant. 200
GET/projects/:projectId/project-product-variantsList project variants. Stock rows are hydrated from product marketplace
GET/projects/:projectId/project-product-variants/:idDetail. Made-to-order returns the snapshot and selections. An unreadable snapshot is 500
POST/product-mtos/:productMtoId/generate-partMaterialize a PartType from an MTO template
POST/project-collections/:productCollectionId/publish-slidesSync Slides.com deck publish
POST/slides/publish-collectionAsync deck job (ProductCollectionDeckJob)
GET/slides/jobs/:jobId/statusDeck job status
GET/slides/launchBlind 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/*.

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. ProjectProductVariant already emits for project-catalog membership; that is separate from portal snapshot sync.