Extend the platform
You extend the platform by implementing a port — the ports are the extension surface. Ports live in packages/platform-domain/**/ports/ (infrastructure-free); your adapter lives in apps/api/**/adapters/.
Decide generic vs consumer-specific
- Generic (any future consumer would want it) → goes in
packages/platform-ingestion/. Example: a new source reader, a new port. - Consumer-specific (only one app cares) → goes in the consumer’s directory. Example: classifieds-listing parsing, bookmark-tag derivation.
Add a pipeline step
Implement IWorkflowStep (or extend BaseWorkflowStep) and register it in the StepRegistry. Gate registration on its dependency so it only appears when configured — that’s how enrich / analyze only register when an LLM key is set.
Add an export destination
Implement IExportTarget and add its key to EXPORT_DESTINATIONS (the canonical map shared by presets and buildJobs). ExportStep dispatches by config.destination, threading context.metadata.userId into the destination config for owner-scoping.
Add an ingestion source
Implement ISourceReader in packages/platform-ingestion/. The consumer seam is IContentHandler — a consumer app implements it to translate BaseContent into its own entity.
Add an LLM provider
Implement ILlmClient. The enrich / analyze steps call the LLM through this provider-agnostic port (Cline / Anthropic / OpenRouter today), so adding a provider is an adapter, not a code change in the steps.
Conventions
- TypeScript + Bun; checks are
bun run typecheck/bun run lint/bun run test:unit. - Keep the domain package infrastructure-free (no
drizzle-orm,pg-boss,fs,http). - Import from the
@abeauvois/platform-domainbarrel, not deep relative paths.