Configuration-driven NestJS stack. One options object becomes a working API — auth, dynamic repositories, generated CRUD controllers, hooks, swagger.
Status: pre-1.0 (1.0.0-alpha.7). The public surface (AuthAdapterInterface, defineResource, defineModuleResource, RepositoryInterface, the RocketsModule.forRoot options shape) is stable; field renames are still possible before 1.0. Pin exact versions in production.
- 1. Introduction
- What problem each layer solves
- The two paths
- 2. Get Started
- Prerequisites
- 3. How-to Guides
- Run multiple auth credentials (chain)
- Mark a route as public
- Add a non-CRUD feature
- Add a nested CRUD resource
- Wire TypeORM without hand-registering entities
- Mix two persistence adapters
- Scope rows to the authenticated user
- Read the authenticated user inside a CRUD handler
- Add role-based access control
- Disable the global guard or the
/mecontroller - Override a default user-metadata handler
- Troubleshooting
- 4. Reference
- 5. Roadmap
- 6. Contributing
- 7. Security
- 8. License
Rockets removes the part of a NestJS backend that you write the same way every time: an auth guard, an entity-to-controller pipeline, validation wiring, swagger annotations, owner scoping, repository plumbing. You describe each feature once as a config object, and the framework registers the modules, providers, controllers, and routes for you.
There is no codegen step. Everything happens at runtime through Nest dynamic modules. Adding a feature means appending an object to a resources[] array.
Engine vs composition: the motor is the upstream @concepta/nestjs-* stack (repository, CRUD, hooks, access control, and — on path B — user/role/otp modules). @bitwild/rockets-* packages are mostly curated re-exports plus wiring: @bitwild/rockets-core runs buildAppRegistrationPlan and turns your resources[] / repository / auth options into Nest imports that call those upstream modules. Rockets does not replace that stack; it centralises configuration. See Engine (upstream) in Reference.
Be explicit about who owns which problem — Rockets is not one monolith.
| Layer | Package(s) | Problem it solves |
|---|---|---|
| Motor | @concepta/nestjs-* (via @bitwild/rockets-repository, rockets-crud, …) |
Reimplementing repository access, CRUD shape, hooks, and ACL primitives on every NestJS project. |
| Composition | @bitwild/rockets-core |
Manually stitching Nest modules, entity registration, guard + adapter chain, and swagger for every new service — even when you already use Concepta motors. |
| Path A — external identity | @bitwild/rockets |
Micro app runtime — shared guard, /me, auth chain, declarative resources[]. Users live outside the app (Firebase, Auth0, central JWT). Primary choice for Stargate-provisioned workflow APIs. See packages/rockets-server/README.md. |
| Path B — built-in identity | @bitwild/rockets-auth |
The app is the user system (signup, login, OTP, roles, invitations) and you do not want to wire seven Concepta identity modules yourself. |
Honest scope: Rockets removes repeated infrastructure work on new backends (auth wiring, CRUD registration, persistence plumbing). Most calendar time on a real product is still domain logic, integrations, and operations — not something any framework eliminates.
There are two ways to run a Rockets app, and the choice depends on where your users live.
Path A — External auth (@bitwild/rockets). You bring an AuthAdapterInterface implementation. The framework gives you /me, a global guard, generated CRUD, hooks, swagger. Pick this when users live in Firebase, Auth0, a custom JWT issuer, or any other identity store.
Path B — Built-in auth (@bitwild/rockets-auth). The framework owns the user table. You get signup, login, password recovery, OTP, invitations, admin user CRUD, role-based access control — all wired through one defineRocketsAuth() call. Pick this when the app is the identity source.
The two paths share the same lower layers (resource planner, dynamic repository, hooks, swagger), so a feature added to one runs identically on the other.
Enterprise shape: Stargate (workflow platform, n8n-like) connects systems and provisions micro apps; each micro app is a small Nest API on @bitwild/rockets with one shared identity across the product.
| Piece | Role |
|---|---|
| Stargate | Design cross-system workflows, call micro apps over HTTP, register URLs — orchestration, not domain CRUD |
| Identity (once) | Firebase / Okta / one @bitwild/rockets-auth deployment — login, tokens, shared user id |
| Micro app | @bitwild/rockets — global guard, /me, userMetadata, resources[] for one domain (billing, CRM, code review…) |
| Stargate workflow | Automation in Stargate (webhook → transform → call API → notify) |
| Micro app workflow | Business rules inside the API (hooks, services, CQRS) |
Users / integrators
│
▼
┌──────────────┐ HTTP / provision ┌──────────────────────────┐
│ Stargate │ ───────────────────────▶│ Micro apps (Rockets) │
│ (workflows) │ │ Billing · CRM · Review │
└──────────────┘ └────────────┬─────────────┘
│ │
▼ ▼
External systems ┌──────────────────────────┐
(email, CRM, webhooks) │ Identity (once) │
│ same token · same user │
└──────────────────────────┘
Do
- One issuer (IdP or central
rockets-auth); every micro app uses anAuthBootstrappointing at the same project/secret soAuthorizedUser.idmatches everywhere. - Same
userMetadatacontract in each micro app (profile row keyed by auth id, exposed on/me). - Each squad owns only
repository+resources[]for its domain (optional Firestore override per entity).
Do not
- Scaffold
defineRocketsAuth()with a separate user DB in every Stargate-generated micro app — breaks SSO. - Treat Stargate as the token issuer unless it actually is; micro apps must trust the real identity layer.
- Put domain persistence and CRUD inside Stargate — Stargate orchestrates; micro apps execute.
| Deployment | Identity (once) | Micro apps (many) |
|---|---|---|
| Path A — external IdP | Firebase / Auth0 / Okta | @bitwild/rockets — adapter validates IdP token; user id = IdP sub |
| Path B — built-in | @bitwild/rockets-auth (signup, login, JWT) |
@bitwild/rockets — same JWT; user id = your user row |
Multiple adapters in auth: [...] are supported when each credential resolves to the same AuthorizedUser.id (e.g. Firebase for users + API key for automation — see sample-code-review).
See also Run multiple auth credentials (chain) and Mix two persistence adapters.
The whole system rests on three TypeScript interfaces. Everything else is a default or a convenience built on top.
AuthAdapterInterface — the only thing the framework asks of your authentication.
interface AuthAdapterInterface {
authenticate(request: AuthRequest): Promise<AuthAttemptResult>;
}
type AuthAttemptResult =
| { matched: false } // not this adapter's credential
| { matched: true; user: AuthorizedUser } // recognised and validated
| { matched: true; error: HttpException }; // recognised but rejected
interface AuthorizedUser {
id: string;
sub: string;
email?: string;
userRoles?: { role: { name: string } }[]; // drives RBAC
claims?: Record<string, unknown>; // free-form IdP payload
}AuthServerGuard iterates a chain of adapters. matched: false means "try the next adapter". matched: true; user stops the chain. matched: true; error stops the chain and throws — no surprising credential passthrough.
RepositoryInterface<T> — the only thing the framework asks of your persistence.
The contract is provided by @bitwild/rockets-repository (a curated re-export of @concepta/nestjs-repository). Adapters that satisfy it: TypeORM (@concepta/nestjs-repository-typeorm), Firestore (@bitwild/rockets-repository-firestore), any custom adapter you write. Domain code uses @InjectDynamicRepository(EntityClass) and RepositoryInterface<EntityClass> — never @InjectRepository. The same handler runs against any adapter.
ResourceInput — the configuration shape that becomes a feature.
type ResourceInput =
| RocketsResourceConfig // hand-built CRUD config
| ReturnType<typeof defineResource> // CRUD with auto-defaults
| ReturnType<typeof defineModuleResource> // non-CRUD Nest slice
| ReturnType<typeof defineSubResource>; // nested CRUDbuildAppRegistrationPlan({ resourceDefinitions, repository, userMetadata }) walks the list, collects entities per adapter, materialises CrudModule features, and emits the final Nest module composition. This is where the "one options object" becomes Nest modules.
A NestJS backend started from scratch needs all of the following — Rockets ships them:
- A JWT guard and
/meroute (path A) or a complete authentication module (path B). - A list / read / create / update / delete controller per entity, with DTO validation and swagger schemas.
- TypeORM (or Firestore) module registration with the entity list — replaced by the planner deriving the list from
resources[]. - An owner-scoping hook so user A doesn't read user B's rows.
- A consistent error filter, a uniform
RepositoryInterface, transaction primitives. - The wiring that connects all of the above.
Your business logic, your DTOs, your entity classes, your custom hooks, your access-control rules. Rockets does not pretend to write those for you.
- Node 18+.
- A package manager (yarn 4 / npm / pnpm — examples below use yarn).
- A database adapter — TypeORM with any supported driver is the most common. Firestore works via
@bitwild/rockets-repository-firestore.
Install (minimal — one Rockets entry package is enough):
yarn add @bitwild/rockets \
@concepta/nestjs-repository-typeorm typeorm @nestjs/typeorm sqlite3 \
class-transformer class-validator reflect-metadata rxjsWhat installs automatically when you add @bitwild/rockets (transitive dependencies):
| Pulled in for you | Packages |
|---|---|
Other @bitwild/* |
rockets-core, rockets-common, rockets-crud, rockets-repository, rockets-access-control |
| Upstream motor | @concepta/nestjs-{repository,crud,hook,common,authentication,access-control,swagger-ui} (via the graph above) |
| Nest (Rockets runtime) | @nestjs/common, @nestjs/core, @nestjs/cqrs, @nestjs/swagger, @nestjs/config |
What you still add explicitly (and why):
| Package | Why not transitive |
|---|---|
@concepta/nestjs-repository-typeorm, typeorm, @nestjs/typeorm, driver (sqlite3, pg, …) |
Persistence adapter is an app choice — kept out of @bitwild/* so Firestore-only apps do not pull TypeORM |
class-transformer, class-validator, rxjs, reflect-metadata |
peerDependencies — npm/yarn expect the host Nest app to provide them (install peers or enable your package manager’s peer auto-install) |
Add @bitwild/rockets-core (or rockets-common, …) only if you import symbols from that package path in app code (e.g. OwnerStampHook from @bitwild/rockets-core). If everything comes from @bitwild/rockets re-exports, you do not need duplicate @bitwild/* lines.
Write an adapter (the only auth code you own):
// src/auth/jwt.adapter.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { verify } from 'jsonwebtoken';
import {
AuthAdapterInterface,
AuthAttemptResult,
AuthRequest,
extractBearerToken,
} from '@bitwild/rockets';
@Injectable()
export class JwtAdapter implements AuthAdapterInterface {
async authenticate(request: AuthRequest): Promise<AuthAttemptResult> {
const token = extractBearerToken(request);
if (token === null) return { matched: false };
try {
const payload = verify(token, process.env.JWT_SECRET!) as {
sub: string; email?: string;
};
return {
matched: true,
user: { id: payload.sub, sub: payload.sub, email: payload.email },
};
} catch {
return { matched: true, error: new UnauthorizedException() };
}
}
}Declare a resource — this is the entire CRUD definition:
// src/pet/pet.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('pet')
export class PetEntity {
@PrimaryGeneratedColumn('uuid') id!: string;
@Column() userId!: string;
@Column() name!: string;
@Column() species!: string;
}Add a TypeORM bootstrap helper in your app — intentionally not a @bitwild/* package, so core/server/auth stay free of a TypeORM dependency. It implements RepositoryBootstrap so the planner calls forRoot(entities) once from resources[] + userMetadata, without a hand-maintained entity list:
// src/repository/define-typeorm-repository.ts
import type { DynamicModule, PlainLiteralObject, Type } from '@nestjs/common';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { TypeOrmRepositoryModule } from '@concepta/nestjs-repository-typeorm';
import type { RepositoryBootstrap } from '@bitwild/rockets-core';
import type {
DynamicRepositoryModule,
RepositoryProviderOptions,
} from '@concepta/nestjs-repository';
export function defineTypeOrmRepository<Connection extends TypeOrmModuleOptions>(
connection: Connection,
): RepositoryBootstrap {
return {
name: 'typeorm-bootstrap',
forFeature(entities: RepositoryProviderOptions[]): DynamicRepositoryModule {
return TypeOrmRepositoryModule.forFeature(entities);
},
forRoot(entities: ReadonlyArray<Type<PlainLiteralObject>>): DynamicModule {
return TypeOrmModule.forRoot({ ...connection, entities: [...entities] });
},
};
}Why this exists: you pass only connection options (type, database, synchronize, …). You never maintain entities: [PetEntity, UserMetadataEntity, …] on TypeOrmModule.forRoot. When RocketsModule boots, the registration planner walks resources[], userMetadata.entity, and any entities contributed by auth integrations, then calls forRoot(mergedEntities) once and forFeature per table. Services use @InjectDynamicRepository(PetEntity) and get a RepositoryInterface<PetEntity> — registration is automatic as long as the entity appeared in that plan.
Compose the app:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { RocketsModule, defineResource } from '@bitwild/rockets';
import {
OwnerStampHook,
OwnerScopeHook,
} from '@bitwild/rockets-core';
import { JwtAdapter } from './auth/jwt.adapter';
import { PetEntity } from './pet/pet.entity';
import { UserMetadataEntity } from './user/user-metadata.entity';
import { UserMetadataCreateDto, UserMetadataUpdateDto } from './user/dto';
import { defineTypeOrmRepository } from './repository/define-typeorm-repository';
@Module({
imports: [
RocketsModule.forRoot({
auth: JwtAdapter,
userMetadata: {
entity: UserMetadataEntity,
createDto: UserMetadataCreateDto,
updateDto: UserMetadataUpdateDto,
},
repository: defineTypeOrmRepository({
type: 'sqlite',
database: ':memory:',
synchronize: true,
dropSchema: true,
}),
resources: [
defineResource({
entity: PetEntity,
hooks: [
OwnerStampHook.for(PetEntity),
OwnerScopeHook.for(PetEntity),
],
}),
],
}),
],
})
export class AppModule {}Run it:
yarn nest start
# GET /me (from MeController, returns user + userMetadata)
# PATCH /me (updates userMetadata)
# GET /pets (owner-scoped list)
# POST /pets (auto-stamps userId)
# GET /pets/:id (owner-scoped read)
# PATCH /pets/:id (owner-scoped update)
# DELETE /pets/:id (owner-scoped delete)
# Swagger at /apiYou wrote one adapter, one entity, one resource definition. The controllers, the validation pipeline, the global guard, the swagger document, the JWT route protection, and the owner scoping are all framework.
Install the same packages as above plus @bitwild/rockets-auth and the upstream @concepta/nestjs-* line (most are transitive dependencies; yarn install will pull them).
Compose with defineRocketsAuth(). Reuse the same defineTypeOrmRepository helper from path A and pass the same instance to both defineRocketsAuth({ persistence: { module: repo } }) and RocketsModule.forRoot({ repository: repo }). Register auth persistence rows via buildRocketsAuthResources() on resources:
import { Module } from '@nestjs/common';
import { defineRocketsAuth, buildRocketsAuthResources } from '@bitwild/rockets-auth';
import { RocketsModule } from '@bitwild/rockets';
import { defineTypeOrmRepository } from './repository/define-typeorm-repository';
const repo = defineTypeOrmRepository({
type: 'sqlite',
database: ':memory:',
synchronize: true,
dropSchema: true,
});
const rocketsAuthInput = {
persistence: {
module: repo,
entities: {
user: UserEntity,
userCredentials: UserCredentialEntity,
userOtp: UserOtpEntity,
role: RoleEntity,
userRole: UserRoleEntity,
federatedIdentity: FederatedEntity,
},
},
invitationEntity: InvitationEntity,
userMetadata: { entity: UserMetadataEntity, createDto, updateDto },
userCrud: { model: UserDto, dto: { createOne, updateOne } },
roleCrud: { model: RoleDto, dto: { createOne, updateOne } },
useFactory: () => ({
services: { mailerService },
authentication: {
ports: {
recoveryNotification: { /* command classes */ },
verifyNotification: { /* command classes */ },
},
},
settings: { /* role names, otp config, email templates */ },
}),
};
const rocketsAuth = defineRocketsAuth(rocketsAuthInput);
const rocketsAuthResources = buildRocketsAuthResources(
rocketsAuthInput.persistence,
rocketsAuthInput.invitationEntity,
);
@Module({
imports: [
RocketsModule.forRoot({
auth: rocketsAuth,
repository: repo,
resources: [...rocketsAuthResources, /* your defineResource bundles */],
}),
],
})
export class AppModule {}You now get POST /signup, POST /token/password, POST /token/refresh, PATCH /me/password, OTP flow, password recovery, admin user / role CRUD, invitation flow — plus everything path A gives you.
The monorepo ships runnable sample apps for both paths (yarn sample:dev and yarn sample-auth:dev from the repo root).
auth accepts a single AuthBootstrap or an array. Each entry is one of:
defineFirebaseAuth({ forRoot | forRootAsync })— Firebase Admin +FirebaseAuthAdapter(@bitwild/rockets-adapter-firebase).defineRocketsAuth(...)— built-in signup/login stack (@bitwild/rockets-auth); pair withbuildRocketsAuthResources()onresources.- App-local
AuthBootstrap—{ adapter, forRoot? }for custom adapters (seedefineApiKeyAuth()in sample-code-review).
Entity rows for auth-owned tables belong on resources[], not inside the auth helper.
import { defineFirebaseAuth } from '@bitwild/rockets-adapter-firebase';
import { defineModuleResource } from '@bitwild/rockets-core';
import { RocketsModule } from '@bitwild/rockets';
import { defineApiKeyAuth, apiKeyAuthResource } from './auth-api-key';
import { UserEntity } from './auth/user.entity';
RocketsModule.forRoot({
auth: [
defineFirebaseAuth({
forRootAsync: { useFactory: resolveFirebaseAuthModuleOptions },
}),
defineApiKeyAuth(),
],
userMetadata: { entity, createDto, updateDto },
repository,
resources: [
defineModuleResource({ entities: [UserEntity] }),
apiKeyAuthResource,
],
});The guard iterates in order. The first adapter that returns matched: true wins. If it returns matched: true; error, the chain stops and the error is thrown.
import { AuthPublic } from '@bitwild/rockets';
@Controller('health')
export class HealthController {
@Get() @AuthPublic() ok() { return { status: 'ok' }; }
}AuthServerGuard skips routes tagged with @AuthPublic. To skip the guard wholesale, pass enableGlobalGuard: false to RocketsModule.forRoot.
defineModuleResource is the escape hatch when you want CRUD generation off and full Nest control on.
import { defineModuleResource } from '@bitwild/rockets';
const billingFeature = defineModuleResource({
entities: [InvoiceEntity],
controllers: [BillingController],
providers: [BillingService],
exports: [BillingService], // exported = globally injectable
});RocketsCoreModule is global, so anything in exports is reachable from every other module — including the outer RocketsModule.forRootAsync factory's inject: list. Export the minimum to avoid name collisions.
import { defineSubResource } from '@bitwild/rockets';
const petTagResource = defineSubResource({
parent: PetEntity,
parentParam: 'petId',
parentFk: 'petId',
entity: PetTagEntity,
});The framework generates /pets/:petId/tags, filters by petId, and verifies the caller owns the parent via PathScopeGuard.
Use a small app-local defineTypeOrmRepository helper (full sample in Path A above). It implements RepositoryBootstrap from @bitwild/rockets-core and stays outside @bitwild/* packages so core/server/auth never take a TypeORM dependency. Firestore-only apps skip it and use @bitwild/rockets-repository-firestore instead.
What you declare vs what the framework registers
| You configure | Planner collects |
|---|---|
defineResource({ entity: PetEntity }) |
PetEntity → default repository adapter |
userMetadata: { entity: UserMetadataEntity, … } |
metadata row |
defineModuleResource({ entities: [InvoiceEntity], … }) |
extra tables (CRUD or not) |
defineRocketsAuth({ persistence: { entities: { user: UserEntity, … } } }) |
auth tables (path B) |
defineModuleResource({ entities: [{ entity: X, repository: FirestoreModule }] }) |
per-entity adapter override |
What you write in app.module.ts:
const repository = defineTypeOrmRepository({
type: 'sqlite',
database: ':memory:',
synchronize: true,
dropSchema: true,
});
@Module({
imports: [
RocketsModule.forRoot({
repository, // connection only — no entities: [...] here
userMetadata: { entity: UserMetadataEntity, createDto, updateDto },
resources: [
defineResource({ entity: PetEntity }),
defineModuleResource({
entities: [InvoiceEntity],
providers: [BillingService],
}),
],
}),
],
})
export class AppModule {}What you write in services/handlers — same for CRUD handlers, custom services, and access-query services:
import { InjectDynamicRepository } from '@bitwild/rockets-repository';
import type { RepositoryInterface } from '@bitwild/rockets-repository';
@Injectable()
export class PetModelService {
constructor(
@InjectDynamicRepository(PetEntity)
private readonly pets: RepositoryInterface<PetEntity>,
) {}
listForUser(userId: string) {
return this.pets.find({ where: { userId } });
}
}No TypeOrmModule.forFeature([PetEntity]) in feature modules. No @InjectRepository. If the entity is in the registration plan, @InjectDynamicRepository resolves at runtime.
Built-in auth (path B): pass the same repository instance to both entry points so one connection serves app tables and auth tables:
const repository = defineTypeOrmRepository({ type: 'sqlite', database: ':memory:', synchronize: true });
const rocketsAuth = defineRocketsAuth({
persistence: { module: repository, entities: { user: UserEntity, role: RoleEntity /* … */ } },
// …
});
@Module({
imports: [
RocketsModule.forRoot({
repository,
auth: rocketsAuth,
resources: [/* pet resources — no per-resource persistence block */],
}),
],
})
export class AppModule {}The default adapter goes in repository:. Override per entity inside a bundle:
import { defineModuleResource } from '@bitwild/rockets';
import { defineFirestoreRepository } from '@bitwild/rockets-repository-firestore';
const firestoreRepository = defineFirestoreRepository();
defineModuleResource({
entities: [
{
entity: AnalyticsEventEntity,
repository: firestoreRepository,
collection: 'analytics_events',
},
],
providers: [AnalyticsService],
});Everything else stays on the default adapter. The same RepositoryInterface<T> works across both.
Canonical mixed-store example: sample-code-review (defineTypeOrmRepository at root + defineFirestoreRepository on report entities). OPS layout: ops-micro-apps-pattern.md.
import { defineResource } from '@bitwild/rockets';
import { OwnerStampHook, OwnerScopeHook } from '@bitwild/rockets-core';
defineResource({
entity: PetEntity,
hooks: [
OwnerStampHook.for(PetEntity), // create/update: stamp userId
OwnerScopeHook.for(PetEntity), // list/read/update/delete: filter by userId
],
});Both default to a userId column; pass a second argument to override (OwnerStampHook.for(PetEntity, 'ownerId')). Hooks run at the repository layer, so direct (non-HTTP) calls are scoped too.
CRUD-generated controllers don't expose method signatures you can decorate. Use getActor inside the command / query handler:
import { CommandHandler } from '@nestjs/cqrs';
import { CrudCreateCommand, CrudWithBodyCommandHandler } from '@bitwild/rockets-crud';
import { getActor } from '@bitwild/rockets-core';
@CommandHandler(CrudCreateCommand)
export class PetCreateHandler extends CrudWithBodyCommandHandler {
async execute(cmd: CrudCreateCommand) {
const actor = getActor(cmd.context);
// actor.id, actor.email, actor.userRoles
return super.execute(cmd);
}
}In controllers you own, import @AuthUser() from @bitwild/rockets-common (same decorator the built-in /me route uses). AuthorizedUser types come from @bitwild/rockets or @bitwild/rockets-core.
The ACL primitives live in @bitwild/rockets-access-control (a re-export of @concepta/nestjs-access-control). Define a grant table, implement AccessControlServiceInterface to feed the guard with user + roles, register the module, decorate routes:
import {
AccessControlModule,
AccessControlGuard,
AccessControlReadOne,
} from '@bitwild/rockets-access-control';
AccessControlModule.forRoot({
settings: { rules: APP_ACL },
service: AcService,
});
// register AccessControlGuard via APP_GUARD
@Controller('pets')
class PetController {
@Get(':id') @AccessControlReadOne('pet') read() { /* ... */ }
}AccessControlServiceInterface.getUserRoles() typically returns user.userRoles?.map(ur => ur.role.name) ?? [] — the same shape AuthorizedUser.userRoles carries.
RocketsModule.forRoot({
auth, userMetadata, repository,
enableGlobalGuard: false,
disableController: { me: true },
});Useful when an upstream module already registers a global guard, or when your app provides its own /me.
import {
AbstractUpsertUserMetadataHandler,
AbstractGetUserMetadataHandler,
} from '@bitwild/rockets';
class MyUpsertHandler extends AbstractUpsertUserMetadataHandler { /* ... */ }
class MyGetHandler extends AbstractGetUserMetadataHandler { /* ... */ }
RocketsModule.forRoot({
/* ... */,
handlers: {
upsertUserMetadata: MyUpsertHandler,
getUserMetadata: MyGetHandler,
},
});The base classes call the dynamic repository against userMetadata.entity. Subclass to add side effects, audit logs, or alternative storage.
| Symptom | Likely cause | Fix |
|---|---|---|
Cannot find provider AUTH_ADAPTERS_TOKEN |
auth: option omitted |
Pass at least one adapter to RocketsModule.forRoot({ auth }). |
| Routes 401 even with a valid token | Adapter returns matched: false |
Read the token: extractBearerToken(request) must not be null. Check Authorization: Bearer <token> header on the request. |
| DTO fields missing from swagger | @nestjs/swagger CLI plugin is NOT enabled |
Add @ApiProperty() / @ApiPropertyOptional() to every public field — type inference alone won't populate the schema. |
OwnerScopeHook doesn't filter |
HookModule not registered in DI |
Don't remove HookModule.forRoot({}) from core's createCoreImports; without it, the hook resolver is undefined and decorators become silent no-ops. |
definitionTransform async wiring broken |
Missed merging defImports |
Always imports: [...defImports, ...createCoreImports(extras)]. Losing defImports silently breaks RAW_OPTIONS_TOKEN injection. |
Two Logger / AuditService collide |
Two bundles export classes with the same name | RocketsCoreModule is global; everything in a defineModuleResource exports array is reachable everywhere. Prefix the name (BillingPriceFormatter) or use an injection token. |
| Custom hook always returns 500 | Threw a generic Error or wrong exception type |
Use @concepta/nestjs-common domain exceptions (ModelValidationException, …) or map in your exception filter. Repository/HTTP hooks run inside upstream hook + CRUD pipeline — see @concepta/nestjs-hook docs. |
The runtime motor is the Concepta Nest modules (@concepta/nestjs-repository, @concepta/nestjs-crud, @concepta/nestjs-hook, @concepta/nestjs-common, @concepta/nestjs-access-control, @concepta/nestjs-authentication, and the domain modules used by built-in auth). Those packages own query execution, CRUD CQRS handlers, hook pipelines, RBAC guards, and — when you opt in — signup/login/user tables.
Rockets does not reimplement that behaviour. It configures and registers it: one RocketsModule.forRoot({ ... }) object is split by buildAppRegistrationPlan into the upstream RepositoryModule, CrudModule, HookModule, and related imports your app would otherwise wire by hand.
| Upstream motor | @bitwild/* facade |
Used for |
|---|---|---|
@concepta/nestjs-repository |
@bitwild/rockets-repository |
RepositoryInterface, dynamic repositories, transactions, repo hooks |
@concepta/nestjs-crud |
@bitwild/rockets-crud |
Generated controllers, CQRS commands/queries, default handlers |
@concepta/nestjs-hook |
@bitwild/rockets-common (with common, authentication, swagger-ui) |
HookModule, specifications, shared exceptions, Swagger UI |
@concepta/nestjs-access-control |
@bitwild/rockets-access-control |
Grant table, AccessControlGuard, route decorators |
@concepta/nestjs-repository-typeorm (app dep) |
app-local defineTypeOrmRepository |
SQL adapter — intentionally not a @bitwild/* package |
@concepta/nestjs-user, role, otp, password, invitation, federated, email, event |
wired inside @bitwild/rockets-auth |
Built-in auth HTTP + persistence rows (path B only) |
| Rockets layer | Role |
|---|---|
@bitwild/rockets-core |
Planner and contracts: defineResource, buildAppRegistrationPlan, AuthServerGuard, owner/path hooks, swagger registration |
@bitwild/rockets (server) |
External-auth presentation: MeController, default APP_GUARD, auth chain merge |
@bitwild/rockets-auth |
Built-in identity bundle: defineRocketsAuth() + buildRocketsAuthResources() |
Path B uses both @bitwild/rockets and @bitwild/rockets-auth: defineRocketsAuth() supplies the auth bootstrap; spread buildRocketsAuthResources() into resources; RocketsModule.forRoot({ auth, repository, resources }) still comes from the server package. They are sibling packages over core, not parent/child.
Repository injection (upstream contract, Rockets-local decorator):
- Recommended:
@InjectDynamicRepository(UserEntity)— key derived viaderiveEntityKey()so it matchesdefineResource({ entity: UserEntity }). - Escape hatch:
@InjectDynamicRepository('billing/invoice')when the registration key is namespaced or does not follow the entity class name (overrides, legacy schemas).
Override a default CRUD handler: set operations.<op>.commandHandler or queryHandler on the resource config — upstream CrudModule uses your class instead of the default; the defaults exist for convenience only.
Docs: architecture-diagram.html (Stargate · identity · micro apps) · ops-micro-apps-pattern.md.
If you maintain @concepta/nestjs-* modules, Rockets is a consumer and configuration façade — not a fork.
| Topic | Current decision |
|---|---|
| Your modules stay the motor | RepositoryInterface, CrudModule, HookModule, RBAC, and identity domains are unchanged upstream; Rockets calls them through buildAppRegistrationPlan. |
| What Rockets owns | defineResource, defineModuleResource, AuthAdapterInterface + guard chain, RepositoryBootstrap, swagger registration, /me (server), defineRocketsAuth() (auth bundle). |
@bitwild/rockets-common |
Re-exports @concepta/nestjs-hook, nestjs-common, nestjs-authentication, nestjs-swagger-ui plus small helpers (deriveEntityKey, …). It is not a rename of “utils” and not a replacement for the upstream app-module composition pattern — that wiring still lives in Concepta; Rockets adds a second entry point (RocketsModule.forRoot) that feeds the same motors. |
| Port backlog (server path) | On v8 today: repository, crud, hook, common, authentication. Still on v7 in this monorepo: access-control, swagger-ui (and email / event on the auth path). Finishing the access-control v8 port unblocks RBAC without changing Rockets’ public API. |
| Repo migration | Moving all of nestjs-modules into this git repo is optional for product validation. Shipping fixes against published @concepta/* alphas is fine; monorepo colocation is for AI context and version lock, not a prerequisite to use Rockets. |
| Safe to keep building on | These are intentional, tested surfaces — not throwaway experiments: AuthAdapterInterface.authenticate, RepositoryInterface + dynamic repository keys (class or string token), defineResource / planner-driven entity registration, defineRocketsAuth({ persistence: { module } }) sharing one repository instance with RocketsModule.forRoot. |
Custom validation / business rules: use upstream @concepta/nestjs-hook (Spec, UseHooks, repository hooks) or replace a CRUD operation handler. Throw domain exceptions from @concepta/nestjs-common (ModelValidationException, etc.) so filters map them to 4xx — a bare Error in a hook often surfaces as 500.
| Package | npm name | Purpose | Docs | Status |
|---|---|---|---|---|
packages/rockets-common |
@bitwild/rockets-common |
Curated re-exports of @concepta/nestjs-{hook,common,authentication,swagger-ui} + 6 local helpers (deriveEntityKey, whitelistedFromDto, stripUndefined, createRepositoryContext, getErrorDetails, logAndGetErrorDetails). |
README | stable |
packages/rockets-repository |
@bitwild/rockets-repository |
Abstract data-access contract: RepositoryInterface, Where/OrderBy/Join helpers, transactions, hooks. Local class-or-string variant of InjectDynamicRepository. |
README | stable |
packages/rockets-repository-firestore |
@bitwild/rockets-repository-firestore |
Firestore adapter implementing RepositoryAdapter. Per-entity opt-in via defineFirestoreRepository() + collection on the entity row. Tests inject InMemoryFirestoreBackend explicitly. |
README | preview |
packages/rockets-crud |
@bitwild/rockets-crud |
Generic CRUD module + ConfigurableCrudBuilder. Base CQRS handler classes (CrudCreateHandler, etc.) and the Crud*Command / Crud*Query pairs. |
README | stable |
packages/rockets-access-control |
@bitwild/rockets-access-control |
RBAC: AccessControlModule, AccessControlGuard, operation decorators, CanAccess query checks. |
README | stable (upstream still on v7) |
packages/rockets-core |
@bitwild/rockets-core |
Composition planner (not the data/CRUD motor). Auth chain, buildAppRegistrationPlan, defineResource / defineModuleResource / defineSubResource, owner / audit / path-scope hooks, swagger registration. |
README | stable |
packages/rockets-server |
@bitwild/rockets |
External-auth presentation layer. MeController, APP_GUARD opt-in, auth chain. |
README | stable |
packages/rockets-server-auth |
@bitwild/rockets-auth |
Built-in auth: signup, login, OTP, recovery, invitations, roles, admin user CRUD. defineRocketsAuth(). |
README | alpha (private package today; first 1.0-track release flips it public) |
packages/rockets-adapter-firebase |
@bitwild/rockets-adapter-firebase |
Firebase Auth adapter implementing AuthAdapterInterface. |
README | preview |
rockets/
├── packages/
│ ├── rockets-common/ @concepta hook/common/auth/swagger re-exports
│ ├── rockets-repository/ Abstract data-access contract
│ ├── rockets-repository-firestore/ Firestore adapter
│ ├── rockets-crud/ Generic CRUD
│ ├── rockets-access-control/ RBAC
│ ├── rockets-core/ Planner + auth wiring (motor = @concepta/*)
│ ├── rockets-server/ External-auth presentation
│ ├── rockets-server-auth/ Built-in auth
│ └── rockets-adapter-firebase/ Firebase auth adapter
├── examples/ sample-server-auth (identity), sample-server (micro app), sample-code-review (micro app + mixed DB)
├── docs/ architecture-diagram.html, ops-micro-apps-pattern.md
└── package.json Yarn 4 workspace root
- Rockets packages:
1.0.0-alpha.7(exceptrockets-repository-firestoreandrockets-adapter-firebaseat1.0.0-alpha.0). - Upstream Concepta packages: v8 line at
8.0.0-alpha.5. Three packages still on v7 (@concepta/nestjs-access-control,@concepta/nestjs-email,@concepta/nestjs-event) pending the v8 port — version-mismatched intentionally and tested in CI. - NestJS:
^11.0.0, pinned to11.1.18via rootresolutions. - Node:
>=18.0.0.
| Command | Purpose |
|---|---|
yarn install && yarn build |
Bootstrap + compile every local @bitwild/* package. |
yarn test |
Unit tests (jest, 30s timeout). |
yarn test:e2e |
E2E tests across all packages and sample apps. |
yarn lint / yarn lint:fix |
ESLint. |
yarn lint:md |
Markdown lint. |
yarn sample:dev |
Run sample-server in watch mode. |
yarn sample-auth:dev |
Run sample-server-auth in watch mode. |
yarn sample-code-review:dev |
Build + run the full-stack example. |
yarn generate-swagger |
Dump the OpenAPI spec from sample-server-auth. |
- Open an issue first for anything beyond a minor bug fix or doc tweak.
- Match the existing patterns: read the surrounding code before editing, prefer minimal diffs, no
any, noas unknown as Type. - Run
yarn lint && yarn test && yarn test:e2ebefore sending a PR. - The repo uses Conventional Commits (
feat:,fix:,chore:, …).huskyruns commit-msg + pre-commit hooks; do not bypass them.
Report vulnerabilities privately through GitHub Security Advisories for this repository. Do not open public issues for security bugs.
BSD-3-Clause.