Skip to main content

Module Configuration

Use whenyou are looking up a specific option or its default.

You getevery key forRoot(), forRootAsync() and forFeature() accept, with type and default.

For a guided introduction see the Quick Start.

forRoot()​

forRoot() registers the transport globally. Call it once in your root AppModule. It creates the shared NATS connection, codec and event bus, and, unless publisherOnly is set, the streams and consumers.

src/app.module.ts
import { Module } from '@nestjs/common';
import { JetstreamModule, TransportEvent, toNanos } from '@horizon-republic/nestjs-jetstream';

@Module({
imports: [
JetstreamModule.forRoot({
name: 'user-events',
servers: ['nats://localhost:4222'],
events: {
stream: {
max_age: toNanos(30, 'days'),
max_bytes: 10 * 1024 * 1024 * 1024, // 10 GB
num_replicas: 3,
},
consumer: {
max_ack_pending: 500,
ack_wait: toNanos(30, 'seconds'),
},
consume: { idle_heartbeat: 10_000 },
concurrency: 200,
ackExtension: true,
},
rpc: { mode: 'core', timeout: 10_000 },
shutdownTimeout: 15_000,
hooks: {
[TransportEvent.Error]: (err, ctx) => console.error(`[${ctx}]`, err),
[TransportEvent.Connect]: (server) => console.log(`Connected to ${server}`),
},
}),
],
})
export class AppModule {}

How name maps to streams and subjects​

Given name: 'user-events', the event stream is user-events__microservice_ev-stream, its subjects are user-events__microservice.ev.{pattern} and its consumer is user-events__microservice_ev-consumer. The __microservice suffix keeps these apart from other NATS clients on the cluster. Naming Conventions has the full table for every kind.

forRootAsync()​

Use forRootAsync() to load the options from environment variables or a config service. It supports three patterns.

useFactory (most common)​

src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JetstreamModule } from '@horizon-republic/nestjs-jetstream';

@Module({
imports: [
ConfigModule.forRoot(),
JetstreamModule.forRootAsync({
name: 'orders',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const mode = config.get<'core' | 'jetstream'>('RPC_MODE', 'core');

return {
servers: [config.getOrThrow('NATS_URL')],
rpc: mode === 'jetstream' ? { mode, timeout: 60_000 } : { mode },
shutdownTimeout: config.get('SHUTDOWN_TIMEOUT', 10_000),
};
},
}),
],
})
export class AppModule {}
The name lives outside the factory

name sits at the top level of forRootAsync(), not in the factory's return value: DI token generation needs it before the factory runs. imports is optional in all three patterns.

useExisting​

Reuse a provider that is already registered:

JetstreamModule.forRootAsync({
name: 'orders',
imports: [NatsConfigModule],
useExisting: NatsConfigService,
})

NatsConfigService must implement Omit<JetstreamModuleOptions, 'name'> directly: the instance itself is the options object, no factory method is called on it.

useClass​

Like useExisting, but the module instantiates the class:

JetstreamModule.forRootAsync({
name: 'orders',
useClass: NatsConfigService,
})

forFeature()​

forFeature() creates a JetstreamClient for a target service on the connection forRoot() opened. Import it in each feature module that talks to that service:

src/orders/orders.module.ts
import { Module } from '@nestjs/common';
import { JetstreamModule } from '@horizon-republic/nestjs-jetstream';
import { OrdersService } from './orders.service';

@Module({
imports: [
JetstreamModule.forFeature({ name: 'users' }),
JetstreamModule.forFeature({ name: 'payments' }),
],
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {}

Injecting clients​

The injection token is the name you passed to forFeature():

src/orders/orders.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class OrdersService {
constructor(@Inject('users') private readonly usersClient: ClientProxy) {}

getUser(id: number) {
return firstValueFrom(this.usersClient.send('user.get', { id }));
}
}

getClientToken(name) returns the same string. A client bound to a named connection (forFeature({ name, connection })) has a namespaced token instead: inject it with getClientToken(name, connection). See Multiple connections.

forFeature() options​

OptionTypeDefaultWhat it does
namestringrequiredTarget service name. Subjects are built from it.
connectionstringthe default connectionConnection the client publishes on.
codecCodecthe forRoot() codecPer-client codec override.
subjectPrefixTargetSubjectPrefixesthe naming conventionThe target's custom subject prefixes. See below.

Per-client codec override​

A client falls back to the forRoot() codec unless you pass its own:

import { MsgPackCodec } from './codecs/msgpack.codec';

JetstreamModule.forFeature({
name: 'legacy-service',
codec: new MsgPackCodec(),
})

See Custom Codec for how to implement the Codec interface.

Targets with a custom subject prefix​

A client builds the target's subjects from the naming convention. When the target sets subjectPrefix on events, ordered or rpc, repeat those values in forFeature(), or the client publishes where nothing listens:

// The asset service
JetstreamModule.forRoot({
name: 'asset',
servers,
rpc: { mode: 'jetstream', subjectPrefix: 'acme.rpc.asset' },
events: { subjectPrefix: 'acme.ev.asset' },
});

// Any service that calls it
JetstreamModule.forFeature({
name: 'asset',
subjectPrefix: { rpc: 'acme.rpc.asset', events: 'acme.ev.asset' },
});
KeyMirrors the target's
eventsevents.subjectPrefix
orderedordered.subjectPrefix
rpcrpc.subjectPrefix
scheduleschedule.subjectPrefix

Kinds you leave out keep the convention. Delayed events follow the prefix too, so scheduleAt() lands in the target's {prefix}_sch. namespace, and client.schedule() writes its holder under the target's {prefix}_cron.. Broadcast has no key: it is one stream shared by every service and follows the caller's own broadcast.subjectPrefix.

Prefixes are validated when the client is created, with the same rules as forRoot(). Passing subjectPrefix to a client that targets the service itself fails at startup, because its own prefixes already come from forRoot().

Full options reference​

Every field in JetstreamModuleOptions. Blocks that need more than a line have their own section below.

Root options​

OptionTypeDefaultWhat it does
namestringrequiredDrives stream, consumer and subject naming. Unique per service.
serversstring[]noneNATS server URLs. Required unless you use connections, and supplying both fails at startup.
connectionsRecord<string, JetstreamConnectionOptions>noneOne entry per NATS cluster, mutually exclusive with servers. See connections.
defaultConnectionstringnoneWhich connection unqualified handlers and clients bind to.
codecCodecJsonCodecMessage serializer. See Custom Codec.
publisherOnlybooleanfalsetrue skips every stream, consumer and route. See publisher-only mode.
rpcRpcConfig{ mode: 'core' }RPC transport mode. See RpcConfig.
eventsStreamConsumerOverridesproduction defaultsWorkqueue event stream and consumer. See StreamConsumerOverrides.
broadcastStreamConsumerOverridesproduction defaultsBroadcast event stream and consumer, same shape as events.
scheduleStreamConsumerOverridesproduction defaultsRecurring schedules stream and consumer, same shape as events.
orderedOrderedEventOverridesproduction defaultsOrdered event consumers. See OrderedEventOverrides.
dlqDlqOptions | booleanenabledRepublishes exhausted messages to a dedicated stream. See dlq.
onDeadLetter(info: DeadLetterInfo) => Promise<void>noneAwaited before term() when a message exhausts every delivery attempt. If it throws, the message is naked.
hooksPartial<TransportHooks>noneLifecycle hook handlers. Unset hooks are ignored in silence.
metricsMetricsOptionnonePrometheus metrics. true for defaults, or a MetricsConfig. See Metrics.
otelOtelOptions | booleanenabledOpenTelemetry tracing, a no-op without an SDK. false turns it off. See Tracing.
metadataMetadataRegistryOptionsautoHandler metadata in NATS KV, on when any handler has meta. No-op under publisherOnly. See metadata.
provisioningProvisioningOptions{ management: Auto }Who owns streams and consumers. See provisioning.
shutdownTimeoutnumber10_000Drain budget in milliseconds. The connection is closed when it runs out. See Graceful Shutdown.
connectionOptionsPartial<ConnectionOptions>noneRaw NATS options for TLS, auth and reconnection. See connectionOptions.

name, hooks, metrics, otel and onDeadLetter stay at the root even when you use connections: they describe or observe the service as a whole.

connections​

Each entry inherits every root field it does not name, and may override critical, publisherOnly, codec, connectionOptions, events, broadcast, schedule, ordered, rpc, dlq, metadata, provisioning and shutdownTimeout.

critical defaults to true. A false connection connects lazily in the background, retrying with exponential backoff capped at 30 s. It reports degraded health instead of failing readiness.

JetstreamModule.forRoot({
name: 'orders',
defaultConnection: 'primary',
events: { concurrency: 4 }, // inherited by both
connections: {
primary: { servers: ['nats://primary:4222'] },
analytics: {
servers: ['nats://analytics:4222'],
critical: false, // startup does not depend on it
events: { concurrency: 32 }, // overrides the root value
},
},
})
The merge is one level deep

Naming a block such as events replaces the root block entirely instead of merging into it. Above, analytics gets concurrency: 32 and loses any other events key set at the root. Repeat the parts you still want.

defaultConnection is optional when the map has a default key, and required otherwise. See Multiple connections.

dlq​

Exhausted messages are republished to a dedicated DLQ stream with tracking headers. Pass true for the defaults, or false to turn it off and leave them where they land.

FieldTypeDefaultWhat it does
streamStreamConfigOverridesDLQ defaultsStream overrides, name included.
managementManagementModefalls back to provisioning.management, then AutoAuto provisions the stream, Manual binds to an existing one. The stream has no consumer.

See Built-in DLQ stream and External DLQ.

Bind-only deployments

Under provisioning: { management: Manual } the implicit default stands down, since a service that provisions nothing should not fail boot over a stream nobody asked for. Set dlq explicitly to bind to an externally provisioned one.

retry​

readonly number[] | false, default [2000, 10000], set per kind on events, broadcast and schedule. Delay in milliseconds before each redelivery after a handler throws or calls ctx.retry(). Index 0 is the first retry, and the last entry repeats once the curve runs out. false naks immediately, which burns every attempt as fast as the handler can fail.

The delays are applied client-side when the message is naked, not through the consumer's backoff, so ack_wait keeps its configured value.

events: {
consumer: { max_deliver: 5 },
retry: [1_000, 5_000, 30_000, 60_000],
}

metadata​

FieldTypeDefaultWhat it does
bucketstring'handler_registry'KV bucket name.
replicasnumber1Bucket replicas, an odd number no larger than the cluster.
ttlnumber30_000Entry TTL in milliseconds, at least 5_000. Each pod refreshes its entries every ttl / 2.

See Handler Metadata.

provisioning​

FieldTypeDefaultWhat it does
managementManagementModeManagementMode.AutoAuto creates and updates every entity. Manual binds to existing ones and fails at boot if any are absent.
preflightStorageCheckbooleanfalseCalls getAccountInfo() before provisioning and warns if the streams would not fit the account budget. Never blocks boot.
allowDestructiveMigrationbooleanfalseRecreates a stream through blue-green sourcing when an immutable property such as storage changes. Off, the conflict is logged and the stream keeps its config. See Stream Migration.

Override management per entity through events.management, broadcast.management and so on. retention is never migrated: the kind decides it, and a mismatch is always an error. Resolution order is per-entity, then global, then Auto. See Bring Your Own Infrastructure.

RpcConfig​

A discriminated union on mode. Pick by whether commands must survive handler downtime.

modePersistenceDefault timeoutBest for
'core'none, NATS native request/reply30_000 (30 s)Low-latency queries and lookups where in-flight requests can error
'jetstream'commands persisted before delivery180_000 (3 min)Commands that must survive a handler restart (payments, state)
// Core mode (default) -- NATS native request/reply
rpc: { mode: 'core', timeout: 10_000 }

// JetStream mode -- commands persisted in a stream
rpc: {
mode: 'jetstream',
timeout: 60_000,
stream: { max_age: toNanos(1, 'minutes') }, // stream overrides
consumer: { max_deliver: 3 }, // consumer overrides
}

The 'jetstream' variant is StreamConsumerOverrides plus mode and timeout, so stream, consumer, consume, concurrency, ackExtension, management and subjectPrefix all mean what they mean elsewhere. retry is absent: a command is delivered once.

Timeout is milliseconds, and applies to both sides

Writing timeout: 30 means 30 ms, which is a bug in almost every case. Use timeout: 30_000 for 30 seconds. The value bounds both the caller's wait and the handler's run, each side reading it from its own forRoot().

See RPC Patterns for a full comparison of the two modes.

StreamConsumerOverrides​

Accepted by events, broadcast, schedule and the 'jetstream' variant of rpc. Overrides merge with the production defaults, so name only what you want to change.

FieldTypeDefaultWhat it does
streamStreamConfigOverridesproduction defaultsStream overrides such as max_age and max_bytes. retention, subjects and description are owned by the transport and not settable.
consumerPartial<ConsumerConfig>production defaultsConsumer overrides such as max_deliver and ack_wait.
consumePartial<ConsumeOptions>nats.js defaultsOptions for nats.js consume(): prefetch, idle_heartbeat, refill thresholds. Do not mix the message-based and byte-based thresholds.
concurrencynumberunlimited, bounded by max_ack_pendingHandler executions in flight at once; the rest queue FIFO. Below max_ack_pending, raise ack_wait too, since queued messages' ack timers keep running.
ackExtensionAckExtensionConfigfalseExtends the ack deadline with msg.working() while a handler runs: true every ack_wait / 2, a number every that many milliseconds, { interval?, maxDuration? } with a per-message limit. Extension stops after 5 minutes by default.
retryreadonly number[] | false[2000, 10000]Redelivery curve. See retry. Not on rpc.
stream.namestring{service}__microservice_ev-stream and friendsBind to an externally provisioned stream with a custom name.
consumer.durable_namestring{service}__microservice_ev-consumer and friendsBind to an externally provisioned consumer.
managementEntityManagementfalls back to provisioning.management, then AutoPer-kind control, with separate stream and consumer entries.
subjectPrefixstring{service}__microservice.ev. and friendsCustom subject prefix. The trailing dot is normalized.
import { ManagementMode, toNanos } from '@horizon-republic/nestjs-jetstream';

events: {
stream: { max_age: toNanos(3, 'days'), name: 'platform_orders_stream' },
consumer: { max_deliver: 5, ack_wait: toNanos(30, 'seconds') },
management: { stream: ManagementMode.Manual, consumer: ManagementMode.Auto },
subjectPrefix: 'company.orders.', // publishes to company.orders.{pattern}
}

A custom prefix turns subjects into {prefix}{pattern}, and consumers then use exact filter_subjects entries instead of a single wildcard filter. See Bring Your Own Infrastructure for how prefixes interact with scheduling subjects.

The toNanos() helper

NATS JetStream uses nanoseconds for every time-based setting. toNanos(value, unit) converts a readable duration, taking 'ms', 'seconds', 'minutes', 'hours' or 'days'.

OrderedEventOverrides​

Ordered events use a separate stream with Limits retention and deliver in strict sequence, at most once. The consumers are ephemeral and managed by the @nats-io/jetstream client, so this block is smaller than StreamConsumerOverrides.

FieldTypeDefaultWhat it does
streamStreamConfigOverridesproduction defaultsStream overrides such as max_age and max_bytes.
consumerOrderedConsumerOverridesnats.js defaultsOrdered-consumer options in raw nats.js spelling: deliver_policy, opt_start_seq, opt_start_time, replay_policy, inactive_threshold, headers_only. filter_subjects comes from the handlers.
managementManagementModefalls back to provisioning.management, then AutoOrdered consumers are ephemeral, so only the stream has a mode.
subjectPrefixstringlibrary conventionCustom subject prefix for ordered-event subjects.
import { DeliverPolicy } from '@nats-io/jetstream';

ordered: {
consumer: { deliver_policy: DeliverPolicy.New }, // only new messages
stream: { max_age: toNanos(12, 'hours') },
}

See Ordered Events for detailed usage.

connectionOptions​

connectionOptions passes raw ConnectionOptions from @nats-io/transport-node to the NATS client, for TLS, authentication and reconnection.

Precedence

The top-level name and servers win over the same keys in connectionOptions. Do not set them there.

TLS​

The tls block goes straight to @nats-io/transport-node, so every TlsOptions field works: file paths (certFile, keyFile, caFile) or inline PEM (cert, key, ca).

JetstreamModule.forRoot({
name: 'orders',
servers: ['nats://nats.prod.internal:4222'],
connectionOptions: {
tls: {
// mTLS with client cert + private key, plus a self-signed CA
certFile: '/certs/client.crt',
keyFile: '/certs/client.key',
caFile: '/certs/ca.crt',
},
},
})

For server-only TLS against a broker whose CA your system already trusts, tls: {} is enough: the client upgrades the connection without sending a client identity.

Authentication​

// Token authentication
connectionOptions: {
token: process.env.NATS_TOKEN,
}

// User/password authentication
connectionOptions: {
user: process.env.NATS_USER,
pass: process.env.NATS_PASS,
}

Reconnection​

connectionOptions: {
maxReconnectAttempts: -1, // unlimited reconnection attempts
reconnectTimeWait: 2_000, // 2s between reconnection attempts
reconnectJitter: 500, // add up to 500ms random jitter
}

Publisher-only mode​

publisherOnly: true skips all consumer infrastructure, for API gateways and other services that only publish:

JetstreamModule.forRoot({
name: 'api-gateway',
servers: ['nats://localhost:4222'],
publisherOnly: true, // no streams, consumers, or message routing
})

The JetstreamStrategy provider then resolves to null: do not call app.connectMicroservice() or app.get(JetstreamStrategy) in main.ts.

What's next?​