# Dependency Flow ``` +-------------+ +-----------------+ +-----------+ | apps/web- | | apps/web- | | apps/cms | | next | | tanstack | | | +------+------+ +--------+--------+ +-----+-----+ | | | +------------------+--------------+ | | | | | | | +----v-----+ +-----v------+ +-----v----v---+ +-------v------+ | core-api | | core-trpc | | feature | | core-cms | | | | | | packages | | | +-----+----+ +-----+------+ +------+-------+ +-------+------+ | | | | | | | | +--+-------+------+---------------+----+ +-------------+ | | | | +----v---+ +-v---------+ +-------v---v---+ | core- | | core-ui | | core-shared | | shared | | | | | +--------+ +-----------+ +----------------+ Boundary rules (enforced by ESLint + Turborepo boundaries): app → app, core, core-composition, feature, tooling feature → core, feature, tooling core → core, core-composition, tooling core-composition → core, core-composition, feature, tooling tooling → tooling feature → feature: a feature may import another feature's PUBLIC exports (its @repo/ contract barrel — types, errors, schemas, event contracts). It must NOT reach another feature's internals, and cross-feature behaviour still flows through IEventBus — never a direct use-case call. Composition exceptions: core-api → @repo//api (subpath only) core-cms → @repo//cms (subpath only) App-side feature subpaths: @repo/ — contracts (types, errors, schemas, IUseCase aliases, router type, constants) @repo//ui — UI artifacts (query builders, components) ``` ## Concrete examples Allowed: ```ts // in apps/web-next import { appRouter } from "@repo/core-api"; import { NextTrpcProvider } from "@repo/core-trpc/next"; import { bindProductionBlog } from "@repo/blog/di/bind-production"; import { signInInputSchema, type SignInInput } from "@repo/auth"; // contracts import { articleBySlugQuery } from "@repo/blog/ui"; // queries // in packages/blog import { slugifyIfMissing } from "@repo/core-shared/payload"; import { userSignedUpEvent } from "@repo/auth"; // ✓ another feature's public contract // in packages/core-api import { blogRouter } from "@repo/blog/api"; // composition exception import { router } from "@repo/core-shared/trpc/init"; // core → core fine // in packages/core-cms import { articles } from "@repo/blog/cms"; // composition exception ``` Disallowed: ```ts // in packages/blog (reaching another feature's internals) import { signInUseCase } from "@repo/auth/src/application/use-cases/sign-in.use-case"; // ❌ not a public export // in packages/blog (deep import past public exports) import { articles } from "@repo/blog/src/integrations/cms/collections/articles"; // ❌ no-private // in packages/core-shared import { blogRouter } from "@repo/blog/api"; // ❌ core → feature import { ArticleNotFoundError } from "@repo/blog"; // ❌ core → feature // (defineErrorMiddleware takes Error // constructors as args from features — // core-shared never imports them) // in packages/core-shared (or any non-composition core package) import { someBlogThing } from "@repo/blog"; // ❌ core → feature (only core-api/core-cms have exception) // in apps (using the wrong subpath) import { articleBySlugQuery } from "@repo/blog"; // ❌ queries live on ./ui import { Article } from "@repo/blog/ui"; // ❌ types live on the root subpath ``` ## Enforcement strategy Three layers work in tandem: 1. **`package.json` dependencies** — if you didn't declare it, you can't import it 2. **`exports` map** — blocks deep imports; only public subpaths are accessible 3. **Two parallel automated checks** (both enforcing the same five-tag model): - **ESLint `eslint-plugin-boundaries`** runs at lint time, catching direct-import violations - **Turborepo `boundaries`** runs at build time, validating the entire workspace graph including transitive dependencies The two enforcement layers are independent but complementary. ESLint is stricter on per-import context (e.g., file-specific exemptions via `// @boundaries-ignore`), while Turborepo catches transitive issues that lint-time checking misses. Run `pnpm lint` and `pnpm turbo boundaries` in CI to catch all violations. ## TRACER / LOGGER / METRICS / AUDIT (ADR-014, ADR-017, ADR-018) The instrumentation layer is **per-feature container** but **app-wide instance**: each feature container binds `INSTRUMENTATION_SYMBOLS.ITracer`, `INSTRUMENTATION_SYMBOLS.ILogger`, and `INSTRUMENTATION_SYMBOLS.IMetrics` to the SAME instances, constructed once by the app's `bindAll()` dispatcher (Rule 0). **Substrate:** OpenTelemetry SDK. Sentry is the exporter via `@sentry/opentelemetry`. PII scrubbing runs at the OTel processor layer (`PiiScrubSpanProcessor` + `PiiScrubLogRecordProcessor`) before the Sentry exporter sees the data. Feature code is never coupled to the OTel SDK or Sentry SDK directly (ADR-017). **Audit is a parallel channel** with different durability and redaction contracts (ADR-018). Whereas OTel is sampled and pruned (best-effort observability), audit is append-only and retained for compliance. `auditLog` is bound by `resolveAudit` and added to `ctx` for feature binders that opt in; the binding is wrapped in `TraceIdEnrichingAuditLog` so every `AuditEntry` auto-correlates with the active OTel span via `correlationId`. See `docs/guides/audit-and-compliance.md` and the visual explainer at `docs/architecture/audit-and-compliance-explainer.html`. ``` apps/web-next/src/server/bind-production.ts (bindAll) │ ├─ Rule 0: WEB_NEXT_SENTRY_DSN set? │ yes → bindOtelInstrumentation(sharedContainer, { dsn, app: "web-next" }) │ → initOtelServerNode(dsn, ...) → OTel SDK + Sentry exporter + PII scrub processors │ no → bindNoopInstrumentation(sharedContainer) │ ↓ │ tracer (OtelTracer) + logger (OtelLogger) + metrics (OtelMetrics) instances │ ↓ ├─ resolveEventsAndJobs* → IEventBus + IJobQueue (ADR-015) │ production → PayloadJobsEventBus + PayloadJobQueue │ dev-seed → InMemoryEventBus + InMemoryJobQueue │ ↓ ├─ resolveRealtime → IRealtimeBroadcaster + IRealtimeHandlerRegistry (ADR-016) │ server.ts → SocketIORealtimeBroadcaster + RealtimeHandlerRegistry (passed in from server.ts) │ page/test → InMemoryRealtimeBroadcaster + RealtimeHandlerRegistry (defaults) │ ↓ ├─ resolveAudit → IAuditLog (ADR-018) │ production → TraceIdEnrichingAuditLog( MultiSinkAuditLog([StdoutJsonAuditLog, PayloadAuditLog]) ) │ dev-seed → TraceIdEnrichingAuditLog( StdoutJsonAuditLog ) — or Noop when core-audit is absent │ ↓ ├─ build ctx: BindProductionContext = { config, tracer, logger, metrics?, bus, queue, realtime, realtimeRegistry, auditLog } │ Required: tracer, logger, config (production only) │ Optional: metrics, bus, queue, realtime, realtimeRegistry, auditLog (guard with ?. when used) │ ↓ ├─ bindProductionBlog(ctx: BindProductionContext) │ │ │ ├─ blogContainer.bind(TRACER).toConstantValue(tracer) │ ├─ blogContainer.bind(LOGGER).toConstantValue(logger) │ ├─ ArticlesRepository(config, tracer, logger) → bound to IArticlesRepository │ │ (real repo: inline this.tracer.startSpan + this.logger.captureException per method) │ ├─ withSpan(tracer, ..., withCapture(logger, ..., useCase(deps))) → UseCase symbol │ ├─ withSpan(tracer, ..., withCapture(logger, ..., controller(uc))) → Controller symbol │ ├─ // bus.subscribe(...) (ADR-015, gen event consume) │ ├─ // queue.register(...) in dev-seed; Payload task in prod │ └─ // realtimeRegistry.register(...) (ADR-016, gen realtime handler) │ └─ (same for auth, marketing-pages, navigation, media — each receives the same ctx) ``` **`BindContext` shape (from `@repo/core-shared/di`):** | Field | Type | Required | Notes | | ------------------ | ------------------------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------- | | `tracer` | `ITracer` | always | Resolved by Rule 0 (OTel+Sentry vs Noop) | | `logger` | `ILogger` | always | Resolved by Rule 0 | | `metrics` | `MetricsProtocol?` | optional | Resolved by Rule 0; per-feature adoption is opportunistic | | `config` | `SanitizedConfig` | production only | Present in `BindProductionContext`, absent in `BindContext` | | `bus` | `EventBusProtocol?` | optional | `IEventBus` at the aggregator; protocol surface at binders | | `queue` | `IJobQueue?` | optional | Present when `core-shared/jobs` is wired | | `realtime` | `RealtimeBroadcasterProtocol?` | optional | `IRealtimeBroadcaster` at the aggregator | | `realtimeRegistry` | `RealtimeRegistryProtocol?` | optional | `IRealtimeHandlerRegistry` at the aggregator | | `auditLog` | `AuditLogProtocol?` | optional | `IAuditLog` at the aggregator; pre-wrapped in `TraceIdEnrichingAuditLog` so callers don't supply `correlationId` (ADR-018) | | `analytics` | `AnalyticsProtocol?` | optional | `IAnalytics` product-analytics channel at the aggregator (ADR-024) | | `consentFactory` | `ConsentFactoryProtocol?` | optional | builds a per-subject consent checker; present when `core-consent` is wired (ADR-025) | | `rateLimit` | `IRateLimit?` | optional | per-use-case rate-limit budgets; present when rate limiting is wired (ADR-025) | Feature binders destructure `ctx` and use optional fields with `?.` or cast to the full interface when the feature unconditionally requires them (e.g. `bus as IEventBus` when a use case always needs the event bus). **Why per-feature containers also get the binding:** lets internal DI-resolved code in a feature pull TRACER/LOGGER without going through the app dispatcher. In practice, only repository classes and feature-internal services would use this — controllers and use cases receive instrumentation via the bind-time wrapper. **Why the shared container exists at all:** isolates Rule 0 resolution from feature containers. Feature containers don't need to know if Sentry is the exporter or not — they just receive an `ITracer` instance. **Boundary rule:** feature packages MUST NOT import `@sentry/*` or `@opentelemetry/sdk-*` directly (R40 + R52, ESLint-enforced). The OTel bridge (`otel/sentry-bridge.ts`), browser init files (`sentry/init-client*.ts`), and app-level `instrumentation*.{ts,mjs}` / `next.config.{mjs}` / `vite.config.{ts}` entries are the only allowlisted paths. Likewise, features MUST NOT import `@repo/core-audit` directly — they consume `IAuditLog` via the `AuditLogProtocol` type re-exported from `@repo/core-shared/di/bind-protocols`.