Skip to content

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-domain barrel, not deep relative paths.