Canonical agent instructions for this repository.
CLAUDE.md is intentionally a symlink to this file so different agents load
the same project guidance.
Additional scoped rules live in .claude/rules/.
- Read this file end-to-end before editing.
- Open the
README.mdof the package you are about to change. - If anything in this file conflicts with the actual code in
packages/*/src/, the code wins — and fix this file in the same PR.
These are lessons from repeated corrections. Violating them repeats mistakes the user has already had to fix more than once.
-
Layer discipline — core vs server.
rockets-core= shared infrastructure (auth abstraction, guard, CQRS, declarative resources, repository config, Swagger UI registration).rockets(server) = presentation + composition for external auth integration (MeController,APP_GUARDopt-in). Before placing a component, ask: "Wouldrockets-server-authalso need this?" Yes → core. No → server. Controllers belong in server or auth, never in core. Swagger IS in core (both server and auth need API docs from a single registration). Access control is core too: the opt-inaccessControloption onRocketsCoreModule/RocketsModuleregisters upstream@concepta/nestjs-access-control; when omitted, no ACL wiring exists. -
Dynamic repository, not
@InjectRepository. In new code, use@InjectDynamicRepository(KEY)+RepositoryInterface<Entity>. Features built on top of core import these from@concepta/rockets-core(it re-exports the repository abstraction —InjectDynamicRepository,RepositoryInterface,RepositoryModuleInterface,Where,getDynamicRepositoryToken— so feature/server code never depends on the upstream repository package directly). The symbols originate in@concepta/nestjs-repository; only core and adapter packages import them from there. Register entities through bundles insideresources[](defineResource()auto-contributes its entity row;defineModuleResource({ entities: [...] })contributes additional rows) plususerMetadata.entityfor the metadata row — never via a module-localTypeOrmModule.forFeature(). The default adapter is the single top-levelrepository: RepositoryModuleInterfacefield.rockets-server-authexposesdefineRocketsAuth(), which contributes auth entity rows to the sameresources[]/ planner pipeline as core. Do not register the same auth keys twice. -
Resource config is flat.
RocketsResourceConfigextendsCrudModuleForFeatureOptionsInterfacedirectly. Nocrud.crudnesting. Handlers declared inoperations[].queryHandler/commandHandlerare auto-extracted by core — do NOT duplicate them inresource.providers. -
One
repositoryadapter at the root, every bundle owns its own entity.RocketsCoreModule/RocketsModuleoptions carry a single top-levelrepository: RepositoryModuleInterface(default adapter) plus auserMetadataconfig (entity+ DTOs, optional per-entityrepositoryoverride). All other persistence rows are contributed by bundles insideresources[]:defineResource()— CRUD-shaped, auto-contributes its entity row.defineModuleResource({ entities, module })— non-CRUD persistence and/or Nest module slice (controllers/providers/exports/imports). Per-entityrepositoryoverrides the root adapter for that one table; bundles withentities: []are valid and useful for CQRS-only workflows. There is norepositories.entities[]block any more — registering the same entity in two places (or splitting key + class across files) is what this rule prevents.
-
Never lose definition imports in a
definitionTransform. Always preserve the imports already present on the generated definition before appending package-owned imports. Otherwise async factory dependencies can become invisible to Nest. Check this every time a module-definition file is edited. -
Every DTO field that must show in Swagger needs
@ApiProperty()or@ApiPropertyOptional(). The@nestjs/swaggerCLI plugin is NOT enabled. Type inference alone will not populate the schema.@Expose()from class-transformer is unrelated to Swagger. -
Verify compilation after edits. Do not declare done based on IDE green state alone. Run
yarn buildand the relevant type/test command; boot the applicable sample when runtime wiring changed. Missing imports and wrong-package auto-imports are caught by the real toolchain, not by editor confidence. -
Do not trust IDE auto-imports.
@Exposefromclass-transformeris NOT@ApiPropertyfrom@nestjs/swagger. Verify the imported symbol actually does what you intend. -
No undocumented workarounds. Bridge modules, lazy placeholders, fake providers, and unchecked assertions must not conceal a design or wiring problem. A production compatibility/variance assertion is allowed only at a boundary TypeScript cannot express, when runtime identity makes it safe and an adjacent comment states that invariant. If the invariant cannot be demonstrated, stop and ask.
-
No unused fields in interfaces. If a field is not actively consumed, remove it.
-
Do not assume the user is right. When asked to analyze, do independent analysis and push back if the premise is wrong.
-
READ before editing. Open the file, understand the surrounding code, THEN modify. Do not edit blindly based on a diff alone.
-
Persistence is database-agnostic by default. The supported contract is
RepositoryInterfaceand dynamic repository keys in@concepta/nestjs-repository(re-exported by@concepta/rockets-core). Concrete backends (TypeORM, Firestore, other adapters) are selected in module options and must remain swappable.rockets-corepublic design, types, and docs must not hard-require a specific ORM — the zod layer stays ORM-free by delegating entity generation to aSchemaEntityCompileradapter. Example configs may use TypeORM as a common case; that does not make TypeORM the definition of Rockets storage. -
Module resource exports are a public surface — export the minimum.
defineModuleResource({ module: { providers, exports } })materialises a Nest dynamic module thatRocketsCoreModulere-exports globally (because core isglobal: true). That makes every entry inexportsinjectable from anywhere in the app — including theinject: [...]factory ofRocketsModule.forRootAsync. Powerful, but also dangerous: collisions are by injection token. Two module resources exporting the same token — the same class reference, or the same string/symbol token value — shadow each other in the DI container (Nest accepts both, the last one wins, and the bug surfaces in production). Two distinct classes that merely share a name (PriceFormatter,AuditService,Logger) are different tokens and don't hard-collide, but they are a real readability/foot-gun hazard — treat them the same way.Exposure rule:
- Provider/service crosses a feature boundary (injected by another
bundle, or by an outer factory's
inject:) → put inprovidersandexports. - Internal use only (helpers, formatters, hooks applied via
extraDecoratorson the bundle's own controller, services private to the bundle) →providersonly.
When you must export a name that could collide, prefix it (
BillingPriceFormatter) or use an injection token (BILLING_PRICE_FORMATTER_TOKEN). The sample-server'sauthFeatureis the canonical reference: it exports onlySampleAuthAdapter(the symbol the outeruseFactoryinjects);AuthControllerand the entity stay internal. - Provider/service crosses a feature boundary (injected by another
bundle, or by an outer factory's
-
Zod-first resources — review checklist.
- Schema is source of truth; use
f.*helpers, not raw.registerunless necessary. - Relations:
f.fk()/f.hasMany(childSchema)— rejectz.array(z.unknown()). - Types:
WireRow<S>for API;SchemaPersistenceRow<S>for hooks/repos — not the entity class. - Compile entity in
*.schema.tsonly to break import cycles; default iszodResource({ schema }). - Persistence hints belong in
rocketsFieldMeta/rocketsEntityMeta; API docs belong in.meta()— never putdbin.meta(). - Unsupported column types need
db.column; many-to-many is a junction sub-resource. - Capability matrix:
packages/rockets-core/README.md(Zod-first section).
- Schema is source of truth; use
When replying to the project owner or maintainer:
- Prefer short, direct answers and code when it clarifies behavior; avoid filler and over-long essays.
- Treat this codebase as high quality bar: designs should remain valid if the repository adapter (or database) is swapped, not only under one ORM.
- This section encodes their preferences for assistants; it is not a technical dependency of the build.
- This root
AGENTS.mdis the default instruction set for the whole repository. .claude/rules/*.mdprovide scoped, glob-filtered rules (TypeScript, build/test/lint, editing).- If a future subdirectory adds its own
AGENTS.md, treat that as a scoped override for files in that subtree. - When instructions conflict, prefer the most specific instruction file for the file path being edited.
The engine is the upstream @concepta/nestjs-* stack consumed from npm
(nestjs-core, nestjs-repository, nestjs-crud, nestjs-authentication,
nestjs-access-control, plus the identity modules used by server-auth).
The Rockets @concepta/* packages are composition + curated re-exports on top
of it.
packages/rockets-core(@concepta/rockets-core): shared server infrastructure — auth abstraction (AuthAdapterInterface,AuthServerGuard), CQRS handlers, declarative resources (defineResource,defineModuleResource,buildAppRegistrationPlan), rootrepositoryadapter +userMetadataconfig, Swagger registration (SwaggerUiModule), opt-inaccessControl(registers@concepta/nestjs-access-controlwhen configured, nothing otherwise), and the shared decorators/utils formerly published in@bitwild/rockets-common(AuthUser,InjectDynamicRepository,InjectCrudAdapter, model interfaces,SchemaEntityCompilercontract, error-logging/entity-key utils — nowsrc/common/). Also owns the zod-first resource layer at the@concepta/rockets-core/zodsubpath (zodResource/zodSubResource/bindZodResources,f.*field helpers,rocketsFieldMeta/rocketsEntityMetaregistries,defineZodUserMetadata). Zod is the first-class schema layer of Rockets;zod+nestjs-zodare optional peers and the main entry stays zod-free, so non-zod consumers pay nothing. The zod layer is still ORM-free: entity generation is delegated to aSchemaEntityCompileradapter. Imported by both server and auth.packages/rockets-repository-typeorm(@concepta/rockets-repository-typeorm): TypeORM implementation of the dynamic repository contract — a thin wrapper whose main entry re-exports upstream@concepta/nestjs-repository-typeormverbatim, so consumers depend on a single Rockets package. The only code it owns is the zod layer's TypeORMSchemaEntityCompilerat the@concepta/rockets-repository-typeorm/zodsubpath (typeOrmZodEntityCompiler). Mirror the/zodcompiler for other stores (rockets-repository-firestore, …).packages/rockets-repository-firestore(@concepta/rockets-repository-firestore): Firestore implementation of the dynamic repository contract.packages/rockets-server(@concepta/rockets): external-auth integration layer and curated core facade.MeController+ global guard opt-in. Use when users live in Firebase / Auth0 / another external system.packages/rockets-server-auth(@concepta/rockets-auth): complete built-in auth system (JWT, signup, login, recovery, OTP, OAuth, admin). Compose it with core or server; it does not mirror the server facade.packages/rockets-adapter-firebase(@concepta/rockets-adapter-firebase): Firebase auth adapter implementingAuthAdapterInterface.examples/sample-server: canonical reference app usingrockets-serverwith an external auth adapter. Wires the zod layer in one line (src/zod-bindings.ts:bindZodResources(typeOrmZodEntityCompiler)).examples/sample-server-auth: reference app usingrockets-server-auth(built-in auth).examples/sample-code-review: full-stack reference (API + web) used for code review walkthroughs.scripts/: checked release, package-contract, repository-integrity, and integration helpers. Prefer the existing Vitest project/pool configuration over custom test-runner plumbing.api/public-api-reports.json: reviewed declaration-level contract for every published TypeScript entry point, alongsideapi/public-api-policy.md. Runyarn api:reportafter public export or signature changes; update it only after reviewing the compatibility impact and documentation or migration note..context/: shared scratchpad for multi-agent collaboration (gitignored).
Modular rules in .claude/rules/:
| Rule file | Scope | Purpose |
|---|---|---|
typescript-strict.md |
**/*.ts |
Strict types and documented boundary exceptions |
build-test-lint.md |
always | Build/test/lint command order |
editing-guidelines.md |
always | Minimal diffs, source of truth |
- E2E / integration tests are the default. New tests should be
*.e2e-spec.ts(real Nest app + supertest + SQLite) unless a focused unit test is the more direct behavior boundary. - The enforced unit coverage gate (
yarn test:ci/yarn test:cov) is statements 50%, branches 50%, functions 40%, and lines 50%.yarn test:e2e:covproduces the package-E2E coverage report without a threshold gate. - Importing a handler barrel evaluates its decorated CQRS classes and attaches metadata to those class objects. Avoid unrelated domain-barrel imports in an E2E file that boots a Nest app; Vitest's fork pool isolates spec files.
- See
.claude/rules/build-test-lint.mdfor full details.
- Prefer precise types and
unknownplus narrowing at untrusted boundaries. - Do not introduce
anyexcept to mirror a documented upstream contract; keep the exception local, commented, and lint-scoped. - Do not use assertions to silence a real mismatch. A narrow production compatibility/variance assertion must preserve runtime identity and carry an adjacent invariant comment. Tests and fixtures may assert controlled mock shapes, but must not use casts to bypass the behavior under test.