TIP
This page mirrors .agents/core.md from the repository. Edit it at the source, not here.
Core Package Patterns
Use this when adding or refactoring shared domain logic in packages/core.
This file is the source of truth for what belongs in packages/core, how to structure it, and how shared contract changes should propagate through the repo.
Goals
- Keep shared domain contracts in one place.
- Make API and frontend behavior follow shared changes by import, not by parallel manual edits.
- Keep
packages/coreruntime-agnostic and safe to consume frompackages/auth,packages/db,packages/api,apps/web, andapps/server.
What Belongs In packages/core
Put code in packages/core when at least one of these is true:
- The same domain contract is consumed by more than one package.
- A change should propagate automatically from shared logic into both API and frontend behavior.
- The code defines a shared constant, enum, filter, status, category, transport shape, normalizer, formatter, or option builder.
- The code is pure domain logic with no React, DB, env, logger, or network dependency.
Typical examples:
- Zod schemas and inferred types for shared transport-safe shapes.
- Shared enums such as categories, statuses, and sort values.
- Pure formatters and normalizers.
- Schema-derived option arrays and defaults used by filters, tabs, selects, or validators.
- Shared constants such as MIME types, upload limits, reserved values, or other package-agnostic constraints.
What Does Not Belong In packages/core
- React components, hooks, route files, or TanStack Query wrappers.
- oRPC procedures, router handlers, or request middleware.
- Drizzle schema, SQL queries, migrations, or database access.
- Env loading, logger setup, storage clients, or other side-effectful integrations.
- One-off app-only view logic unless the task explicitly calls for divergence from the shared contract.
- One-off utilities or library functions that are not derived from or directly related to the shared contract, they should be placed within their respective package or slice.
Shared Security Limits
Keep cross-transport limits and pure validation policy under packages/core/src/security/; keep request readers and provider clients in their runtime owners.
- Default JSON/RPC bodies are bounded before parsing by
apps/server. - Stripe raw webhook bodies are capped at 1 MiB before signature verification.
- Browser log ingestion is capped at 64 KiB plus batch/depth/string limits in
packages/logger. - Media contracts cap general uploads at 8 MB and avatars at 2 MB; the local upload route streams only to the row-specific maximum.
- Outbound webhook policy allows HTTP/HTTPS on ports 80/443, rejects private/reserved addresses, pins validated DNS answers, revalidates redirects, and enforces connect/header/body/total limits.
Do not duplicate these numbers in routers or UI. Import the shared constants, and add behavior tests at both the pure policy and runtime boundary when changing them.
Default Folder Shape
Shared domain modules in packages/core should usually look like this:
src/<domain>/
constants.ts
types.ts
utils.ts
index.tsconstants.ts
Use this for support constants that back the domain but are not themselves the primary cross-package contract.
Examples:
- MIME type lists
- byte limits
- reserved values
- expiry durations
If the value is a shared contract that API and frontend validate against, it usually belongs in types.ts as a Zod schema instead.
types.ts
Use this for shared Zod schemas and inferred types.
- Put cross-layer enums here, even when they are simple literal sets.
- Export both the schema and the inferred type as named exports.
- Export schema-derived defaults here when they are part of the shared contract surface.
- Prefer one shared schema over duplicated literal unions in consumers.
utils.ts
Use this for pure helpers derived from the shared contract.
- formatters
- normalizers
- schema-derived option arrays
- shared defaulting helpers
Prefer deriving labels and UI options from shared schema values instead of introducing parallel frontend arrays or label unions.
index.ts
Keep a small domain barrel that re-exports the public API for that domain.
Consumers should import the domain surface, not deep internal files.
Propagation Rule
Unless explicitly told not to, shared domain enums and validation logic should propagate from packages/core into both the frontend and API.
That means:
apps/webroute validators should import shared schemas and defaults from core instead of repeatingz.enum([...])literals.- Frontend filters, tabs, and select options should use core-derived helpers or schema values, not duplicated local arrays.
packages/apiprocedures should validate shared inputs and outputs with core schemas when the contract crosses package boundaries.- Slice query modules in
apps/webmay re-export shared types for convenience, but they should not redefine local literal unions if core already owns the contract.
If adding one enum value requires hand-editing multiple literal unions in apps/web or packages/api, the contract probably belongs in packages/core.
Design Rules
- Keep exports domain-specific and explicit.
- Keep helpers deterministic and side-effect free.
- Favor transport-safe primitives at the package boundary.
- Prefer a formatter or option builder over separate label schemas.
- Prefer names like
formatX,normalizeX,getXBaseover vague generic helper function names. - Prefer SCREAMING_SNAKE_CASE for shared constant values and PascalCase for shared enums.
- Zod schemas should be named like
const ObjectSchemaand inferred types should be named liketype ObjectType = z.infer<typeof ObjectSchema>. - For input and output shapes that are shared between API and frontend, prefer naming them like
ObjectCreateInputSchemaandObjectUpdateNameInputSchemato clarify their transport role. - Keep each domain isolated under its own subpath.
When To Extract Into Core
Move logic into packages/core when:
- the same domain shape is needed by both
packages/apiandapps/web packages/authandpackages/apishare the same normalization or validation rule- a shared enum, default, or format should drive frontend behavior automatically
Keep logic local when it is truly slice-specific, UI-only, router-only, or persistence-only.
Introducing A New Shared Enum Or Contract
When you add a new shared category, status, or contract:
- Add or update the schema in
packages/core/src/<domain>/types.ts. - Update any derived helpers in
packages/core/src/<domain>/utils.ts. - Make
apps/webconsume the shared schema, defaults, and options instead of duplicating literals. - Make
packages/apiconsume the shared contract instead of redefining parallel schemas. - Update persisted enums in
packages/dband generate a migration if the value is stored in the database.
This is the default repo behavior unless the task explicitly asks for package-local divergence.