Package Categories
Section titled “Package Categories”Canonical category model for all top-level code areas in this monorepo.
Seven categories are defined: app · worker · feature · domain · platform · contract · shared UI
Related docs:
- 00-repo-map.md — what each area owns
- 01-code-placement.md — where to add new code
- 03-dependency-rules.md — allowed import directions
Quick reference
Section titled “Quick reference”| Category | Location pattern | Deployable | React | CF Runtime | D1/KV |
|---|---|---|---|---|---|
| app | apps/{name}/ | yes | yes | yes (SSR + server fns) | yes (via server fns) |
| worker | workers/{name}/ | yes | no | yes | yes (directly) |
| feature | packages/feature-{name}/ or apps/*/src/features/{name}/ | no (library) | yes | no | no |
| domain | packages/{name}/ (db, utils) | no (library) | no | no | yes (db only) |
| platform | packages/platform-{name}/ | no (library) | conditionally | conditionally | no |
| contract | packages/{name}/ (schemas, types, api-contract, integration-core) | no (library) | no | no | no |
| shared UI | apps/dashboard/src/components/ | no (in-app) | yes | no | no |
Current repo mapping
Section titled “Current repo mapping”| Path | Category | Import alias | Notes |
|---|---|---|---|
apps/dashboard/ | app | @apps/dashboard | TanStack Start + Cloudflare Worker |
apps/marketing/ | app | @apps/marketing | Astro, public site only |
apps/docs/ | app | @apps/docs | Starlight, Cloudflare Pages |
apps/legaciti.org/ | ⚠ deprecated | — | No package.json; legacy static content — see §Ambiguities |
apps/my.legaciti.org/ | ⚠ deprecated | — | No package.json; pre-monorepo artifact — see §Ambiguities |
apps/docs.legaciti.org/ | ⚠ deprecated | — | No package.json; build artifact only |
workers/ingestion-orchestrator/ | worker | @workers/ingestion-orchestrator | Queue orchestrator |
workers/ingestion-process/ | worker | @workers/ingestion-process | Heavy ingestion processor |
workers/notification/ | worker | @workers/notification | Notification queue consumer |
packages/platform-ingestion/ | platform | @platform/ingestion | Shared ingestion runtime extracted from the legacy worker package |
workers/consumer-api/ | worker | @workers/consumer-api | Hono HTTP, read-only |
workers/rate-limiter/ | worker | @workers/rate-limiter | Durable Object |
workers/log-processor/ | worker | @workers/log-processor | Log tail consumer |
packages/feature-activity/ | feature | @legaciti/feature-activity | extracted |
packages/feature-people/ | feature | @legaciti/feature-people | extracted |
packages/feature-publications/ | feature | @legaciti/feature-publications | extracted |
apps/dashboard/src/features/admin/ | feature | (in-app) | not yet extracted |
apps/dashboard/src/features/auth/ | feature | (in-app) | auth client concerns |
apps/dashboard/src/features/bug-reports/ | feature | (in-app) | not yet extracted |
apps/dashboard/src/features/dashboard/ | feature | (in-app) | shell/home view |
apps/dashboard/src/features/integrations/ | feature | (in-app) | not yet extracted |
apps/dashboard/src/features/site-tools/ | feature | (in-app) | not yet extracted |
apps/dashboard/src/features/workspace-settings/ | feature | (in-app) | app-owned settings UI + data/query layer |
apps/dashboard/src/features/admin-users/ | feature | (in-app) | not yet extracted |
apps/dashboard/src/features/email-templates/ | feature | (in-app) | not yet extracted |
apps/dashboard/src/features/version-sync/ | feature | (in-app) | not yet extracted |
packages/db/ | domain | @platform/db | Drizzle ORM, D1 schema |
packages/utils/ | domain | @platform/utils | DOI normalization, sharding, JSON Patch |
packages/platform-cloudflare/ | platform (server) | @legaciti/platform-cloudflare | CF runtime adapters |
packages/platform-query/ | platform (client) | @legaciti/platform-query | TanStack Query infra |
packages/platform-telemetry/ | platform (client) | @legaciti/platform-telemetry | metrics/timing |
packages/platform-events/ | platform (client) | @legaciti/platform-events | event bus |
packages/platform-i18n/ | platform (client) | @legaciti/platform-i18n | i18next bootstrap |
packages/schemas/ | contract | @platform/schemas | Zod schemas |
packages/types/ | contract | @platform/types | TS interfaces |
packages/api-contract/ | contract | @platform/api-contract | OpenAPI, integrations API |
packages/integration-core/ | contract | @platform/integration-core | ⚠ contract + runtime mix — see §Ambiguities |
apps/dashboard/src/components/ui/ | shared UI | @/* → src/components/ui/ | shadcn primitives |
apps/dashboard/src/components/ | shared UI | @/* → src/components/ | app-wide shared components |
packages/host-tsc-shims/ | tooling | @host/* | TS path shims only |
tooling/ | tooling | — | Vitest coverage preset |
cli/ | tooling/CLI | legaciti bin | admin CLI binary |
src/monitoring/ | deleted | — | Removed 2026-04-23; canonical: packages/platform-ingestion/src/infra/telemetry/ |
Category definitions
Section titled “Category definitions”1. App
Section titled “1. App”What it is: A user-facing or operator-facing deployable application. Owns a domain, an entry point, and a complete runtime stack. Assembles features, shared UI, and platform packages into a coherent product experience.
Location: apps/{name}/ (no worker- prefix)
Current apps: dashboard, marketing, docs
Owns:
- Route definitions and layouts (file-based routing)
- App shell: navigation, auth gate, error boundaries, global providers
- Server functions (
createServerFn) — dashboard only - Auth configuration and session management — dashboard only
- Email sending — dashboard only
- In-app feature modules (
src/features/{domain}/) not yet extracted to packages - App-specific configuration (
vite.config.ts,wrangler.jsonc,astro.config.mjs)
Must not own:
- Business logic that needs to be shared with a second app or worker
- Cloudflare binding access from client-side code paths (only in
src/server/)
Deployable: Yes — each app deploys as a distinct Cloudflare Worker or Pages project
Allowed dependencies:
- feature packages (
@legaciti/feature-*) - platform packages (
@legaciti/platform-*) - contract packages (
@platform/schemas,@platform/types,@platform/api-contract) - domain packages (
@platform/db,@platform/utils) — server-side only - shared UI (in-app via
@/*)
2. Worker
Section titled “2. Worker”What it is: A backend-only Cloudflare Worker with no user-facing HTML surface. Workers process events (queues), serve raw APIs (Hono), or implement Cloudflare primitives (Durable Objects, log tail). Each is a deployable unit with its own wrangler config.
Location: workers/{name}/
Current workers: ingestion-orchestrator, ingestion-process,
notification, consumer-api, rate-limiter, log-processor
Workspace package names: @workers/ingestion-orchestrator,
@workers/ingestion-process, @workers/notification, @workers/consumer-api,
@workers/rate-limiter, @workers/log-processor
Internal structure:
src/ index.ts Entrypoint — Queue, Hono, or DurableObject export features/ Pure domain logic — no CF imports, no `env` {domain}/ infra/ CF adapters — DB, HTTP, rate-limiter, telemetryOwns:
- Worker entrypoint and routing/queue binding logic
- Domain-specific pipeline logic (
src/features/) — pure TypeScript, testable without CF bindings - Infrastructure adapters (
src/infra/) — D1 queries, external HTTP clients, CF API wrappers
Must not own:
- React, Vite, or any browser/UI code
- Dashboard-specific server functions or auth sessions
- Logic that logically belongs in a shared feature or domain package
Deployable: Yes — wrangler deploy, excluded from Turbo pipeline
Allowed dependencies:
@platform/db,@platform/schemas,@platform/types,@platform/utils@legaciti/platform-cloudflare@platform/api-contract—@workers/consumer-apionly@platform/integration-core—@apps/dashboardonly- External npm packages (Hono, citation-js, etc.)
3. Feature
Section titled “3. Feature”What it is: A product domain module encapsulating UI, state management, query hooks, and the server-function transport layer for one domain (e.g., publications, people, projects). Features are consumed by apps; they do not run independently.
Two forms — same category:
| Form | Location | When to use |
|---|---|---|
| In-app | apps/dashboard/src/features/{domain}/ | Always the canonical home for dashboard pages and screen composition |
| Extracted package | packages/feature-{name}/ | Reusable data/logic layer (queries, mutations, API wrappers, types, reusable components) |
The distinction is critical: In-app features own the screen. Extracted packages own the logic. An extracted package is not the canonical home for dashboard page components or route-option bundles — those always belong in the app.
Canonical internal structure (both forms):
{domain}/ api.ts Transport: dynamic import() + call() wrappers for server fns queries.ts TanStack Query options and hooks query-keys.ts Query key factory mutations.ts Mutation hooks types.ts Domain-local TS types components/ Domain-specific UI components (reusable across screens) pages/ Page-level components — IN-APP ONLY; must not be in packages hooks/ Domain-specific React hooks index.ts Public API — explicit exports onlyOwns:
- All UI and state management for one product domain — reusable components only
- Domain-local types (if not shared across multiple features)
- The API transport adapter calling server functions
- Query helpers, mutation hooks, search param schemas
Must not own:
- Dashboard page components — these belong in
apps/dashboard/src/features/{domain}/pages/ - Route-option bundles (
*RouteOptions,*-route.tsx,*-routes.tsx) — these belong in the app - Dashboard-screen-specific orchestration — this belongs in the app
- Cloudflare binding access (D1, KV, SES) — belongs in server functions
- Static imports of server functions — use
api.tsdynamicimport()+call()pattern - Shared UI primitives — use
@host/components/ui/*or@/* - Other features’ internal modules — import via their
index.tsonly
Deployable: No — library
Allowed dependencies:
@legaciti/platform-*(query, events, telemetry, i18n)@platform/schemas,@platform/types,@platform/utils@platform/api-contract@host/components/ui/*or@/*— shared UI- Other
@legaciti/feature-*viaindex.ts(sparingly) - React, TanStack Query, TanStack Router
4. Domain
Section titled “4. Domain”What it is: A package containing core business or data-access logic that has no UI and no Cloudflare-runtime-specific concerns (callers supply the binding). Domain packages are the lowest stable layer; both apps and workers depend on them.
Location: packages/{name}/ legacy (db, utils) or packages/domain-{name}/ new
Current domain packages: db, utils
Owns:
- Data model definitions (Drizzle schema, table types, migrations config)
- Reusable query builders and shard routing helpers
- Pure business utilities (DOI normalization, JSON Patch application, sanitization)
Must not own:
- React or any UI framework import
- Cloudflare binding access (callers pass
env.DB_0in;dbdoes not hold the binding) - HTTP fetching or external API calls
- Feature or application logic
Deployable: No — library
Allowed dependencies:
@platform/types,@platform/schemas- External npm packages only (Drizzle, fast-json-patch, Zod, etc.)
- No
@legaciti/*packages
5. Platform
Section titled “5. Platform”What it is: Technical/runtime infrastructure shared across multiple app or worker surfaces. Platform packages abstract over framework patterns, Cloudflare APIs, telemetry, events, and i18n bootstrap. They contain no product or domain logic.
Sub-types:
| Sub-type | Runtime | Current packages |
|---|---|---|
| Client platform | Browser / React | platform-query, platform-telemetry, platform-events, platform-i18n |
| Server/edge platform | Cloudflare Worker runtime | platform-cloudflare |
Location: packages/platform-{name}/
Owns:
- Technical abstractions: query key factories, retry config, event bus, metrics, locale bootstrap
- Cloudflare runtime helpers shared across ≥2 workers (server platform only)
- No product feature logic or domain rules
Must not own:
- Feature business logic or domain-specific knowledge
- App-specific configuration
- Imports from
packages/feature-*
Deployable: No — library
Allowed dependencies (client platform):
@platform/types,@platform/schemas- React, TanStack Query, i18next, etc.
- No
@legaciti/feature-*, no CF runtime APIs
Allowed dependencies (server platform):
@platform/types,@platform/schemas,@platform/utils- Cloudflare Workers runtime APIs
- No React, no feature packages
6. Contract
Section titled “6. Contract”What it is: Lightweight packages that define typed boundaries between systems. Contract packages contain Zod schemas, TypeScript interfaces, and OpenAPI specifications. They are imported by almost all other categories and must stay thin.
Location: packages/{name}/ (schemas, types, api-contract, integration-core)
Current contract packages: schemas, types, api-contract, integration-core
Owns:
- TypeScript interfaces and type aliases shared across multiple surfaces
- Zod schemas for API input/output validation
- OpenAPI specification for versioned external APIs (
api-contract) - Runtime validators derived from schemas
Must not own:
- Business logic or data transformations
- UI code or React
- Cloudflare runtime APIs
- DB schema (that is
packages/db)
Deployable: No — library; should have minimal/zero runtime overhead
Allowed dependencies:
- Other contract packages (
types↔schemasis permitted) - Zod, TypeScript utility libraries
- No app, feature, domain, or platform imports
7. Shared UI
Section titled “7. Shared UI”What it is: Reusable presentational components, form primitives, layout
shells, and charts that are not tied to any one product feature. These live
inside apps/dashboard/src/components/ rather than a standalone package because
they are coupled to the dashboard’s Tailwind config and shadcn theme and are not
needed by any other app today.
Location: apps/dashboard/src/components/
Sub-areas:
ui/— shadcn/ui primitives: button, card, dialog, input, select, table, badge, etc.layout/— page and shell layout wrappersform/— reusable form field compositionsfeedback/— loading, error, and empty-state patterns- Top-level components (e.g.,
BarSeriesChart.tsx) — domain-agnostic data visualization
Owns:
- Visual primitives with no domain or feature knowledge
- Tailwind-variant styled wrappers
- Generic layout, feedback, and chart patterns
Must not own:
- Imports from
src/features/{domain}/orpackages/feature-* - D1, KV, or server-function calls
- Domain-specific labels or hardcoded business data
Deployable: No — bundled with the dashboard app
Allowed dependencies:
- Tailwind CSS, Radix UI, shadcn/ui
- React
@platform/typesfor prop types if needed
Dependency matrix by category
Section titled “Dependency matrix by category” ┌─────┬────────┬─────────┬────────┬──────────┬──────────┬──────────┐ may import → │ app │ worker │ feature │ domain │ platform │ contract │ sharedUI │───────────────────┼─────┼────────┼─────────┼────────┼──────────┼──────────┼──────────┤ app │ │ ✗ │ ✓ │ svr ✓ │ ✓ │ ✓ │ ✓ │ worker │ ✗ │ ✗ │ ✗ │ ✓ │ svr ✓ │ ✓ │ ✗ │ feature │ ✗ │ ✗ │ idx ✓ │ ✗ │ cli ✓ │ ✓ │ ✓ │ domain │ ✗ │ ✗ │ ✗ │ ✗ │ ✗ │ ✓ │ ✗ │ platform (client) │ ✗ │ ✗ │ ✗ │ ✗ │ ✗ │ ✓ │ ✗ │ platform (server) │ ✗ │ ✗ │ ✗ │ ✓ │ ✗ │ ✓ │ ✗ │ contract │ ✗ │ ✗ │ ✗ │ ✗ │ ✗ │ peer ✓ │ ✗ │ shared UI │ ✗ │ ✗ │ ✗ │ ✗ │ ✗ │ ✓ │ ✗ │───────────────────┴─────┴────────┴─────────┴────────┴──────────┴──────────┴──────────┘
✓ = allowed ✗ = prohibited svr = server-side paths onlycli = client platform only idx = via public index.ts only peer = same-category peers onlyAmbiguous and mixed-responsibility areas
Section titled “Ambiguous and mixed-responsibility areas”packages/utils — domain vs utility mix
Section titled “packages/utils — domain vs utility mix”Observed mix: DOI normalization (domain/publication logic), D1 shard routing (infrastructure concern), JSON Patch helpers (generic utility). Three different concerns under one name.
Preferred category: domain (primary), with generic utility elements
Recommendation: No immediate action. If utils grows past ~600 lines, move
D1 routing helpers into packages/db and leave pure utilities in utils.
packages/integration-core — contract vs platform
Section titled “packages/integration-core — contract vs platform”Observed mix: Sits under @platform/integration-core but contains runtime
request/response helpers alongside type definitions, making it part contract and
part platform (server).
Preferred category: contract for type/schema surface; if the runtime-helper
portion grows, extract it into packages/platform-integrations.
Current treatment: Mapped as contract with runtime helpers co-located.
apps/dashboard/src/features/workspace-settings/ — ownership resolved
Section titled “apps/dashboard/src/features/workspace-settings/ — ownership resolved”Current state: apps/dashboard/src/features/workspace-settings/ is the
single canonical owner for workspace-settings UI and data/query code.
Category: feature (in-app canonical)
src/monitoring/ — ✅ deleted (2026-04-23)
Section titled “src/monitoring/ — ✅ deleted (2026-04-23)”The stale src/monitoring/tracing.ts was a diverged duplicate of
packages/platform-ingestion/src/infra/telemetry/tracing.ts. The root file and the
dead companion test tests/monitoring/tracing.test.ts were deleted. The
canonical implementation lives in the worker package with co-located tests.
apps/dashboard server functions — acting as domain/data-access layer
Section titled “apps/dashboard server functions — acting as domain/data-access layer”Observed mix: apps/dashboard/src/server/functions/ mixes HTTP entry-point
concerns (auth checks, response shaping) with D1 query logic inline.
Preferred split: Server functions own the HTTP contract and orchestration;
reusable D1 query helpers belong in packages/db.
Recommendation: Incrementally extract repeated D1 query patterns into
packages/db query helpers. No immediate refactor required.
apps/legaciti.org/ and apps/my.legaciti.org/ — undocumented legacy directories
Section titled “apps/legaciti.org/ and apps/my.legaciti.org/ — undocumented legacy directories”Observed mix: Two directories exist under apps/ with no package.json:
apps/legaciti.org/ (has src/content and dist; predates apps/marketing/)
and apps/my.legaciti.org/ (has a nested apps/ tree; predates apps/dashboard/).
apps/docs.legaciti.org/ contains only build output.
Preferred category: Not applicable — these are deprecated remnants.
Recommendation: Confirm whether any content in apps/legaciti.org/src/ was
migrated to apps/marketing/. If so, delete all three directories. If any
content was not migrated, move it first. Tracked as a cleanup item.
Namespace inconsistency: @platform/* vs @legaciti/platform-*
Section titled “Namespace inconsistency: @platform/* vs @legaciti/platform-*”Observed mix: Domain and contract packages use @platform/* (legacy), while
platform packages use @legaciti/platform-*. New packages should prefer
@legaciti/ for consistency but no renames are needed now.
Category decision rules
Section titled “Category decision rules”App vs Worker
Section titled “App vs Worker”| Signal | Category |
|---|---|
| Serves HTML or has a React/Astro UI surface | app |
| Uses TanStack Router, SSR, or auth sessions | app |
| Is a queue consumer, log tail, or Durable Object only | worker |
| Has no user-facing HTML surface | worker |
| Exposes a JSON API consumed by browsers | app (server fn) or worker (Hono) depending on auth requirements |
In-app feature vs extracted feature package
Section titled “In-app feature vs extracted feature package”Extract to packages/feature-{name}/ when:
- A second app or worker surface needs to consume it
- The feature has >5 files and is under active development
- It benefits from isolated type-check and lint in CI
- It has a stable public API imported by other features
Keep in apps/dashboard/src/features/{domain}/ when:
- Truly dashboard-only and small (≤5 files)
- Involves app-shell-specific concerns (auth guards, version sync, admin layout)
- Is a thin wrapper with minimal reusable UI
When business logic should become domain code
Section titled “When business logic should become domain code”Move to packages/db or packages/utils when the same query or transformation
is used by ≥2 server functions or workers. Keep in the server function when it
is a one-off orchestration mixing auth, caching, and DB.
When infrastructure logic belongs in platform code
Section titled “When infrastructure logic belongs in platform code”Make it packages/platform-{name} when it is a technical abstraction (retry
logic, event bus, metrics) used by ≥2 features with no product knowledge. Keep
it in the app when it is app-specific configuration of a platform primitive.
When typed boundaries belong in contract packages
Section titled “When typed boundaries belong in contract packages”- Zod schema for cross-process validation →
packages/schemas - TypeScript interface shared across workers + apps →
packages/types - OpenAPI spec or external consumer types →
packages/api-contract - Internal-to-one-feature types → stay local, not exported from
index.ts
When a component belongs in shared UI vs a feature
Section titled “When a component belongs in shared UI vs a feature”Shared UI (apps/dashboard/src/components/):
- Generic visual primitive (button, input, table, card)
- Domain-agnostic layout, chart, or feedback component
- Used by ≥2 features, or has no feature-specific props
Feature component (src/features/{domain}/components/):
- Renders domain data (publication card, researcher row)
- Imports domain-local types, query hooks, or mutations
- Used only within one feature
Naming guidance for future packages
Section titled “Naming guidance for future packages”| Category | Pattern | Examples |
|---|---|---|
| app | apps/{name}/ | apps/dashboard, apps/marketing |
| worker | workers/{name}/ | workers/ingestion-process |
| feature (extracted) | packages/feature-{name}/ | packages/feature-publications |
| domain (new) | packages/domain-{name}/ | packages/domain-doi |
| platform (client) | packages/platform-{name}/ | packages/platform-query |
| platform (server) | packages/platform-{name}/ | packages/platform-cloudflare |
| contract (types) | packages/types (shared) or packages/{scope}-types | — |
| contract (schemas) | packages/schemas (shared) or packages/{scope}-schemas | — |
| contract (API) | packages/{name}-contract | packages/api-contract |
| shared UI (extracted) | packages/ui-{name}/ | packages/ui-primitives |
| tooling | tooling/ or packages/{name}-shims | tooling/, packages/host-tsc-shims |
Namespace alignment:
@legaciti/platform-*— all platform packages (preferred going forward)@legaciti/feature-*— all feature packages@platform/*— legacy domain and contract packages (existing; do not rename)- New packages should use
@legaciti/namespace for consistency