Dependency Rules
Section titled “Dependency Rules”Allowed and prohibited import directions across the monorepo.
These rules are partially enforced by Oxlint (.oxlintrc.json).
Category definitions that underpin these rules: 02-package-categories.md. The dependency matrix organized by category is in that document’s “Dependency matrix” section.
The fundamental direction
Section titled “The fundamental direction”apps → packages → (nothing above packages)- Apps may import from packages.
- Packages must not import from apps.
- Packages may import from other packages, following the rules below.
Allowed dependency flow (most to least privileged)
Section titled “Allowed dependency flow (most to least privileged)”apps/dashboard routes └─ apps/dashboard features (src/features/* or packages/feature-*) └─ packages/platform-* (query, events, telemetry, i18n) └─ packages/types, packages/schemas, packages/utils └─ (no further dependencies)
apps/dashboard server functions (src/server/functions/*) └─ packages/db └─ packages/schemas └─ packages/utils └─ packages/platform-cloudflare └─ packages/types
workers (workers/*) └─ packages/db └─ packages/schemas └─ packages/utils └─ packages/platform-cloudflare └─ packages/types └─ packages/api-contract (@workers/consumer-api only) └─ packages/integration-core (@apps/dashboard only)Specific rules
Section titled “Specific rules”Packages must not import from apps
Section titled “Packages must not import from apps”✅ packages/feature-people → @legaciti/platform-query❌ packages/feature-people → apps/dashboard/src/server/authClient code must not statically import server functions
Section titled “Client code must not statically import server functions”Server functions run in the Cloudflare Worker runtime and cannot be bundled
into the client. Use the dynamic import + call() pattern.
// ✅ CORRECT — dynamic import in feature api.tsconst mod = await import("../../server/functions/people");return call(mod.getPeople, params);
// ❌ WRONG — static import from client code (blocked by Oxlint)import { getPeople } from "@host/server/functions/people";Shared UI components must not import from feature modules
Section titled “Shared UI components must not import from feature modules”✅ apps/dashboard/src/components/ui/button.tsx (no feature imports)❌ apps/dashboard/src/components/ui/button.tsx → apps/dashboard/src/features/peoplesrc/lib/ must not import from src/features/
Section titled “src/lib/ must not import from src/features/”src/lib/ is shell glue — pure helpers, app-wide utilities, and routing
infrastructure. It must not depend on product feature internals.
✅ apps/dashboard/src/lib/utils.ts (no feature imports)❌ apps/dashboard/src/lib/analytics/versionSync.ts → @/features/version-sync/apiException: src/lib/routing/middleware.ts may import @/features/auth/*
because it is the central auth middleware. This is a known, deliberate coupling.
No other lib/ file should import from src/features/.
Feature packages may import from other feature packages only via public index
Section titled “Feature packages may import from other feature packages only via public index”✅ @legaciti/feature-publications → @legaciti/feature-people (via index.ts)❌ @legaciti/feature-publications → @legaciti/feature-people/src/internal-helpersCloudflare bindings stay near runtime assembly
Section titled “Cloudflare bindings stay near runtime assembly”Bindings (env.DB_0, env.PUB_CACHE, etc.) must only be accessed in:
apps/dashboard/src/server/(server functions, auth, middleware)workers/*/src/(worker entry and infra layer)packages/platform-cloudflare/(shared binding helpers)
Never in:
src/features/src/lib/- Any
packages/feature-*
Data packages have no upward dependencies
Section titled “Data packages have no upward dependencies”packages/db, packages/schemas, packages/utils, packages/types must not
import from packages/platform-*, packages/feature-*, or any app.
packages/platform-* must not import packages/feature-*
Section titled “packages/platform-* must not import packages/feature-*”Platform packages are infrastructure for features, not the other way around.
✅ packages/feature-people → packages/platform-query❌ packages/platform-query → packages/feature-peopleWorkers must not import client-platform or feature packages
Section titled “Workers must not import client-platform or feature packages”workers/* run in a server-only Cloudflare Worker context. They must not
import React-dependent or browser-only packages.
✅ workers/ingestion-process → @legaciti/platform-cloudflare❌ workers/ingestion-process → @legaciti/platform-query (React/browser only)❌ workers/ingestion-process → @legaciti/feature-people (React UI)Client/server/runtime boundary
Section titled “Client/server/runtime boundary”@/shared/transport/server-fn-client is the client-side bridge
Section titled “@/shared/transport/server-fn-client is the client-side bridge”The file apps/dashboard/src/shared/transport/server-fn-client.ts is the
browser-safe transport bridge for TanStack server function calls. It
provides:
call()— wraps dynamic server function invocationServerFnError— error class used by featureapi.tsfilesgetRecentServerFnFailures()— in-memory failure log for the bug-report widget- version-sync request metadata helpers used by the browser
Features, app UI, and shared browser-safe code may import these APIs from this file. They must NOT import from:
@/server/functions/*(actual server functions — runtime-only, CF Worker)@/server/auth/*(session logic, OAuth, request context)@/server/events/*(server event helpers — runtime context)
Separation summary
Section titled “Separation summary”| Import source | Client-safe? | Server-only? | Notes |
|---|---|---|---|
@/shared/transport/server-fn-client | ✅ yes | no | client bridge; call(), ServerFnError |
@/shared/auth/permissions | ✅ yes | no | re-exports @platform/types only |
@/shared/auth/terminal-debug | ✅ yes | no | constant false; safe anywhere |
@/shared/auth/auth-context | ✅ yes | no | shared type only; no runtime logic |
@/server/functions/* | ❌ no | yes | use dynamic import() + call() |
@/server/auth/* | ❌ no | yes | session logic, OAuth, request context |
@/server/events/* | ❌ no | yes | server event helpers |
@legaciti/platform-cloudflare | ❌ no | yes | CF runtime bindings |
@legaciti/platform-query | ✅ yes | no | React/browser only |
Oxlint enforcement
Section titled “Oxlint enforcement”Implementation note: use patterns.regex, not patterns.group
Section titled “Implementation note: use patterns.regex, not patterns.group”Oxlint 1.60.0 silently ignores the ESLint patterns.group glob syntax for
no-restricted-imports. All boundary rules in .oxlintrc.json use
patterns.regex instead. When adding new rules, always use patterns.regex.
// ✅ correct — regex syntax works in oxlint 1.60.0{ "regex": "^@legaciti/feature-", "message": "..." }
// ❌ wrong — group glob syntax silently does nothing in oxlint{ "group": ["@legaciti/feature-*"], "message": "..." }Currently enforced (.oxlintrc.json)
Section titled “Currently enforced (.oxlintrc.json)”| Rule | Scope | Status |
|---|---|---|
| No static server-fn imports from client features and routes | apps/dashboard/src/features/**, src/routes/** | ✅ active |
No src/server/http imports from client features and routes | apps/dashboard/src/features/**, src/routes/** | ✅ active |
| No Cloudflare runtime package or worker imports from dashboard client code | apps/dashboard/src/{features,routes,app,shared}/** | ✅ active |
No direct src/server/* imports from src/shared/** | apps/dashboard/src/shared/** | ✅ active |
No direct src/server/http or src/server/functions imports from src/app/** | apps/dashboard/src/app/** | ✅ active |
| No direct app aliases/imports from feature packages | packages/feature-*/src/** | ✅ active |
| No static server-fn or DB imports from feature packages | packages/feature-*/src/** | ✅ active |
No deep cross-feature imports (@legaciti/feature-*/src/*) | packages/feature-*/src/** | ✅ active |
| Feature package overrides (api.ts, queries.ts, mutations.ts allow call()) | packages/feature-*/src/**/api.ts etc. | ✅ active |
| No feature imports from shared UI | apps/dashboard/src/components/** | ✅ active |
| No feature imports from shared UI primitives | apps/dashboard/src/shared/ui/** | ✅ active |
No feature imports from src/lib/ (except routing middleware) | apps/dashboard/src/lib/** | ✅ active |
| middleware.ts exception (auth feature imports allowed) | src/lib/routing/middleware.ts | ✅ active |
| No client-platform or feature pkgs in workers | workers/*/src/** | ✅ active |
| No app-local aliases/imports from platform packages | packages/platform-*/src/** | ✅ active |
| No feature imports from platform packages | packages/platform-*/src/** | ✅ active |
| No app-local aliases/imports from non-feature packages | packages/{domain-*,db,utils,types,schemas,api-contract,integration-core}/src/** | ✅ active |
| No route imports from non-feature packages | packages/{domain-*,db,utils,types,schemas,api-contract,integration-core}/src/** | ✅ active |
| Domain/data packages cannot import React/TanStack UI/runtime layers | packages/{domain-*,db,utils}/src/** | ✅ active |
| React rules of hooks | all TSX files | ✅ active |
| Exhaustive deps | all TSX/TS files | ✅ active |
Enforcement gaps (not yet enforced mechanically)
Section titled “Enforcement gaps (not yet enforced mechanically)”| Gap | Risk | Notes |
|---|---|---|
Feature packages currently depend on @host/* app shims | medium | Deliberate transitional coupling for extracted dashboard features |
| Cross-feature deep imports via relative paths | low | Current rule blocks package-name deep imports; relative deep imports remain for later tightening |
Route-level consumption via @host/features/* in feature pkgs | medium | Keep temporarily while route composition migrates to pure package APIs |
Violation audit (2026-Q2)
Section titled “Violation audit (2026-Q2)”Violations discovered via manual grep audit. All other boundaries were clean.
Must-fix violations
Section titled “Must-fix violations”No import-boundary violations found in the current Oxlint run.
Non-boundary lint finding (outside this boundary rollout):
apps/dashboard/src/features/workspace-settings/components/PersonTaxonomySection.tsx→ unusedloadingLabelparameter (eslint(no-unused-vars))
— fixed: moved to
apps/dashboard/src/lib/analytics/versionSync.tsapps/dashboard/src/features/version-sync/analytics.ts.
Accepted exceptions
Section titled “Accepted exceptions”| File | Import | Why accepted |
|---|---|---|
apps/dashboard/src/lib/routing/middleware.ts | @/features/auth/queries | Auth middleware is shell glue that must coordinate auth state. Only exception to lib→feature rule. |
packages/feature-*/src/** | @host/* | Transitional host-shim strategy for extracted dashboard features. Keep scoped and migrate gradually toward package-owned boundaries. |
Test-only imports (not production violations)
Section titled “Test-only imports (not production violations)”| File | Import | Notes |
|---|---|---|
apps/dashboard/src/lib/app-events.test.ts | @/server/functions/activity/activity-writer | Test-only type import |
Enforcement: how to add a new boundary
Section titled “Enforcement: how to add a new boundary”- Add a new
overridesentry in.oxlintrc.json:
{ "files": ["<glob matching restricted scope>"], "rules": { "no-restricted-imports": [ "error", { "patterns": [ { "regex": "<forbidden-regex>", "message": "<clear human-readable reason>" } ] } ] }}- Run
pnpm lintto confirm no false positives. - Document the rule in this file under “Currently enforced.”
- If existing code triggers the rule, either fix the code or add a narrow exception override scoped to the specific file.
When to add a scoped exception
Section titled “When to add a scoped exception”Add a "no-restricted-imports": "off" override scoped to a specific file only
when the import is structurally necessary and clearly safe (e.g., api.ts
files that use call() from the client bridge). Keep exceptions narrow —
never turn off a boundary rule for a whole directory without documenting why.
Import alias reference
Section titled “Import alias reference”| Alias | Resolves to |
|---|---|
@/* | apps/dashboard/src/* |
@app/* | apps/dashboard/src/app/* |
@features/* | apps/dashboard/src/features/* |
@shared/* | apps/dashboard/src/shared/* |
@server/* | apps/dashboard/src/server/* |
@routes/* | apps/dashboard/src/routes/* |
@platform/db | packages/db/src |
@platform/schemas | packages/schemas/src |
@platform/types | packages/types/src |
@platform/utils | packages/utils/src |
@platform/domain-auth | packages/domain-auth/src |
@platform/domain-people | packages/domain-people/src |
@legaciti/feature-* | packages/feature-*/src |
@legaciti/platform-* | packages/platform-*/src |
@host/* | apps/dashboard/src/* (resolved in feature packages only, via Vite alias or host-tsc-shims) |