Skip to content

Collections & schemas

A collection is a MongoDB collection exposed over the data API. It can carry a schema, a policy, indexes and a vector config — all optional except the policy, without which nothing non-admin can reach it.

They produce the same thing, so mix them freely.

Best while you are still deciding on the shape. With grove dev running, open the dashboard link it printed → DatabaseCreate collection:

  1. Define — name and fields, each with a type (Text, Number, Boolean, Object, Array, Relation) and whether it is required.
  2. Access — pick a policy template: Public, Private (per owner), Admin-only writes or Custom. This is the step worth doing here — it writes correct policy rules for you.
  3. Review — the API you are about to get, in REST, GraphQL and SDK form, before anything is created.

Then bring it into the repo:

Terminal window
grove pull --target local

With grove dev --persist, that happens on its own when you stop the server.

Best when you already know the shape, or are editing one that exists.

groveback/
meta.json
collections/
notes.json
{ "formatVersion": 1, "project": { "name": "my-app" } }
{
"name": "notes",
"schema": {
"type": "object",
"required": ["title"],
"properties": {
"title": { "type": "string", "maxLength": 200 },
"done": { "type": "boolean" },
"priority": { "type": "string", "enum": ["low", "high"] }
}
},
"policy": { "admin": {} }
}

Restart grove dev and it applies them.

Validation is opt-in. A collection with no schema accepts free-form documents.

The validator implements a subset of JSON Schema draft 2020-12, and the subset is an allowlist — an unsupported keyword is rejected when you save the schema, not silently ignored at write time. A typo fails loudly, while you are looking at it.

type · properties · required · additionalProperties · items · enum · minLength · maxLength · pattern · minimum · maximum · minItems · maxItems · description · title · format · x-ref

Of these, four are annotation-only and never affect validation:

Keyword What it is for
description Documentation; becomes JSDoc in the generated client.
title Documentation.
format A hint the dashboard uses to pick an editor widget — e.g. "markdown".
x-ref Declares a relation. See below.
{
"type": "object",
"properties": {
"title": { "type": "string", "minLength": 1, "maxLength": 200 },
"body": { "type": "string", "format": "markdown" },
"status": { "type": "string", "enum": ["draft", "published"] },
"views": { "type": "integer", "minimum": 0 },
"authorId": { "type": "string", "x-ref": "authors" },
"tags": { "type": "array", "items": { "type": "string" }, "maxItems": 10 }
},
"required": ["title", "status"],
"additionalProperties": false
}
  • The root schema must be type: "object".
  • required must be an array of strings.
  • pattern must compile as a regular expression.
  • items supports only the single-schema form — tuples are not supported.
  • additionalProperties is honored only in its boolean false form.
  • x-ref is scanned on top-level properties only. Nested and array refs are not resolved.

Validation runs on create, on full replacement (PUT), and per field on $set updates — operator payloads are validated against the matching sub-schema.

A x-ref on a type: "string" field names the target collection:

{ "authorId": { "type": "string", "x-ref": "authors" } }

At write time a ref is just a string. The target collection is deliberately not checked for existence — you may create it later.

Refs are read by three things:

  • REST, via ?expand=
  • GraphQL, which adds a sibling <field>_ref field resolving the target document
  • the OpenAPI spec, and so the generated typed client’s JSDoc

Expansion is policy-gated. A referenced document you may not read resolves to null — it never leaks existence.

Three fields are stamped by the server and stripped from your payload: id, createdAt, updatedAt. A fourth, ownerId, is forced from the authenticated caller on any non-admin create.

create does accept a client-supplied id matching ^[A-Za-z0-9_-]{1,64}$ — URL-segment safe — for idempotent retries. A duplicate returns 409 Conflict.

Create an index for every field a policy filter mentions. Without one, each authorized list is a collection scan.

Terminal window
curl -X POST "$URL/api/v1/admin/collections/posts/indexes" \
-H "authorization: Bearer $ADMIN_KEY" -H 'content-type: application/json' \
-d '{"keys":{"ownerId":1},"unique":false}'

In the bundle, indexes live alongside the schema:

{
"name": "posts",
"schema": { },
"policy": { },
"indexes": [{ "name": "by_slug", "keys": { "slug": 1 }, "unique": true }]
}

System indexes such as id_1 cannot be dropped.

A handful of names are refused, and it is worth knowing before you design a schema — users and projects are the two that catch people out.

System collections, which Groveback already owns with their own endpoints:

users · sessions · verification_tokens · memberships · projects · organizations

API route names, which would be unreachable at /api/v1/<name>:

auth · admin · billing · control · storage · graphql · health · realtime · run

Plus anything starting with __ or system..

Creating one fails with "users" is a reserved collection name, and nothing is created. The same check applies to database-trigger functions and webhooks.

Pick another name — app_users, client_projects, boards. users in particular is reserved because Groveback gives you end-user auth: sign-up, login, sessions and password hashing live at /api/v1/auth/*. For extra per-user fields, keep a separate collection keyed by the uid.

Terminal window
curl -X POST "$URL/api/v1/admin/collections/posts/rename" \
-H "authorization: Bearer $ADMIN_KEY" -H 'content-type: application/json' \
-d '{"to":"articles"}'

Dropping a collection deletes its documents. Note that grove push --prune will never delete a collection or a bucket — those hold data, so removing one is a deliberate act in the dashboard or through the API.