---
title: "MongoDB Atlas | FoxAuth"
description: "The connection string, where the database name goes, network access, and what provisioning creates."
canonical: "https://foxauth.dev/docs/deploy/mongodb-atlas/"
lastmod: "2026-10-07T23:49:28+03:00"
---
MongoDB Atlas

Everything this server cannot lose lives in MongoDB: end-user accounts, clients, projects, buckets,
sessions, the append-only audit trail, and the signing keys. A managed instance is probably the
shortest path to having that backed up.

Create a cluster in Atlas, in the region closest to where the server runs.

Create a database user with read/write access, and copy the connection string Atlas offers
for it. It looks like:

mongodb+srv://user:password@cluster.example.mongodb.net/?retryWrites=true&w=majority

Allow the application’s egress addresses under Network Access. See the note below.

Set the two variables — the connection string as MONGODB_URI and the database name
separately as DATABASE_NAME:

Terminal window

fly secrets set MONGODB_URI='mongodb+srv://user:password@cluster.example.mongodb.net/?retryWrites=true&w=majority'

DATABASE_NAME=OAuth

Provision the schema against the cluster, once, before the server serves traffic:

Terminal window

MONGODB_URI='mongodb+srv://…' DATABASE_NAME=OAuth ISSUER=https://auth.example.com bun run db:setup

On an upgrade, follow it with bun run db:migrate: the server refuses to start against a
database whose migrations have not run. On Fly both are the release_command and you do not run
them by hand; on Kubernetes they are the provisioning Job; under Compose they are the setup
service.

The database name is not in the URI path

DATABASE_NAME is a separate variable, and it is the one the server uses. A database named in the
connection string’s path is not what gets read, so mongodb+srv://…/mydb with DATABASE_NAME=OAuth
writes to OAuth, which is a confusing way to discover the rule. Leave the path empty and set the
name in DATABASE_NAME.

Network accessSection titled “Network access”

Atlas denies every address by default. Add the addresses the server’s egress actually uses:

Fly.io: Machines egress from shared addresses that are not stable per app. Either use Atlas
Private Endpoints / peering, or allow the outbound addresses Fly documents for your region. If you
resort to 0.0.0.0/0, understand that the database user’s password is then all that protects the
data, so it must be long, unique, and rotated when anyone with a copy leaves.

Kubernetes: allow the cluster’s NAT gateway addresses.

A single VM: allow its public address.

Prefer a private connection over an allow-list wherever the platform offers one.

What provisioning createsSection titled “What provisioning creates”

Nothing but bun run db:setup should be creating structure in this database:

one collection per storage area the server writes to;

the TTL indexes that expire tokens, authorization codes, sessions, interactions and
verification challenges. Atlas runs the TTL monitor, so expiry is the database’s job once the
index exists. A deployment whose indexes were never created accumulates dead documents forever;

the unique e-mail index on each bucket’s end-user collection;

the initial RS256 signing key;

the admin panel seed (reserved project, “Administrators” bucket, the admin-panel and
admin-mcp clients, the system group, the default redfox bucket).

It also drops stale expiry indexes on collections that no longer write the field they indexed. It is
idempotent, and it exits non-zero when a constraint it declares is not in force, for example a
unique index it could not build because the existing data violates it. Treat that exit code as a
failed deploy.

BackupsSection titled “Backups”

Atlas takes scheduled snapshots. Enable them, set a retention period, and then do the part people
usually skip: restore one into a throwaway cluster once, so you know the restore works and how long
it takes.

Two things worth knowing about a restore. The signing keys come back with the snapshot, so
tokens issued before it still verify; restoring a database without them would invalidate every
live token and every session. And the audit trail is append-only by design, with no update or
delete exposed anywhere in the API. A restore that rolls it back is the only way an entry
disappears, which is probably worth stating in whatever your compliance story is.

PreviousBehind a reverse proxyNextPostgreSQL
