Browse all documentation

MongoDB

Limited

Create or configure a Ridu project for the supported MongoDB replica-set deployment.

Choose MongoDB when you can run the supported replica-set topology below. Collections, access rules, REST, the generated SDK, and the admin work the same way as they do with Ridu’s other database adapters; application code does not use MongoDB filters or driver types.

Production support is limited to:

  • an ordinary Ridu-generated starter or blank project;
  • Linux x86-64;
  • MongoDB Community 8.2.9, with release evidence built from digest-pinned images;
  • SCRAM-SHA-256 authentication and TLS with CA and hostname verification; and
  • a writable three-member replica set.

This does not include Atlas, Amazon DocumentDB, Azure Cosmos DB, a standalone mongod, another MongoDB version or topology, another operating system or architecture, arbitrary scale, network-partition/failover matrices, or point-in-time recovery. See Releases and compatibility before choosing this profile.

Create a project

npm create ridu@latest my-app

Choose Starter and MongoDB in the wizard, then run the exact install and dev commands it prints. Review the summary carefully: MongoDB is not the default database choice.

Preselect every scaffold choice

For a non-interactive npm setup, pass every choice as an argument:

terminal
npm create ridu@latest -- \
  --template starter \
  --database mongodb \
  --module github.com/acme/content \
  --scope @acme \
  --package-manager npm \
  --agent codex \
  content

Both generated templates include the same adapter wiring. The dev script starts the pinned local single-node replica set from compose.yaml and uses:

text
mongodb://127.0.0.1:27029/ridu?directConnection=true&replicaSet=ridu-rs0

This convenient plaintext, unauthenticated service is for local development only. It is still a replica set because Ridu operations require MongoDB transactions; a standalone mongod is rejected. To use another development replica set without putting its URL in process arguments, supply a command-scoped environment value from your shell’s secret binding:

DATABASE_URL="$MONGODB_DEVELOPMENT_URL" npm run dev -- --no-docker

Configuration discovery, generate, generate --check, and migrate create through the project-local CLI remain offline. They do not open MongoDB or serialize its URL into config, generated contracts, or migration artifacts.

Add MongoDB to an existing project

Use this path for a clean generated Ridu project before it has committed migration history or live data. Ridu has no generic PostgreSQL/SQLite-to-MongoDB data migration facility. Moving a live dataset is application-owned: export and transform every current document, localized value, version, auth/session record, relationship, task, preference, lock, and upload reference; preserve stable IDs; validate the destination; and rehearse backup, cutover, and rollback.

1. Select MongoDB in project metadata

Replace the existing adapter value. This example starts from PostgreSQL:

ridu.toml
version = 1
database = "postgres"
database = "mongodb"
entry = "./cmd/server"
admin = "./admin"

The value must match the CLI’s public adapter name. It controls development orchestration and migration routing; it does not contain a connection URL.

2. Use the generated adapter handshake

MongoDB development needs more than replacing one Open call. The internal candidate started by ridu dev must verify the resolved manifest’s exact indexes before the stable proxy promotes it. Copy the following generated pattern into cmd/server/main.go (retaining the project’s admin asset and handler options):

cmd/server/main.go
import (
  "context"
  "fmt"
  "log"
  "os"

  "github.com/acme/content/content"
  "github.com/acme/content/internal/adminassets"
  "github.com/riducms/ridu"
  "github.com/riducms/ridu/adapters/postgres"
  "github.com/riducms/ridu/adapters/mongodb"
  "github.com/riducms/ridu/store"
)

const internalDevelopmentServerArgument = "--ridu-internal-development-server"

func main() {
  applicationConfig := content.Config()
  internalDevelopmentServer := len(os.Args) == 2 && os.Args[1] == internalDevelopmentServerArgument
  if internalDevelopmentServer {
    os.Args = os.Args[:1]
  }

  // The migration driver lets the built binary apply its own migrations with
  // `migrate up`, so production needs no Ridu CLI. Register data transforms here.
  options := []ridu.ExecuteOption{
    ridu.WithProjectMigrations(postgres.ProjectMigrations()),
    ridu.WithProjectMigrations(mongodb.ProjectMigrations()),
  }
  if len(os.Args) == 1 {
    options = append(options, runtimeOptions(applicationConfig)...)
    options = append(options, runtimeOptions(applicationConfig, internalDevelopmentServer)...)
  }
  if err := ridu.Execute(applicationConfig, options...); err != nil {
    log.Fatal(err)
  }
}

func runtimeOptions(applicationConfig ridu.Config) []ridu.ExecuteOption {
func runtimeOptions(applicationConfig ridu.Config, internalDevelopmentServer bool) []ridu.ExecuteOption {
  return []ridu.ExecuteOption{
    ridu.WithStore(func(ctx context.Context) (store.Store, error) {
      return postgres.OpenWithConfig(ctx, postgres.PoolConfig{
        DatabaseURL:              os.Getenv("DATABASE_URL"),
        AllowInsecureTransport:   envBool("RIDU_ALLOW_INSECURE_DATABASE"),
        MaxUploadLockConnections: envInt32("RIDU_POSTGRES_UPLOAD_LOCK_CONNECTIONS"),
      })
      backend, err := mongodb.OpenWithConfig(ctx, mongodb.Config{
        DatabaseURL:            os.Getenv("DATABASE_URL"),
        AllowInsecureTransport: envBool("RIDU_ALLOW_INSECURE_DATABASE"),
        ApplicationName:        "ridu-server",
      })
      if err != nil {
        return nil, err
      }
      if internalDevelopmentServer {
        manifest, err := ridu.Resolve(applicationConfig)
        if err != nil {
          _ = backend.Close()
          return nil, fmt.Errorf("resolve MongoDB development manifest: %w", err)
        }
        if err := backend.VerifyIndexes(ctx, manifest); err != nil {
          _ = backend.Close()
          return nil, fmt.Errorf("verify MongoDB development indexes: %w", err)
        }
      }
      return backend, nil
    }),
    ridu.WithAddress(env("RIDU_ADDRESS", ":8080")),
    ridu.WithHandlerOptions(ridu.HandlerOptions{
      AdminAssets:             adminassets.FS(),
      AllowedOrigins:          envList("RIDU_ALLOWED_ORIGINS"),
      AllowedHosts:            envList("RIDU_ALLOWED_HOSTS"),
      TrustedProxyCIDRs:       envList("RIDU_TRUSTED_PROXY_CIDRS"),
      TrustedProxyHops:        envInt("RIDU_TRUSTED_PROXY_HOPS"),
      ReadinessTimeout:        envDuration("RIDU_READINESS_TIMEOUT"),
      StrictTransportSecurity: os.Getenv("RIDU_STRICT_TRANSPORT_SECURITY"),
    }),
    ridu.WithServerOptions(ridu.ServerOptions{
      ShutdownTimeout:            envDuration("RIDU_SHUTDOWN_TIMEOUT"),
      WorkerDrainTimeout:         envDuration("RIDU_WORKER_DRAIN_TIMEOUT"),
      ReadinessDrainDelay:        envDuration("RIDU_READINESS_DRAIN_DELAY"),
      AllowUnverifiableReadiness: envBool("RIDU_ALLOW_UNVERIFIABLE_READINESS"),
      SkipReadinessPreflight:     envBool("RIDU_SKIP_READINESS_PREFLIGHT"),
      AllowUnverifiableReadiness: internalDevelopmentServer,
      SkipReadinessPreflight:     internalDevelopmentServer,
    }),
  }
}

The red PostgreSQL lines are context from the existing generated server. Remove them after adding the MongoDB lines; the finished file has one adapter import, one runtimeOptions call and declaration, one store factory, and one value for each readiness option.

The readiness exceptions are only for the CLI’s internal development candidate, after the explicit index check. Do not expose them as production environment fallbacks. Keep the template’s envBool helper so invalid environment values fail clearly, and close the backend on every resolution/verification error as shown.

3. Add the local replica-set service

Copy this service into compose.yaml:

compose.yaml
services:
  mongodb:
    image: mongo:8.2.9-noble@sha256:007773db61cb1aa44e526fb7175fc582902e67d4e6cc5f13106445767d46c818
    command: ['mongod', '--replSet', 'ridu-rs0', '--bind_ip_all']
    ports:
      - '127.0.0.1:27029:27017'
    healthcheck:
      test:
        - CMD-SHELL
        - >-
          mongosh --quiet --eval '
          try {
            const h = db.hello();
            if (h.setName === "ridu-rs0" && h.isWritablePrimary) {
              quit(0);
            }
            if (!h.setName) {
              try {
                rs.initiate({
                  _id: "ridu-rs0",
                  members: [{ _id: 0, host: "mongodb:27017" }]
                });
              } catch (_) {}
            }
          } catch (_) {}
          quit(1);'
      interval: 2s
      timeout: 5s
      retries: 30
      start_period: 5s

The health check initializes ridu-rs0 and succeeds only when the node is its writable primary. Ridu requires sessions and transactions, so a standalone MongoDB server is not sufficient. The local URL is:

text
mongodb://127.0.0.1:27029/ridu?directConnection=true&replicaSet=ridu-rs0

This single-node, unauthenticated, plaintext service is development-only and is not the three-member authenticated TLS production profile.

4. Start and verify development

go mod tidy
npm run dev

ridu dev starts the generated replica-set service, regenerates contracts, synchronizes safe additive changes, and starts the API and admin. New projects already include their initial migration. Before deployment, verify it and inspect the selected database:

terminal
export DATABASE_URL='mongodb://127.0.0.1:27029/ridu?directConnection=true&replicaSet=ridu-rs0'
ridu migrate verify
ridu migrate plan
ridu migrate up
ridu migrate status

For a separately managed development replica set, omit the Compose service and rerun the selected tab’s dev --no-docker command with DATABASE_URL="$MONGODB_DEVELOPMENT_URL". Keep TLS and authentication enabled unless the target is a disposable local development database. Before production, replace the local URL and insecure admission with the authenticated, CA- and hostname-verified three-member profile below and follow the full release cutover.

Development schema changes

During ridu dev, an additive manifest change creates only missing collections and indexes. A separate serving Store then non-mutatingly verifies the exact physical index plan and passes development readiness before the stable proxy promotes the candidate. A failed build, incompatible index, or unhealthy candidate leaves the last working process in place.

Development synchronization never drops state or guesses how stored content should move. For a compiled transform, index replacement, or reviewed retirement, create and apply an immutable production migration instead of repairing MongoDB by hand.

MongoDB stores what a save accepted: drafts may stay incomplete, and a document saved before a field became required stays readable. A synchronization or migration that makes a field required reads the same documents as on PostgreSQL and SQLite and stops with RIDU_REQUIRED_VALUES_MISSING while any lacks the value. The migration needs --allow-maintenance, because MongoDB cannot lock collections against old writers.

A change that looks like a rename is the exception in an interactive terminal: ridu dev asks whether to preserve the data. Answer y and it writes the rename migration, records the migrations the database already has, stops the running server, applies the rename with maintenance admitted, and starts the replacement. See Renames in ridu dev. Without a terminal it rejects every reload until you restore the old name or create and apply the migration yourself.

--no-sync skips additive mutation, but it does not weaken verification: the candidate starts only when another owner has already prepared the selected database.

Concurrency and server limits

MongoDB has no row locks, so Ridu builds them from writes, which MongoDB lets only one open transaction make to a document. Where PostgreSQL share-locks a relationship target so it cannot change or be deleted while a save that references it commits, Ridu increments a small shared fence record of the target in the z_ridu_reference_fences collection. Each open save of a Ridu process uses its own record, so saves that share a target, such as one image used by many pages, proceed together. Updating or deleting a document increments a fence on the document itself and each of its fence records, which conflicts with every save that holds one: the update or delete waits for those saves to commit and then sees their references, so restrict and nullify apply to them. A document has at most 32 fence records. They stay for later saves and are removed when the document is deleted.

Until a transaction writes content, Ridu absorbs a lock conflict. It waits, restarts the transaction on a newer snapshot, locks again every document it had locked, and continues when each is unchanged. The first save to use one of a document’s fence records also fences the document itself, so concurrent first saves take turns once. Replicas coordinate through the same records; two replicas, or more concurrent saves in one process than records, can share one and then take turns on it. A save still fails with a 409 conflict when a document it locked changed meanwhile, when the conflict follows its first content write, or when 10 seconds or its request deadline pass first.

Within one Ridu process, lock requests also wait in order, as they do on PostgreSQL. An update or delete that waits for a document holds back the references of saves that began after it, so it waits only for saves already under way, however many keep arriving. A request that waits for another request of the same process resumes as soon as that one ends, and a later update of a document a waiting save already locked waits for that save. The order does not span processes: a conflict with another replica is retried after a short randomized pause. An update of a document that saves on other replicas reference without pause can therefore still wait for up to 10 seconds and fail.

Every database statement of an operation carries a server time limit: its caller’s deadline, and at most 60 seconds, MongoDB’s default transaction lifetime. MongoDB stops a query whose caller gave up instead of finishing it.

List totals, filtered bulk selections, and distinct values count only documents whose stored root matches the schema: every top-level and group field’s type and, for array and blocks fields, the row count, row keys, and block types. The content of rows, including nested blocks, is validated when a document is read. A filter through arrays or blocks also checks the rows on its path against their fields and row keys, so a negated filter cannot count a malformed row as a non-match. The check runs for every row the filter traverses in a matching document, so a filter that matches many documents through several block levels costs noticeably more than one through a single level.

The database’s schema record stores the manifest compressed. The manifest stores each block once, however often it is used, so even block-heavy schemas are a small fraction of MongoDB’s 16 MiB document limit. A schema that still exceeds the limit is rejected before anything is written.

Production connection and credentials

The production DATABASE_URL must select the application database, authenticate with SCRAM-SHA-256, name the replica set, and enable certificate and hostname verification. For example:

text
mongodb://ridu-app:<secret>@mongo-1.example.internal,mongo-2.example.internal,mongo-3.example.internal/content?authSource=content&authMechanism=SCRAM-SHA-256&replicaSet=ridu-rs0&tls=true&tlsCAFile=/run/secrets/mongodb-ca.pem

Keep the running application credential scoped to its database. Use separate, short-lived credentials for verification and backups:

Credential Scope
Application Database-scoped access to the Ridu database. Use it for the running app and, when sufficient, plan, up, and status.
Verification Permission to create and drop the temporary database used by migrate verify.
Backup The database-scoped permissions required by mongodump and mongorestore.

Supply each credential only to its command. Ridu redacts credentials and topology details from normal connection failures, but operators must still keep URLs out of shell history, process arguments, logs, generated files, and image layers. A mode-0600 Database Tools configuration file is one way to keep backup credentials out of process arguments.

A database whose every seed host is localhost or a loopback address needs no TLS. An SRV URL never counts as local. RIDU_ALLOW_INSECURE_DATABASE and --allow-insecure-database admit plaintext to another development host only. They do not expand the production support profile.

Immutable migration lifecycle

New artifacts created by ridu migrate create use MongoDB planner contract 5.0.0 inside shared artifact-envelope format 1. Create and inspect the plan before the cutover:

terminal
ridu migrate create --name add-post-summary
ridu migrate plan --json

Ridu accepts only planner 5.0.0 artifacts, binds them to their manifest history, and rejects altered or reordered migration files. Its initial layout includes independent live heads and cross-head unique reservations. Earlier framework layouts and planner histories are unsupported; recreate the database and migration history rather than rewriting old artifacts. Startup and readiness never convert an old layout. The active live head does not depend on retained versions.

Migrations that add or change a unique index on versioned content require maintenance admission: an explicit resumable step recalculates reservations across working and published heads before the new index is built. Typed field renames recalculate those reservations inside the semantic transaction.

up takes a fenced lease with a bounded wait, records durable step progress, and resumes the same immutable history after an interruption. It requires explicit maintenance admission only when the pending or incomplete history suffix contains a rename, transform, unique-reservation rebuild, reference-index rebuild, or resource retirement. Run that work only after every old application process and worker is drained.

verify creates a random isolated database, replays the complete history with the adapter’s migration runner, checks the final ledger and index state, and drops that database. No application can reach that database, so verify admits semantic work without --allow-maintenance. status is non-mutating. ridu build embeds a fingerprint of the exact ordered migration filenames, artifact digests, recorded head, and storage schema, which omits presentation settings. Application readiness requires the live ledger and executable schema to match that fingerprint, and every required Ridu index to pass non-mutating verification. Presentation changes do not require another migration; other schema changes and a mismatched recorded head still fail readiness. ridu check and ridu build remain offline and fail when committed history does not end at executable config. A direct go build has no fingerprint and fails closed in ordinary production startup. Deployment cutover additionally fails when the applied ledger or physical indexes do not exactly match the release history.

MongoDB does not expose down, reset, refresh, or fresh. Correct forward with another reviewed artifact, or restore the complete matched recovery point.

Deploy and recover

For a code-only replacement with the same manifest digest and migration-history fingerprint, start the candidate, require /readyz, then drain the old process. For any new migration artifact—even an additive or same-manifest data-only one—use the coordinated sequence:

  1. Rehearse the exact binary and history against a restored recovery point.
  2. Drain every old application process and worker.
  3. Run migrate verify through the project-local CLI with DATABASE_URL="$MONGODB_OPERATIONAL_URL" so the shadow-database authority exists only for that command. The shadow database has no traffic, so verification needs no --allow-maintenance.
  4. After verification succeeds, capture one recovery point containing a database-scoped mongodump and the upload store. Use a separately scoped $MONGODB_BACKUP_URL.
  5. Run migrate up through the project-local CLI with DATABASE_URL="$MONGODB_MIGRATION_URL", using the selected database-scoped application or controlled operator identity. Append --allow-maintenance only when the pending or incomplete history suffix contains semantic work.
  6. Run migrate status with the same migration identity and require exact complete history and Ridu-managed index state.
  7. Start only the target release binary carrying the expected manifest and migration-history fingerprints with $MONGODB_APP_URL, then wait for /readyz before admitting traffic.

Take the cutover recovery point only after draining writers and successfully completing the command-scoped verification, but before up. The recovery drill uses database-scoped mongodump --archive --gzip --dumpDbUsersAndRoles, restores that archive into an empty target with mongorestore --archive --gzip --drop --restoreDbUsersAndRoles, and restores upload storage from the same named point. The dump/restore must remain scoped to the selected application database; never restore over unrelated databases. Confirm the ledger and status, the restored scoped user, auth and sessions, content and versions, relationships, SDK/admin workflows, rich text, upload objects, tasks, and representative data before directing traffic.

This is a logical dump/restore path, not point-in-time recovery. Define and rehearse your retention, encryption, off-site storage, recovery-point objective, and recovery-time objective.

Test the deployment

Before production, rehearse migrations, startup, readiness, a primary election, graceful shutdown, and database/upload restoration on the topology you will deploy. Exercise authentication, content, versions, relationships, uploads, tasks, the SDK, and the admin after restoration. Use disposable databases for these tests; never point them at production.