@dunx/infra

Database, Redis, queue, AMQP, schedule, storage, image and logging infrastructure for dunx. drizzle over bun:sqlite and Bun.SQL, bullmq over Bun.RedisClient, rabbitmq-client for RabbitMQ, plus Bun.file, Bun.Glob, Bun.S3Client, Bun.Image and @arkv/logger

@dunx/infra
@dunx/infra/amqp
@dunx/infra/cache
@dunx/infra/db
@dunx/infra/email
@dunx/infra/email/react
@dunx/infra/email/resend
@dunx/infra/email/smtp
@dunx/infra/files
@dunx/infra/images
@dunx/infra/logger
@dunx/infra/pagination
@dunx/infra/pagination/cursor
@dunx/infra/queue
@dunx/infra/redis
@dunx/infra/schedule

Infrastructure for dunx: databases, Redis/Valkey, caching, queues, message brokers, file storage, images, scheduling, email, pagination and logging. Eleven areas, one package.

Where Bun ships the primitive, the primitive is what runs: Bun.SQL, bun:sqlite, Bun.RedisClient, Bun.file, Bun.Glob, Bun.S3Client, Bun.Image, Bun.cron. No pg, no better-sqlite3, no ioredis, no @aws-sdk, no glob, no sharp.

Five areas integrate a mature library rather than hand-rolling one: /db over drizzle-orm, /queue over bullmq, /amqp over rabbitmq-client, /logger over @arkv/logger, and /email over whichever provider a subpath names.

The first three are optional peer dependencies, so an app using only /files installs none of them. @arkv/logger is first-party and a plain dependency. The first two drive a Bun API underneath; rabbitmq-client speaks a wire protocol Bun ships no client for, over node:net, which Bun implements natively.

The subpaths

The guide is canonical for every row; this table is the index.

SubpathWhat it isGuide
@dunx/infra/dbdrizzle over bun:sqlite and Bun.SQL, transactions, seeds, named and per-tenant data sources, query timingsDatabase
@dunx/infra/redisBun.RedisClient, named connections, pub/subDatabase
@dunx/infra/cacheCache over a memory, Redis or two-tier CacheStore, CacheMetricsCaching
@dunx/infra/queuebullmq over Bun.RedisClient: handlers, publisher, workerQueues
@dunx/infra/amqprabbitmq-client: @AmqpHandler, publisher, drained consumersMessage brokers
@dunx/infra/scheduleBun.cron and timers: @Cron, @Interval, @OnceOnBootScheduling
@dunx/infra/filesOne Storage contract over Bun.file and Bun.S3ClientFiles and images
@dunx/infra/emailOne EmailTransport contract, dry run, pacing, a renderer seam, and bunx dunx-email previewEmail
@dunx/infra/email/resendresendEmail
@dunx/infra/email/smtpnodemailerEmail
@dunx/infra/email/reactReact Email, as one TemplateRenderer among othersEmail
@dunx/infra/imagesAn immutable pipeline over Bun.ImageFiles and images
@dunx/infra/logger@arkv/logger bound to core's Logger contractLogging
@dunx/infra/paginationKeyset pagination: cursor codec, options parser, drizzle queryDatabase
@dunx/infra/pagination/cursorThe same codec and parser without the drizzle query, so it resolves with no drizzle installedDatabase

Usage

TS
import { Module } from '@dunx/core';
import { DbModule, SqliteOptions } from '@dunx/infra/db';
import { RedisModule } from '@dunx/infra/redis';
import * as schema from './schema.js';

@Module({
  imports: [
    DbModule.forRoot(new SqliteOptions({ schema, filename: './app.db' })),
    // No url: Bun resolves $VALKEY_URL, then $REDIS_URL, then localhost.
    RedisModule.forRoot({ maxRetries: 0 }),
  ],
})
export class InfraModule {}

With OtelModule from @dunx/core/otel imported, /db, /redis, /queue and /amqp open OpenTelemetry spans; see Tracing.

Every area also has a forRootAsync, which is forRoot with a factory in front of it. It exists for the one thing a zero-argument function cannot do: inject, so the url or the filename can come off ConfigService.

Two conventions

Anything injectable by a constructor parameter is a runtime class, never an interface, because an interface erases and leaves nothing for @dunx/transform to record.

When dunx defines the contract, the token is an abstract class (Storage, DbConnection, ImagesOptions). Otherwise it is the library's own class (BunSQLiteDatabase, ContextStore). Three exports are token()s instead, because no class fits them: redisConnection(name), redisMetrics(name) and LoggerSettings.

If an area is in the root barrel at all, all of it is. /db, /queue, /amqp and /email are the four the barrel does not re-export: each reaches an optional peer through a static import, so exporting them would make drizzle-orm, ioredis, rabbitmq-client or resend hard requirements of import '@dunx/infra'. Reach them at their subpaths.

Within /email the split goes one level further. Each vendor import sits in the subpath that needs it and nowhere else: resend in /email/resend, nodemailer in /email/smtp, react and @react-email/render in /email/react.

So @dunx/infra/email loads with none of the four installed, and you can use its log and memory transports. The dunx-email bin loads a renderer with import() only when it needs one.

@dunx/infra's tests check which file may import each vendor, that the base subpath imports none of them, and that all four are marked optional.

Timing is off unless asked for. { metrics: true } is the last argument to DbModule, CacheModule, RedisModule and QueueModule, binding a QueryMetrics, CacheMetrics, RedisMetrics or QueueMetrics.

What each one measures:

SubpathTimed
/dbEvery query. dunx wraps the driver it creates, since drizzle's logger option does not report durations
/cacheHits and misses on the configured CacheStore, including one injected directly. A coalesced wrap counts as one read
/redisEvery command
/queuepublish(), and handlers run in this process. A background handler runs in a forked process and is not timed

See Metrics.

Verified against

Bun 1.4.2, drizzle-orm 0.45.2, bullmq 6.0.5, rabbitmq-client 5.0.8, resend 6.28.0, nodemailer 10.0.9 and @react-email/render 2.1.0. Bun's documentation is incomplete across every area here, so the behaviour was measured rather than read. The evidence is in docs/ARCHITECTURE.md and docs/bun-apis.md.