Skip to content

13 — Naming Conventions

Migrated from root technical docs.

Generated: 2026-04-22 Status: Active — canonical guide for naming across the monorepo.

This document standardizes naming patterns across apps, workers, packages, folders, files, and exports. The goal is to make code location, ownership, and responsibility predictable.


WhatPatternExampleNotes
App namekebab-casedashboard, marketingno app- prefix
Worker nameworker-{name}worker-ingestion-processlowercase, always worker- prefix
Worker package name@workers/{name}@workers/ingestion-processworkspace package identity
Feature packagefeature-{domain}feature-peopleextracted client domains
Domain package{domain} or domain-{name}db, utils, domain-authdomain/business/runtime logic
Platform packageplatform-{capability}platform-querytechnical/infrastructure
Contract package{purpose} or *-contractschemas, typescontracts, APIs, boundaries
Feature folder (app)kebab-case, pluralsrc/features/publicationsplural when domain-oriented
Subfolder (feature)kebab-case, singularapi/, queries/, pages/owning responsibility
File (query keys)query-keys.tsquery-keys.tssingular responsibility
File (query options)queries.tsqueries.tsplural (multiple queries)
File (mutations)mutations.tsmutations.tsplural (multiple mutations)
File (API wrapper)api.tsapi.tssingular, focus
File (types)types.tstypes.tssingular, focus
File (validation){entity}-validators.tsperson-validators.tsentity-specific validators
File (policy/perms){entity}-policy.tsworkspace-permissions.tsdomain-specific rules
File (adapter){service}-adapter.tscloudflare-adapter.tsruntime-bound integration
File (repository)repository.tsrepository.tsdata access abstraction
Query key object{domain}Keys (singular)publicationKeyssingular domain + “Keys”
Query factory{domain}QueryOptionspublicationsQueryOptionspascal case, plural domain
Mutation hookuse{Action}{Entity}MutationuseUpdatePersonMutationaction + entity, use prefix
Custom hookuse{Feature}{Purpose}useOrcidImportuse prefix, clear intent
Page component{Feature}PageAdminUsersPagePascalCase, no “Index” suffix
Feature component{Feature}{Component}PeopleTablePascalCase, feature-owned
Shared UI component{Generic}{Role}Button, Dialog, TabsPascalCase, generic UI names
Route/page file{route-name}.tsxadmin-users.tsxkebab-case, matches URL
Layout component{Domain}LayoutDashboardLayoutPascalCase, clear purpose
Hook file (shared)use{Purpose}.tsuseLocalizedContent.tskebab or camelCase, use prefix
Utility file{responsibility}.tssearch-params.tsspecific responsibility

Pattern: {name} (kebab-case, no prefix)

Location: apps/{name}/

Current examples:

  • dashboard
  • marketing
  • docs

Deprecated (ambiguous status):

  • legaciti.org/ — static content, no package.json → document intention or move
  • my.legaciti.org/ — pre-monorepo, no package.json → deferred: document canonical home
  • docs.legaciti.org/ — build artifact only, no package.json → deferred: confirm deployment model

Rule: Apps are user-facing or operator-facing deployables. Name should clearly identify the product or service.


Pattern: worker-{name} (kebab-case, always worker- prefix)

Location: workers/{name}/

Workspace package pattern: @workers/{name}

Current examples:

  • worker-ingestion-process
  • worker-consumer-api
  • worker-rate-limiter
  • worker-log-processor

Current package examples:

  • @workers/ingestion-process
  • @workers/consumer-api
  • @workers/rate-limiter
  • @workers/log-processor

Rule: Workers are Cloudflare runtime deployables. Folder/runtime names keep the worker- prefix. Workspace package identities use the @workers/ namespace.


Pattern: feature-{domain} (kebab-case, always feature- prefix)

Location: packages/feature-{domain}/

Current examples:

  • feature-activity
  • feature-people
  • feature-publications

Rule: Feature packages are reusable client-side modules extracted from the dashboard. The feature- prefix clearly marks them as domain code, not infrastructure. Domain name should match route/page/component names where possible.


Pattern: {domain} or domain-{name} (kebab-case)

Location: packages/{domain}/ or packages/domain-{name}/

Current examples (inconsistent):

  • db — no prefix, but core infrastructure ⚠ deferred rename
  • utils — no prefix, too broad ⚠ deferred rename (high-impact)
  • domain-auth — explicit prefix, client-side policy logic ✓
  • domain-people — explicit prefix, client-side policy logic ✓

Rules:

  • Domain packages contain business/domain logic not tied to runtime or platform concerns.
  • Use domain- prefix only when the package contains policy, permission, or entity-specific rules that need to be separated from runtime concerns.
  • Packages like db and utils are legacy naming; future domain packages should use clearer names:
    • db → stay as-is for now (high-impact rename deferred)
    • utils → stay as-is for now (high-impact rename deferred); consider subpackages for distinct domains

Pattern: platform-{capability} (kebab-case, always platform- prefix)

Location: packages/platform-{name}/

Current examples:

  • platform-query ✓ — TanStack Query infrastructure
  • platform-telemetry ✓ — metrics/timing infrastructure
  • platform-events ✓ — event bus infrastructure
  • platform-i18n ✓ — internationalization infrastructure
  • platform-cloudflare ✓ — Cloudflare runtime adapters

Rule: Platform packages provide technical capabilities and infrastructure abstractions. The platform- prefix marks them as non-domain code. They may be reusable across multiple features or domains.


Pattern: {purpose} or *-contract (kebab-case, no mandatory prefix but purpose-clear)

Location: packages/{name}/

Current examples (inconsistent):

  • schemas ✓ — Zod validation schemas (purpose clear)
  • types ✓ — TypeScript type definitions (purpose clear)
  • api-contract ✓ — API boundary specification (purpose clear with suffix)
  • integration-core ⚠ — mixes contract + runtime; see below

Rule: Contract packages define boundaries between subsystems (API, schema, type contracts). Naming should make their role obvious. If boundary + runtime logic are mixed, document clearly or split.

Deferred: integration-core — currently mixes contract definitions with runtime logic. Document or split in future cleanup.


Pattern: host-{purpose} (kebab-case, host- prefix)

Location: packages/host-{name}/

Current examples:

  • host-tsc-shims ✓ — TypeScript path resolution shims

Rule: Host packages are build-time or dev-time tooling. The host- prefix marks them as build infrastructure, not runtime code.


src/
app/ Shell composition, layouts, middleware
routes/ File-based routing (TanStack Router)
features/ Feature modules (domain code)
shared/ Truly shared across features
ui/ Shared UI primitives (shadcn-style)
hooks/ Shared React hooks (cross-feature)
transport/ Shared API transport wrappers
utils/ Shared utilities (low-level, pure helpers)
server/ Server functions, auth, runtime logic
lib/ App glue, shell configuration (narrow use)

Key rules:

  • app/ — app-wide shell concerns (layout, middleware, composition)
  • routes/ — TanStack Router file-based routes, thin orchestration only
  • features/ — feature-local business logic
  • shared/ — code reused across multiple features (check before adding here)
  • server/ — server functions, Cloudflare bindings, auth, request context
  • lib/ — narrow use only: locale bootstrap, shell glue, monitoring registry
  • ⚠ Avoid: generic lib/helpers/, broad lib/utils/, catch-all lib/common/

Feature Structure (Both App-Local and Packages)

Section titled “Feature Structure (Both App-Local and Packages)”
{feature}/
api.ts Transport wrappers
queries.ts Query factories + keys (or queries/ subfolder)
query-keys.ts Query key definitions (may be inlined in queries.ts)
mutations.ts Mutation hooks, invalidation logic
types.ts Feature-local TypeScript interfaces
model/ Pure business logic, validators, mappers
components/ Reusable feature UI components
pages/ Page/screen entry components
hooks/ Feature-specific React hooks (if needed)
index.ts Public exports

Rules:

  • Each subfolder has a single owning responsibility.
  • Files are not separated arbitrarily; proportionality matters (small features may inline or omit subfolders).
  • No catch-all folders within features (helpers/, utils/, common/).
  • All business logic goes to model/ or to feature-specific files with clear names.

PatternFile NameExport Name ExampleNotes
Query key factoryquery-keys.tspublicationKeyssingular domain + “Keys”
Query option factoriesqueries.tspublicationsQueryOptionsmay split into queries/list.ts, queries/detail.ts
List query optionsqueries/list.tspublicationsListQueryOptionswhen split
Detail query optionsqueries/detail.tspublicationDetailQueryOptionswhen split
Transport wrappersapi.tsgetPublications()call server functions

PatternFile NameExport Name ExampleNotes
Mutation hooksmutations.tsuseUpdatePublicationMutationaction + entity + “Mutation”
Mutation invalidation logicmutations.ts(part of hook)included in mutation file

PatternFile NameExport Name ExampleNotes
Feature typestypes.tsPublicationDTO + form types
Entity validators{entity}-validators.tspublicationValidatorsresponsibility-specific
Form validators (feature)form-validators.tsformValidatorswhen not entity-specific
Search param helperssearch-params.tsparseSearchParamsresponsibility-specific
Mappers/transformersmappers.tstoPublicationDTOresponsibility-specific
Display/view-model helpersmodel/{file}.ts(various)kept in model/ subfolder

PatternFile NameExport Name ExampleNotes
Workspace permissionsworkspace-permissions.tscanEditPublication()domain-specific rules
Entity-specific permissions{entity}-policy.tspublicationVisibilityPolicyentity-focused, domain-owned
Membership/affiliation rules{entity}-membership.tsgetActiveAffiliation()entity rules
Business-specific validators{domain}-validators.tspersonSlugValidatordomain-specific validation

PatternFile NameExport Name ExampleNotes
Cloudflare adaptercloudflare-adapter.tsinitCloudflareClient()runtime-bound integration
Database/repositoryrepository.tsPublicationRepositorydata access abstraction
Cache layercache.tscachePublication()caching abstraction
Event busevent-bus.tsemitPublicationUpdated()event infrastructure
Request contextrequest-context.tsgetRequestUser()runtime request state
Permission/trust checkingpermissions.tsisUserAdmin()access control logic
Trusted originstrusted-origins.tsTRUSTED_ORIGINSsecurity configuration

PatternFile NameExport Name ExampleNotes
Page component{Feature}Page.tsxexport function AdminUsersPage()matches route
Layout component{Domain}Layout.tsxexport function DashboardLayout()app-wide or feature-wide
Route integration fileN/A in new patterns(deprecated)avoid; keep routes thin

PatternFile NameExport Name ExampleNotes
Custom React hookuse{Purpose}.tsexport function useOrcidImport()use prefix mandatory
Shared hookuse{Purpose}.tsexport function useLocalizedContent()shared across features
Low-level utility{responsibility}.tsformatDate()pure functions, no side effects
String formattingformatters.ts or specificformatPersonName()responsibility-specific
Parsing/normalizationparsers.ts or specificparseDoi()responsibility-specific
Helpers (avoid)(not recommended)use specific responsibility names instead

Rule: Query key objects use singular domain name + “Keys”. Query option factories use plural domain + “QueryOptions”.

// Query keys — singular domain
export const publicationKeys = {
all: ["publications"] as const,
list: (search) => ["publications", search] as const,
detail: (doi) => ["publication-detail", doi] as const,
} as const;
// Query options — plural domain
export const publicationsQueryOptions = (search) =>
queryOptions({
queryKey: publicationKeys.list(search),
queryFn: () => getPublications(search),
});
export const publicationDetailQueryOptions = (doi) =>
queryOptions({
queryKey: publicationKeys.detail(doi),
queryFn: () => getPublication(doi),
});

Rule: Mutation hooks use use{Action}{Entity}Mutation (PascalCase action and entity).

export function useUpdatePersonMutation() {
/* ... */
}
export function useDeletePublicationMutation() {
/* ... */
}
export function useUploadPersonPhotoMutation() {
/* ... */
}

Rule: Custom hooks use use{Purpose} (clear intent, use prefix).

export function useOrcidImport() {
/* ... */
}
export function useIngestionActivityToasts() {
/* ... */
}
export function usePersonModalFieldLocks() {
/* ... */
}

Rule: Page components use {Feature}Page (PascalCase, “Page” suffix). Feature components use {Feature}{Component} (PascalCase, feature name + role).

// Pages
export function AdminUsersPage() {
/* ... */
}
export function LoginPage() {
/* ... */
}
export function WorkspaceSettingsPage() {
/* ... */
}
// Feature components
export function PeopleTable({ people }: PeopleTableProps) {
/* ... */
}
export function PublicationsFilters({ onChange }: PublicationsFiltersProps) {
/* ... */
}
// Shared UI (generic names, no feature prefix)
export function Button({ children, onClick }: ButtonProps) {
/* ... */
}
export function Dialog({ isOpen, onClose }: DialogProps) {
/* ... */
}

Rule: Shared UI primitives use generic UI role names (no feature prefix). These are shadcn-style primitives.

// src/components/ui/ — generic, feature-agnostic names
export function Button() {
/* ... */
}
export function Dialog() {
/* ... */
}
export function Tabs() {
/* ... */
}
export function Card() {
/* ... */
}
export function Input() {
/* ... */
}
export function Select() {
/* ... */
}

Rule: Feature folder names are plural when representing a domain area (collection of entities or concerns).

features/
publications/ — plural (domain area, multiple publications)
people/ — plural (domain area, multiple people)
projects/ — plural (domain area, multiple projects)
workspace-settings/ — compound, domain-oriented
admin/ — noun, domain-oriented (admin area, not "admins")

Rule: TypeScript types and interfaces use singular names (represent one entity).

export interface Publication {} // singular entity
export interface Person {} // singular entity
export interface Project {} // singular entity

Rule: Query key objects use singular domain names. Query option factories use plural when they fetch collections, singular for single resources.

// Query keys — singular domain
export const publicationKeys = {
list: (search) => ["publications", search], // plural inside, singular object name
detail: (doi) => ["publication-detail", doi], // singular
};
// Query option factories
export const publicationsQueryOptions = (search) => /* list */
export const publicationDetailQueryOptions = (doi) => /* detail */
// Function names inside api.ts can be plural when returning collections
export async function getPublications(search) { }
export async function getPerson(orcid) { }

Rule: Mutation hooks use singular entity names (action on one entity at a time).

export function useUpdatePersonMutation() {} // singular: update one person
export function useDeletePublicationMutation() {} // singular: delete one publication

Discouraged Names (and Preferred Alternatives)

Section titled “Discouraged Names (and Preferred Alternatives)”
Anti-PatternProblemPreferred Alternative
lib/helpers.tsToo vague, mixed purposeName by responsibility: search-params.ts, formatters.ts
lib/utils.tsToo broad, hides responsibilityName by responsibility: date-utils.ts, string-utils.ts or split
shared/common.tsUnclear what is “common”Name by feature or responsibility
helpers/Folder, vague contentsName by responsibility: formatters/, validators/
common/Folder, vague contentsUse shared/ only for cross-feature reuse
misc/Folder, unclear purposeAvoid; organize by responsibility
stuff.tsNo responsibility indicatedAvoid; choose clear name
Generic shared/utils/Not all shared code should be “utils”shared/hooks/, shared/transport/, shared/ui/

Rule: Use shared/ only for code that is actually reused across multiple features. Do not create shared/ speculatively.

Good candidates for shared/:

  • UI primitives reused across features (shared/ui/)
  • Cross-feature hooks (shared/hooks/)
  • Transport wrappers shared across features (shared/transport/)

Bad candidates for shared/:

  • Feature-specific utilities (keep in feature)
  • Generic catch-alls for utilities (name by responsibility instead)
  • Single-use helpers (may not belong in shared yet)

File: search-params.ts

Exports:

export const publicationSearchSchema = z.object(/* ... */);
export function parsePublicationSearch(params: URLSearchParams) {}
export function stringifyPublicationSearch(search: PublicationSearch) {}
export const DEFAULT_PUBLICATION_SEARCH = {
/* ... */
};

File: form-validators.ts or {entity}-validators.ts

Exports:

export const personSlugValidator = (slug: string) => {
// validation logic
};
export const personNameValidator = z.string().min(1).max(255);

File: mappers.ts (feature-level) or model/mappers.ts

Exports:

export function toPublicationDTO(publication: PublicationRecord): Publication {}
export function fromFormToPublication(
formData: PublicationFormData,
): Partial<Publication> {}

File: {service}-adapter.ts or repository.ts

Exports:

// Adapter pattern (runtime integration)
export class CloudflareAdapter {}
export class SESEmailAdapter {}
// Repository pattern (data access)
export class PublicationRepository {}
export async function getPublication(doi: string) {}

ConceptPatternExample
Domain nameskebab-case, clearpublications, people
Feature packagesfeature-{domain}feature-publications
Platform packagesplatform-{capability}platform-query
Domain packagesname or domain-{name}db, domain-auth
Query key objectssingular domain + KeyspublicationKeys
Query optionsplural domain + QueryOptionspublicationsQueryOptions
Mutation hooksuse{Action}{Entity}MutationuseUpdatePersonMutation
Page components{Feature}PageAdminUsersPage
Feature components{Feature}{Role}PeopleTable
Shared UIgeneric namesButton, Dialog
Feature foldersplural, kebab-casesrc/features/publications/
Responsibility filesspecific namesquery-keys.ts, mappers.ts
Avoidgeneric namesno helpers.ts, utils.ts

  • Define conventions (this document)
  • Communicate to team
  • Rename apps/dashboard/src/shared/utils/utils.tsapps/dashboard/src/shared/utils/common.ts (or split further)
  • Update any imports

Phase 3: Feature File Normalization (Deferred)

Section titled “Phase 3: Feature File Normalization (Deferred)”
  • Standardize feature file names across extracted packages
  • Ensure all follow the canonical shape from 08-feature-internals.md
  • May involve renaming responsibility files like personFieldResyncChrome.ts

Phase 4: Package-Level Renames (Deferred High-Impact)

Section titled “Phase 4: Package-Level Renames (Deferred High-Impact)”
  • Rename packages/utils → more specific name (e.g., packages/database-utils, packages/domain-utils split)
  • Rename packages/db → clearer name (e.g., packages/database)
  • Clarify integration-core split (contract vs runtime)

Phase 5: Deprecated App Cleanup (Deferred)

Section titled “Phase 5: Deprecated App Cleanup (Deferred)”
  • Clarify or remove apps/legaciti.org/, apps/my.legaciti.org/, apps/docs.legaciti.org/
  • Document canonical home for each

When adding new code, check:

  • Package name follows category pattern (feature-, platform-, domain-, etc.)
  • Folder name communicates responsibility or domain
  • File name is specific, not generic (search-params.ts not utils.ts)
  • Exports follow naming convention (query keys singular, options plural, hooks with use, pages with Page)
  • No catch-all folders (helpers/, common/, misc/)
  • Shared code is actually shared across multiple features
  • Domain logic is clearly separated from UI logic
  • Runtime logic lives in src/server/ not in generic folders

Deferred High-Impact Renames (Document for Future)

Section titled “Deferred High-Impact Renames (Document for Future)”
  1. packages/utils — Too broad; should split into responsibility-specific packages or rename to clearer name

    • Current: @platform/utils — DOI normalization, sharding, JSON Patch
    • Future: consider @platform/database-utils, @platform/domain-utils, or namespace under feature
    • Status: DEFERRED — high-impact rename; deferred to next cleanup pass
  2. packages/db — Too generic; should rename to @platform/database or similar

    • Current: @platform/db — Drizzle ORM, D1 schema, client
    • Future: @platform/database or keep as-is with better docs
    • Status: DEFERRED — core dependency; high-impact rename; deferred to next cleanup pass
  3. packages/integration-core — Mixes contract definitions with runtime logic

    • Current: Contains both API boundary specs and runtime code
    • Future: Split into integration-contract (boundaries only) and integration-runtime (implementation)
    • Status: DEFERRED — requires architectural split; not blocking
  4. Deprecated apps (legaciti.org/, my.legaciti.org/, docs.legaciti.org/)

    • Current: No package.json, unclear status in monorepo
    • Future: Document canonical deployment model or archive
    • Status: DEFERRED — clarify intent and document in architecture
  5. Feature file normalization (e.g., personFieldResyncChrome.ts)

    • Current: Some feature files use unclear naming patterns
    • Future: Align with 08-feature-internals.md standard shape
    • Status: DEFERRED — incremental, per-feature cleanup