npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@joyb-works/nestjs-futkaey

v0.0.4

Published

NestJS => TypeORM DDD patterns with configurable tenancy support

Readme

NestJS Futkaey

Say it right: FOO-t-keh (Φούτ και — from Greek "Fut and").
Fut (φούτ) = "FOO-t" — the t is pronounced, not silent. Kai (και) = "keh".

NestJS Futkaey is a library that brings Domain-Driven Design (DDD) patterns to NestJS applications backed by TypeORM. It provides configurable multi-tenancy, aggregate roots, repository abstractions, and dynamic entities—dramatically reducing the infrastructure layer so you can focus on domain logic.

Why Futkaey?

In traditional DDD + NestJS setups, you spend significant effort building:

  • Request context propagation (tenant, user, correlation ID)
  • Tenant-scoped repository filters
  • Domain ↔ persistence mapping
  • Audit fields (createdBy, updatedBy, soft delete)
  • Aggregate roots with event sourcing semantics

Futkaey provides all of this out of the box. Your infrastructure layer becomes a thin integration—you declare entities, domain models, and repositories; Futkaey handles context, tenancy, and marshalling.

Core philosophy

  1. Domain first — Your aggregates, entities, and value objects are the center. Persistence adapts to them.
  2. Configurable tenancy — Tenancy is not hardcoded. Use tenantId, companyId, customerId, or any hierarchy you need.
  3. Infrastructure as composition — Mix in repositories; no boilerplate for tenant scoping or marshalling.
  4. Request context — Tenant, user, correlation ID flow through the request without passing them explicitly.

Features

  • Configurable multi-tenancy — Regular, single-tenant, or custom hierarchy (e.g. tenantIdcompanyIdcustomerId)
  • Dynamic tenant fields — Use tenantId, companyId, customerId, or any field name you choose
  • Aggregate rootsBasicAggregateRoot and TenantAggregateRoot with @AggregateProp, domain events, and context
  • Repository mixinRepositoryMixin(Entity, Model) for automatic tenant filtering and entity↔domain mapping
  • Dynamic entities — Per-aggregate collections (e.g. card_xyz_transactions) with @DynamicEntity and DynamicRepository
  • CQRS-ready — Integrates with @nestjs/cqrs
  • Audit trailcreatedBy, updatedBy, deletedBy, soft delete
  • MongoDB & SQL — Works with TypeORM drivers (MongoDB, PostgreSQL, MySQL, etc.)

Installation

npm install @joyb-works/nestjs-futkaey

Peer dependencies (install in your app):

npm install @nestjs/common @nestjs/core @nestjs/typeorm typeorm reflect-metadata
# For multi-tenant mode:
npm install nestjs-cls
# Optional, for CQRS:
npm install @nestjs/cqrs

Quick Start

1. Configure the module

import { Module } from '@nestjs/common';
import { NestjsFutkaeyModule, setModuleOptions } from '@joyb-works/nestjs-futkaey';
import { TypeOrmModule } from '@nestjs/typeorm';

// Configure tenancy before the app boots (e.g. in main.ts or a root module)
setModuleOptions({
  tenancy: {
    mode: 'multi-tenant',
    tenant: {
      fieldName: 'companyId',      // Your tenant field name
      headerName: 'x-company-id',  // HTTP header to read tenant from
    },
  },
  audit: {
    userIdHeader: 'x-user-id',
    correlationIdHeader: 'x-correlation-id',
    enableSoftDelete: true,
  },
});

@Module({
  imports: [
    NestjsFutkaeyModule.forRoot({
      tenancy: { mode: 'multi-tenant', tenant: { fieldName: 'companyId', headerName: 'x-company-id' } },
      audit: { userIdHeader: 'x-user-id' },
    }),
    TypeOrmModule.forRoot({ /* ... */ }),
  ],
})
export class AppModule {}

2. Tenant field names: tenantId, companyId, customerId

You can use any tenant field name. Examples:

| Use case | fieldName | headerName | |------------------------|--------------|-----------------| | B2B SaaS (per company) | companyId | x-company-id | | Multi-tenant platforms | tenantId | x-tenant-id | | Customer-scoped data | customerId | x-customer-id | | Partner/white-label | partnerId | x-partner-id |

Your entities declare the matching column:

@Entity('orders')
@TenantAware()
export class OrderEntity extends AuditableEntity {
  @ObjectIdColumn() id: ObjectId;
  @Column() companyId: ObjectId;  // matches fieldName in config
  @Column() total: number;
}

3. Domain model (aggregate)

import { BasicAggregateRoot, TenantAggregateRoot, AggregateProp, DomainAccess, newDatabaseId } from '@joyb-works/nestjs-futkaey';

// Non-tenant model (e.g. global lookup)
export class LookupModel extends BasicAggregateRoot {
  @AggregateProp() name!: string;
  @AggregateProp() code!: string;

  constructor(access: DomainAccess) {
    super(access);
  }

  create(payload: { name: string; code: string }) {
    this.access = DomainAccess.Create;
    this.id = newDatabaseId().toString();
    this.name = payload.name;
    this.code = payload.code;
    this.apply(new LookupCreatedEvent(this.id, this.context, payload));
    return this;
  }
}

// Tenant-scoped model (has companyId, tenantId, etc. from config)
export class OrderModel extends TenantAggregateRoot {
  @AggregateProp() total!: number;

  constructor(access: DomainAccess) {
    super(access);
  }

  create(payload: { total: number }) {
    this.access = DomainAccess.Create;
    this.id = newDatabaseId().toString();
    this.total = payload.total;
    this.apply(new OrderCreatedEvent(this.id, this.context, payload));
    return this;
  }
}

4. Repository

import { RepositoryMixin } from '@joyb-works/nestjs-futkaey';
import { Injectable } from '@nestjs/common';
import { OrderEntity, OrderModel } from './domain';

const Base = RepositoryMixin(OrderEntity, OrderModel);

@Injectable()
export class OrderRepository extends Base {}

All find, findOne, save, update calls automatically filter by tenant and inject tenant/user context.


Tenancy modes

Regular (no tenancy)

tenancy: { mode: 'regular' }

No tenant filtering. Use for single-tenant or non-multi-tenant apps.

Multi-tenant (single level)

tenancy: {
  mode: 'multi-tenant',
  tenant: {
    fieldName: 'companyId',
    headerName: 'x-company-id',
  },
}

One tenant field. HTTP requests must include x-company-id (or your header).

Custom hierarchy (multiple levels)

tenancy: {
  mode: 'custom-hierarchy',
  hierarchy: [
    { fieldName: 'tenantId', headerName: 'x-tenant-id' },
    { fieldName: 'companyId', headerName: 'x-company-id' },
    { fieldName: 'customerId', headerName: 'x-customer-id' },
  ],
}

Multiple context levels. Entities can declare any of these columns; Futkaey filters and injects them automatically.

Custom hierarchy with required/optional levels

tenancy: {
  mode: 'custom-hierarchy',
  hierarchy: [
    { fieldName: 'tenantId', headerName: 'x-tenant-id', required: true },
    { fieldName: 'companyId', headerName: 'x-company-id', required: true },
    { fieldName: 'customerId', headerName: 'x-customer-id', required: false },
  ],
}

// Entity can use any of these
@Entity('invoices')
@TenantAware()
export class InvoiceEntity extends AuditableEntity {
  @ObjectIdColumn() id: ObjectId;
  @Column() tenantId: ObjectId;
  @Column() companyId: ObjectId;
  @Column() customerId?: ObjectId;
  @Column() amount: number;
}

Headers are read in order. All levels are available in context.tenantContext for filtering and injection.

Reading context programmatically

import { getContextValue } from '@joyb-works/nestjs-futkaey';

const companyId = getContextValue('x-company-id');
const tenantId = getContextValue('x-tenant-id');

Use getContextValue when you need tenant/user values outside of repository or aggregate flows.


DDD and infrastructure omission

In a typical DDD layering:

┌──────────────────────────────────────────────────────────────┐
│  Presentation (Controllers, DTOs)                            │
├──────────────────────────────────────────────────────────────┤
│  Application (Commands, Queries, Handlers)                   │
├──────────────────────────────────────────────────────────────┤
│  Domain (Aggregates, Entities, Value Objects)                │
├──────────────────────────────────────────────────────────────┘
│  Infrastructure (Repositories, Persistence Adapters)  ← Futkaey drastically reduces this layer
└───────────────────────────────────────────────────────────────

With Futkaey:

  • Context propagation — Tenant, user, correlation ID flow automatically via CLS
  • Tenant filtering — Repositories add WHERE tenantId = ? (or equivalent) without you writing it
  • Entity ↔ Domain mappingMarshallerMixin handles TypeORM entities ↔ domain models
  • Audit fieldscreatedBy, updatedBy set from request context

Your infrastructure becomes: declare the entity, declare the model, mix in the repository. No custom repository logic for tenancy or marshalling.

| Concern | Without Futkaey | With Futkaey | |---------------------|---------------------------------|-------------------------------| | Request context | Custom middleware + CLS | Built-in ContextMiddleware | | Tenant filtering | Manual WHERE in every query | Automatic via contextualize | | Audit fields | Manual createdBy, updatedBy | Set from context on save | | Entity ↔ Domain map | Manual mapping in repositories | MarshallerMixin handles it | | Tenant injection | Manual before save | Automatic on create/update |


Dynamic entities

For per-aggregate collections (e.g. one cards collection and a dynamic transactions collection per card):

@Entity('cards')
@TenantAware()
export class CardEntity extends AuditableEntity {
  @ObjectIdColumn() id: ObjectId;
  @Column() companyId: ObjectId;
  @Column() lastFour: string;
  // ...
}

@DynamicEntity({
  collectionNameGenerator: (creditcardId) => `card_${creditcardId}_transactions`,
  idField: 'creditcardId',
})
export class TransactionEntity extends AuditableEntity {
  @Column() creditcardId: ObjectId;
  @Column() amount: number;
  @Column() merchant: string;
  // ...
}

Use DynamicRepository with the entity class and aggregate ID (e.g. creditcardId) to read/write. The collection name is computed at runtime (e.g. card_<id>_transactions).


Samples

See the samples/ directory for full NestJS examples:

  • samples/01-basic — Regular mode, single tenant
  • samples/02-multi-tenant — Multi-tenant with companyId
  • samples/03-custom-hierarchytenantIdcompanyIdcustomerId
  • samples/04-dynamic-entities — Cards collection + per-card transactions (dynamic collections)

API reference

| Export | Description | |-----------------------|-----------------------------------------| | NestjsFutkaeyModule | NestJS module | | BasicAggregateRoot | Non-tenant aggregate root | | TenantAggregateRoot | Tenant-scoped aggregate root | | AggregateProp | Marks domain properties for marshalling | | RepositoryMixin | Repository with tenant + marshalling | | AppRepository | Repository interface type | | DynamicEntity | Per-aggregate collection decorator | | DynamicRepository | Repository for dynamic entities | | TenantAware | Marks entity as tenant-aware | | setModuleOptions | Set tenancy/audit config imperatively | | getContextValue | Read value from CLS (e.g. tenant, user) |


Release process

Futkaey uses standard-version and Commitlint for versioning and changelog.

Conventional commits

Follow Conventional Commits:

<type>(<scope>): <subject>

Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore

Examples: feat(repository): add findByIds, fix(aggregate): correct tenant context

Local release

npm run release        # Bump version, update CHANGELOG, create tag
npm run release:first  # First release (no previous tag)

CI/CD

On merge to main: CI (ci.yml) runs lint/build/test; Release (release-pr.yml) runs standard-version, pushes tag, publishes to npm.

Requirements: Add NPM_TOKEN as repository secret for publishing. For the initial release (no tags yet), the workflow runs standard-version --first-release to create v0.0.1.


License

MIT