db-mongoose
| Package | @theoven/db-mongoose |
| Adds to context | ctx.db — a Mongoose Connection |
| Endpoints | none |
| Creates files | none |
| Creates tables | only the collections your models declare |
| Status | shipped |
Install
Section titled “Install”bun add @theoven/db @theoven/db-mongoose mongooseimport { createApp, env } from '@theoven/core'import { db } from '@theoven/db'import { mongooseDb } from '@theoven/db-mongoose'
export const app = createApp().use(db(mongooseDb({ url: env.string('MONGO_URL') })))import { userSchema } from '../../models'
export default async ({ db }) => db.model('User', userSchema).find()What it does
Section titled “What it does”Connects Mongoose to MongoDB, verifies it at boot with a real ping, and closes it on shutdown.
ctx.db is a Mongoose Connection — models come off it, and every Mongoose feature is reachable
because nothing is wrapped (D16).
It is also the adapter that proves the database contract is a contract; see What this adapter is for.
Endpoints
Section titled “Endpoints”None by default. The db brick mounts one health endpoint if you ask for it:
db(mongooseDb({ url: env.string('MONGO_URL') }), { healthPath: '/healthz' })| Method | Path | Purpose | Auth |
|---|---|---|---|
GET |
whatever you pass as healthPath |
pings the server; 200 healthy, 503 not |
none — guard it yourself |
A Connection, not the global singleton
Section titled “A Connection, not the global singleton”ctx.db is a Mongoose Connection, created
with createConnection — not the mongoose default export everyone reaches for first.
Models come off it: ctx.db.model('User', schema). The difference matters in three places. Two
Oven apps in one process do not fight over global state. A test suite gets its own connection
instead of sharing one with whatever ran before it. And app.close() genuinely closes this
app’s connection rather than a global one something else may still be using.
Configuration
Section titled “Configuration”| Option | Default | Purpose |
|---|---|---|
url |
— | required; mongodb://… |
connection |
— | passed to Mongoose unchanged |
Everything Mongoose can be told about pooling, timeouts and TLS goes in connection. Restating
those options here would mean falling behind Mongoose’s own within a release.
mongooseDb({ url: env.string('MONGO_URL'), connection: { maxPoolSize: 20, serverSelectionTimeoutMS: 5000 },})Define schemas once and compile models off ctx.db:
import { Schema } from 'mongoose'
export const userSchema = new Schema({ email: { type: String, required: true, unique: true }, name: String, createdAt: { type: Date, default: Date.now },})import { z } from 'zod'import { userSchema } from '../../models'
export const body = z.object({ email: z.email(), name: z.string() })
export default async ({ db, body }) => db.model('User', userSchema).create(body)import { z } from 'zod'import { userSchema } from '../../models'
export const params = z.object({ id: z.string() })
export default async ({ db, params }) => { const user = await db.model('User', userSchema).findById(params.id).lean() if (!user) throw new NotFound(`No user ${params.id}.`) return user}db.model() is idempotent for a given name and connection, so calling it per request is cheap —
Mongoose returns the already-compiled model.
Aggregations, populate, change streams, bulkWrite, indexes: all Mongoose’s, all available,
none of them wrapped.
What it creates
Section titled “What it creates”Files: none.
Collections: only what your models declare, created by Mongo on first write. This brick migrates nothing.
Health checks
Section titled “Health checks”The db brick’s health path issues a real ping to the server. readyState
would be cheaper and would report healthy for a connection whose server had gone away — a health
check that cannot fail is not a health check.
Transactions
Section titled “Transactions”This is the one place the adapter does not fit the shared contract, and it is worth explaining rather than hiding.
// This throws for Mongoose, naming the provider.await transaction(ctx.db, async (tx) => { /* … */ })
// This is the form to use.await ctx.db.transaction(async (session) => { await Order.create([order], { session }) await Inventory.updateOne(query, update, { session })})Every SQL provider scopes a transaction to a client: Drizzle hands you a tx and every query
through it is inside the transaction. Mongoose scopes a transaction to a session, attached
to each query individually — there is no session-scoped Connection to hand you.
We could fake one by proxying the connection and every model it returns. It would work for
find and save, and quietly not work for aggregate, bulkWrite, watch and whatever else
you reached for. A transaction that silently covers some of your writes is worse than none, so
the provider declares the truth and transaction() refuses.
The same applies to transactional() — it fails
loudly on a Mongo app rather than pretending.
What this adapter is for
Section titled “What this adapter is for”Beyond running Mongo: it is the second, structurally different implementation that tests whether the database contract is real. One adapter proves nothing — a contract shaped around a single ORM will fit that ORM perfectly and nothing else.
Three of the four contract methods fit Mongoose unchanged. The fourth did not, and the contract
did not bend to accommodate it: transaction is optional, and a provider that lacks it says so.
That is the outcome the split was designed for.
Capabilities
Section titled “Capabilities”connect, health, close |
✓ |
transaction |
✗ — declared absent; transaction() and transactional() refuse loudly |
| Pooling, TLS, timeouts | ✓ via connection, passed to Mongoose unchanged |
| Migrations | ✗ — no oven db support; schema lives in your models |
What fails at boot: a missing url is refused at construction, and an unreachable server fails
during boot rather than on the first query.
How it is verified
Section titled “How it is verified”The suite drives a real MongoDB: connect, a typed ctx.db that is genuinely a Mongoose
Connection, models compiled off it, the health endpoint answering from a live ping, and a clean
close on shutdown.
The most useful assertions are the negative ones — that transaction() throws for this
provider and names it, and that transactional() fails loudly on a Mongo app instead of wrapping
nothing. A contract violation that stays silent is the failure mode this whole split exists to
avoid.
Limitations
Section titled “Limitations”- No portable transactions — see above.
- Mongoose 8, not 9. Mongoose 9 pulls
bson@7, which calls anode:v8API Bun has not implemented (isBuildingSnapshot); importing it fails outright. Still missing as of Bun 1.4, so the peer range stays^8.0.0until it lands. - No migration commands.
oven dbcovers Drizzle and Prisma; Mongo’s schema lives in your models, and there is no equivalent worth wrapping. - Integration tests run in CI, not on your laptop, against a real
mongo:8service. Locally they skip unlessMONGO_URLis set, and say so rather than passing silently.