PackagesPackage contract

Package contract

Every @withpotter/* package abides by one contract so the engine can compose any entitled subset without surprises. These are the rules the engine relies on.

The seven rules

1. Only dist/ is published

Packages publish their build output (dist/) via npm pack. Source (src/) is never installed into a box. Boxes consume compiled JavaScript, not TypeScript.

2. forRoot() is the only host contract

A package exposes a single DynamicModule factory: forRoot(options). Options are limited to:

  • authGuard: Type<CanActivate> — bound under the AUTH_GUARD token.
  • Feature-specific config with safe defaults.
  • Optional imports and providers escape hatches for adapter bindings.

No other public surface is assumed by the host.

3. Self-owned models

Each package exports a <PKG>_MODELS array (for example TENANT_CORE_MODELS = [Tenant, Account, ...]). The engine aggregates all of them into ENGINE_MODELS. A package never reaches into another package’s tables directly.

4. Ports, not imports

Cross-package dependencies go through DI tokens (@Inject(TOKEN)), with the token definitions living in @withpotter/shared. A package never imports another engine package directly. This is what makes adapters swappable.

5. Migrations are bundled

Each package ships its migrations and exports their path. The engine globs node_modules/@withpotter/*/migrations/*.js, sorts by timestamp, and runs each once. See Engine → OpenAPI → Migrations.

6. Honest peer dependencies

Framework packages (NestJS, Sequelize, etc.) are declared in peerDependencies, not dependencies, so a box resolves a single shared copy rather than many conflicting ones.

7. Outside-monorepo smoke test

A CI workflow (engine-smoke.yml) packs each package and boots it inside a throwaway Nest app, proving the package composes cleanly outside the monorepo — exactly as a box consumes it.

These rules are why the engine can stay a thin shell: it makes no assumptions beyond forRoot(), aggregated models, port tokens, and bundled migrations.

Why it matters for operators

  • Predictable installs. Any entitled subset composes; there are no hidden cross-package imports to break.
  • Swappable infrastructure. Because dependencies are ports, you replace a mailer or payment gateway without forking a package.
  • Zero-step migrations. Adding an entitled package and restarting the engine brings its tables online automatically.