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/.

  • 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.

Implement IWorkflowStep (or extend BaseWorkflowStep) and register it in the StepRegistry. Most steps register unconditionally and resolve their credential (an LLM, Gmail, Notion, … Channel) per run, failing clearly at that point when the owning user has no connected channel of that scheme — see Steps for the current registration model.

Implement IDispatchTarget (packages/platform-domain/src/dispatch/ports/IDispatchTarget.ts) and add its scheme to DESTINATION_SCHEMES (packages/platform-domain/src/connector/Destination.ts — the canonical vocabulary shared by presets and buildJobs). The dispatch step (DispatchStep) routes by config.destination’s URI scheme, threading context.metadata.userId/orgId into the destination config for owner-scoping.

Implement ISourceReader in packages/platform-ingestion/. The consumer seam is IContentHandler — a consumer app implements it to translate BaseContent into its own entity.

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.

  • 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.