Skip to content

Bring your own Mongo

A project can be created against your own MongoDB connection string. Groveback then layers its features — the policy-gated data API, auth, functions, realtime, vector search, the dashboard — on top of data you already have, without taking ownership of it.

The split is deliberate: your cluster holds only your data collections, and everything Groveback needs to run the project stays in the platform’s own database.

In your database In the platform’s database
Your data collections, and any created later through the API users, sessions, verification_tokens
Indexes and search indexes on them __schemas, __policies, __roles, __enduser_roles
Change streams for realtime __api_keys, __audit, __usage, __vectorconfigs
Vectors, stored next to the documents __functions, __function_logs, __function_versions
The ctx.db bridge target __integrations, __webhooks, __config, buckets and blobs

Your database stays pure — you keep using it with your own tooling, backups and other apps, and there is nothing of Groveback’s to garbage-collect if you leave. Write-heavy operational data (audit, usage, function logs) never touches your cluster.

The decrypted connection string never leaves the connection pool: it is never attached to a project reference or handed to any service.

Terminal window
curl -X POST "$URL/api/v1/control/projects" \
-H "authorization: Bearer $CONTROL_TOKEN" -H 'content-type: application/json' \
-d '{"name":"acme","mongoUrl":"mongodb+srv://user:pass@cluster0.x.mongodb.net/mydb"}'

Rules:

  • Scheme must be mongodb:// or mongodb+srv://.
  • A database name in the URL path is required — no silent default to test.
  • URLs targeting a groveback_* database are rejected.
  • In cloud mode (GROVEBACK_CLOUD=true), private and loopback hosts are rejected (SSRF) and TLS is required. Self-hosting skips both, since localhost clusters are the normal case.

The URL is encrypted at rest and never returned. Reads show only:

{ "byoMongo": { "host": "cluster0.x.mongodb.net", "db": "mydb",
"capabilities": { "realtime": true, "preImages": false } } }

The create probes connectionStatus { showPrivileges: true } against the target database and checks what the authenticated user can actually do.

Required — readWrite on the target database: find, insert, update, remove, createCollection, createIndex. Missing any of these fails the create, with a message naming the missing actions and the fix.

Optional — dbAdmin on the target database: collMod, needed to enable change-stream pre-images, which is what makes realtime delete events reach filtered subscribers. Missing it never fails the create: the project stores preImages: false, the response carries a warnings entry, and the factory skips pre-image enablement with a single log line.

Resource matching is conservative. A privilege counts database-wide only via anyResource, a whole-database grant, or an any-database grant — a grant scoped to one named collection does not, because it cannot cover collections created later.

An inconclusive probe (auth disabled, or the command failing) blocks nothing, so self-hosting without auth keeps working.

At create, Groveback probes and stores what your cluster can do:

Capability Determined by If absent
realtime hello.setName — change streams need a replica set. Realtime degrades with a warning, never an error.
preImages The collMod privilege. Realtime delete events are dropped for filtered subscribers.

Vector search is not probed at create; it keeps the runtime VECTOR_SEARCH_UNAVAILABLE path.

BYO projects get an introspection API — the wizard for mapping what you already have.

Terminal window
curl "$URL/api/v1/admin/introspect" -H "authorization: Bearer $ADMIN_KEY"
curl "$URL/api/v1/admin/introspect/orders" -H "authorization: Bearer $ADMIN_KEY"

Introspection is read-only. It uses only listCollections, estimatedDocumentCount, indexes and $sample. Registering a collection writes nothing to your cluster — the schema and policy live in Groveback’s own database.

A report looks like:

{ "collection": "orders", "count": 120400, "sampled": 100,
"indexes": [ ], "schema": { },
"importable": true, "blockers": [], "emptySchema": false }
Blocker Meaning
reserved-name The name is reserved. BYO projects only reserve the __* and system.* prefixes, so a collection literally named users is fine.
id-conflict An existing id field that is non-string or has duplicate values. The data API needs a unique public string id.
unsupported-type A field whose only observed types are outside the schema subset.
  • Root is always type: "object"; required is the set of fields present in ≥ 99% of samples.
  • _id is always dropped. __* fields — ODM artifacts like Mongoose’s __v — are dropped at every level and never counted as blockers.
  • ObjectId → { type: "string", format: "objectId" }; Date → format: "date-time"; Decimal128 → format: "decimal".
  • Numbers become integer or number by sample; a field seen as both collapses to number.
  • Nested objects recurse; arrays become { type: "array", items } with element types unioned.
  • Low-cardinality string fields (2–12 distinct sampled values) are suggested as an enum.
  • Mixed or ambiguous types widen to string with a note, rather than failing.

The result is always editable before import, and always stays inside the validator’s supported subset, so it round-trips through the dashboard’s schema builder unchanged.

Terminal window
curl -X POST "$URL/api/v1/admin/introspect/orders/import" \
-H "authorization: Bearer $ADMIN_KEY" -H 'content-type: application/json' \
-d '{"schema": { … }}'

Importing grants nobody access. An explicit, visible deny-all policy is written — read and list rules with an unsatisfiable filter ({ "$expr": false }) and no create, update or delete rules at all. Even a permission-holder matches nothing. You open it deliberately afterwards in the policy editor.

An empty collection imports schema-less (emptySchema: true) rather than failing — it stays fully usable through the generic document API, and the project’s typed GraphQL schema is not broken by it.

The one mutation this feature makes to your cluster:

Terminal window
curl -X POST "$URL/api/v1/admin/introspect/orders/backfill" \
-H "authorization: Bearer $ADMIN_KEY"

It re-checks the id-conflict blocker, sets id from _id on documents missing it, creates a unique index on id, and returns { matched, modified }.

This runs as a single updateMany. Very large collections may want batching — plan a maintenance window.

BYO projects reserve only the __* and system.* prefixes, dropping the named system list. So an existing collection called users or projects can be mapped, which is normally the whole reason the relaxation exists.

An environment is an ordinary project with a parent, so the same rules apply per environment:

  • mongoUrl present → the environment’s data lives in your infrastructure. A separate staging cluster per environment is fine.
  • mongoUrl absent → the environment’s data lives in the platform’s database, even when the root project is BYO.

A parent’s URL is never inherited. Pointing two projects at the same external database is an explicit choice you make per create. Mixed families — production on your cluster, throwaway environments on the platform’s — are expected, not an edge case.

BYO clients are LRU-capped (GROVEBACK_BYO_MAX_CLIENTS, default 50) and released when a project changes. Realtime and functions cross clusters — change streams watch yours, everything else reads the platform’s — which is the accepted cost of keeping your database clean.