domain

Service Marketplace

Bounded context for sourcing, profiling, and dispatching independent service providers. Active api-v1 implementation persists Provider, Professional, ServiceArea, and City; marketplace profile overlays remain stubbed.

DomainStatus: Implemented in api-v1Airtable-backed

Overview

Service Marketplace owns provider sourcing in apps/api-v1/src/modules/service-marketplace. It replaces the deprecated name Labor Marketplace. A Provider is a marketplace projection of a service-providing organization, created from a source URL or entered manually. Future party-directory integration links that projection to a canonical Organization.

The module follows api-v1 hexagonal structure with Airtable as the persistence adapter for Provider, Professional, ServiceArea, and City. ProviderProfile, ServiceOffering, LicenseCredential, and InsuranceCredential exist as TypeScript domain types and events, but their stores are in-memory placeholders and the HTTP detail routes return 501. Do not treat those four as persisted Airtable behavior.

Implementation Status

Status: Implemented in api-v1

The future api-v2 Prisma module is the long-term target for first-class profile, offering, and credential persistence. This page documents the active api-v1 implementation first.

Aggregates and supporting types

ConceptPersistence todayBadge
ProviderAirtable providersImplemented in api-v1
ProfessionalAirtable professionals (HTTP register returns 501)Implemented in api-v1
ServiceAreaAirtable service areasImplemented in api-v1
CityAirtable citiesImplemented in api-v1
ProviderProfile, ServiceOffering, LicenseCredential, InsuranceCredentialIn-memory pending repositories; HTTP 501Stubbed / Planned for api-v2

Provider is the aggregate root. ProviderProfileId is branded as the same identity as ProviderId.

Create from URL

Create Provider From URL is the active scrape-and-persist path. CreateProviderFromUrlUseCase calls DeterministicProviderScraperAdapter, which loads a module-local Playwright scraper (currently Houzz) — not the shared-scraping catalog service and not BrightData. After persist, the use case optionally calls party-directory ForRegisteringOrganizations.findOrCreateByDomain and links the provider via Provider.linkToOrganization.

There is no nested provider-ingestion System: the scrape path does not use shared-scraping or BrightData, so a product-ingestion-style System would invent machinery that is not there.

Primary Code References

  • Module root: apps/api-v1/src/modules/service-marketplace
  • Glossary: apps/api-v1/src/modules/service-marketplace/GLOSSARY.md
  • Composition: apps/api-v1/src/modules/service-marketplace/composition/service-marketplace.composition.ts
  • REST: apps/api-v1/src/modules/service-marketplace/adapters/inbound/rest
  • Airtable base id: appf6O7p7uadUeS4o (AIRTABLE_SERVICE_MARKETPLACE_BASE_ID)

HTTP surface

Canonical mount: /api/v1/service-marketplace.

MethodPathBehavior
POST/providers/from-urlCreate-from-URL scrape (202)
DELETE/providers/:providerIdSoft-delete provider
POST / DELETE/providers/:providerId/service-areasAdd or remove coverage
POST/providers/:providerId/profile (and offerings, licenses, insurance, professionals)501 placeholders
POST/contractors/...Same router as /providers (legacy alias)

Also mounted: /api/v1/labor-marketplace and /api/v1/service-marketplace/contractor-from-url legacy aliases. There is no colocated *.openapi.ts for this module yet — do not invent OpenAPI operation ids.

Open Questions

  • When first-class ProviderProfile / offering / credential persistence lands in api-v2, which Airtable tables (if any) remain a read model versus a write model?
  • Whether provider scrape should eventually share the shared-scraping / BrightData path used by product-marketplace. Today it does not.