Documentation menu

Collections

A constrained, multi-tenant-safe document store for your game - keyed JSON documents plus a few declared indexes. No SQL, no schema migrations, no shared database.

Collections give your game a place to keep its own structured records - a farm and its visitor list, a guild roster, a market listing - without ever touching a database. You declare a few named collections, store JSON documents in them, and query them through the indexes you declared. The network is derived from your API key, so the data is isolated per network with no networkId to pass.

It is deliberately not a SQL or Mongo surface. The constraints are the product: keyed JSON documents and a few declared access paths are exactly what makes a shared store safe to run multi-tenant. There is no ad-hoc WHERE, no joins, no scans - so one query can never starve the shard you share with other operators.

Three concepts

  • Collection - a named bucket (farms, guilds). Has a key field and a list of declared indexes.
  • Document - an arbitrary JSON object addressed by an id. Schema-less: every document in a collection can look different.
  • Index - a field you declared as queryable. Two kinds: eq (exact-match filter) and sorted (range / top-N, i.e. a leaderboard). Only declared fields are queryable.

Declare your collections

Declare collections in your game manifest, next to worlds and playerData. Each collection names its key field and the handful of indexes you want to query by.

hive-game.json
{
  "collections": [
    {
      "name": "farms",
      "key": "id",
      "indexes": [
        { "field": "ownerId", "type": "eq" },
        { "field": "level",   "type": "sorted" }
      ]
    }
  ]
}

Or register them from code on startup - same effect, useful when the set is dynamic:

Startup.java
hive.collections().register(List.of(
    CollectionDef.of("farms", "id",
        CollectionDef.Index.eq("ownerId"),
        CollectionDef.Index.sorted("level"))));

Registering is idempotent: call it once on boot. Declaring an index is what makes a field queryable, so add the index before you rely on queryBy or top for it.

Give each document its own generated id (use CollectionsClient.newId()), and make relationships indexed fields instead. A farm keyed by id with an ownerId eq index can change owners with one put; a farm keyed by its owner can’t. Don’t key a document by something that can change.

Store and read documents

Grab a handle to a collection and put, get, query, or delete. Every call is async and returns a CompletableFuture.

Farms.java
var farms = hive.collection("farms");

farms.put(farmId, farmDoc);             // upsert (doc serialized to JSON)
farms.get(farmId);                      // by id -> raw JSON string, or null
farms.getMany(List.of(a, b, c));        // batch get; missing ids omitted
farms.queryBy("ownerId", uuid, 50);     // declared eq index only
farms.top("level", 10);                 // declared sorted index only
farms.range("level", 10.0, 20.0, 50, true);  // window on a sorted index
farms.page("level", cursor, 50, true);  // keyset walk; feed nextCursor back
farms.delete(farmId);
CallWhat it does
put(id, doc)upsert a document (serialized to JSON via Gson)
putJson(id, json)upsert a document already serialized as a JSON object string
get(id)fetch one document as a raw JSON string, or null if absent
getMany(ids)batch fetch by id (up to 200); missing ids are omitted
queryBy(field, value, limit)exact-match on a declared eq index
top(field, limit)top-N by a declared sorted index, highest first
range(field, min, max, limit, desc)bounded window on a sorted index; either bound optional
page(field, cursor, limit, desc)keyset-paginated walk over a sorted index; pass nextCursor back until it’s null
delete(id)remove a document by id

Query calls return raw JSON document strings. They fail if you call them with a field that is not a declared index of that type - so only ever query the fields you declared.

Every read also has a typed form: hive.collection("farms", Farm.class) returns a handle whose reads come back as Farm / List<Farm> instead of raw JSON.

What the constraints buy you

The things Collections refuses are what keep it safe and cheap to run:

  • Only declared fields are queryable. No free-text search, no querying arbitrary JSON. The query surface is bounded by what you declared, so it stays fast.
  • No ad-hoc SQL, joins, or predicates. One bad query can’t become everyone’s outage.
  • No raw database handles. You never get a Postgres or Redis connection; the network boundary is enforced server-side from your API key.

Writes are last-write-wins (put upserts; there is no compare-and-set here). Documents are capped at 256KB, batch gets at 200 ids, and every query result at 500 rows - past that, walk with page.

Browse them from the dashboard

Operators can browse and edit a network’s collections from the dashboard: list collections and their index badges, page through documents, filter by an eq index, view a sorted leaderboard, and view/edit/delete an individual document as raw JSON. The dashboard only ever offers the filters and sorts you declared - the same constrained model, made visible.

Current limits

This ships narrow and honest. Today that means:

  • One index per query - no compound queries yet (e.g. filter by an eq index and sort by a sorted one in the same call).
  • No compare-and-set - writes are last-write-wins whole-document upserts.
  • No per-edit audit trail on collection writes (the player-data store has one; collections do not yet).
  • First-N, not totals - query results are the first N matches; there is no per-query total count beyond a collection’s document count.

Need a compound query or an export? Ask us - we answer fast.