@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
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.
| Subpath | What it is | Guide |
|---|---|---|
@dunx/infra/db | drizzle over bun:sqlite and Bun.SQL, transactions, seeds, named and per-tenant data sources, query timings | Database |
@dunx/infra/redis | Bun.RedisClient, named connections, pub/sub | Database |
@dunx/infra/cache | Cache over a memory, Redis or two-tier CacheStore, CacheMetrics | Caching |
@dunx/infra/queue | bullmq over Bun.RedisClient: handlers, publisher, worker | Queues |
@dunx/infra/amqp | rabbitmq-client: @AmqpHandler, publisher, drained consumers | Message brokers |
@dunx/infra/schedule | Bun.cron and timers: @Cron, @Interval, @OnceOnBoot | Scheduling |
@dunx/infra/files | One Storage contract over Bun.file and Bun.S3Client | Files and images |
@dunx/infra/email | One EmailTransport contract, dry run, pacing, a renderer seam, and bunx dunx-email preview | |
@dunx/infra/email/resend | resend | |
@dunx/infra/email/smtp | nodemailer | |
@dunx/infra/email/react | React Email, as one TemplateRenderer among others | |
@dunx/infra/images | An immutable pipeline over Bun.Image | Files and images |
@dunx/infra/logger | @arkv/logger bound to core's Logger contract | Logging |
@dunx/infra/pagination | Keyset pagination: cursor codec, options parser, drizzle query | Database |
@dunx/infra/pagination/cursor | The same codec and parser without the drizzle query, so it resolves with no drizzle installed | Database |
Usage
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:
| Subpath | Timed |
|---|---|
/db | Every query. dunx wraps the driver it creates, since drizzle's logger option does not report durations |
/cache | Hits and misses on the configured CacheStore, including one injected directly. A coalesced wrap counts as one read |
/redis | Every command |
/queue | publish(), 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.