Skip to content

Collections

This page shows how to use collections: named sets of JSON records, each stored under a key, that the server can filter and sort. A collection suits structured data you look up or query: contacts, bookmarks, settings per device.

Collection paths follow the document rules: letters, digits, -, _ and /, at most 256 characters. Keys are at most wire.MaxCollectionKeyLength (256) bytes.

err := cl.CreateCollection(ctx, "contacts", "Contacts")

CreateCollection(ctx, path, name, opts...) creates a collection you own. It is private by default; pass client.WithCollectionVisibility(wire.VisibilityShared) or wire.VisibilityPublic to open it.

Content must be valid JSON; anything else is refused with status 400.

res, err := cl.PutCollectionItem(ctx, "contacts", "ann",
[]byte(`{"name":"Ann","age":31,"tags":["work"]}`))
fmt.Println(res.Created, res.ETag, res.Version) // true sha256:… 1
item, err := cl.GetCollectionItem(ctx, ownerID, "contacts", "ann")
if item != nil {
fmt.Println(string(item.Content), item.Hash, item.Version, item.UpdatedAt)
}

PutCollectionItem creates or replaces the item and returns a *client.CollectionItemResult with the new ETag and version. GetCollectionItem returns nil, nil for a key that does not exist, and client.ErrForbidden for a collection you may not read. Content is a json.RawMessage, ready to unmarshal.

For optimistic locking, pass the ETag you last saw. A mismatch is a *client.ConflictError:

_, err = cl.PutCollectionItem(ctx, "contacts", "ann", updated,
client.WithCollectionIfMatch(item.Hash))
if errors.Is(err, client.ErrConflict) {
// re-read, merge, retry
}

Delete one item, or the whole collection:

deleted, err := cl.DeleteCollectionItem(ctx, "contacts", "ann") // false, nil if absent
deleted, err = cl.DeleteCollection(ctx, "contacts")

QueryCollection filters items on their JSON content:

page, err := cl.QueryCollection(ctx, ownerID, "contacts",
map[string]any{"age": map[string]any{"$gte": 30}},
client.WithQuerySort("name", true),
client.WithQueryLimit(20),
)
for _, it := range page.Items {
fmt.Println(it.Key, string(it.Content))
}
Filter Matches
{"field": value} Items whose field equals value (JSON containment).
{"field": {"$lt": n}}, $lte, $gt, $gte Numeric comparison.
{"field": {"$ilike": "%ann%"}} Case-insensitive pattern on a string field.
{"field": {"$contains": v}} field contains v, such as an element of an array.
{"$and": [f1, f2]}, {"$or": [f1, f2]} Combinations.

A nil or empty filter matches every item. Field names may contain letters, digits, _, . and -.

Option Effect
WithQuerySort(field, asc) Sort by a top-level field, then by key. Default: key order.
WithQueryLimit(n) Page size. Server default 50.
WithQueryCursor(c) Continue from the previous page’s NextCursor.
WithQueryOffset(n) Skip n items. Ignored when a cursor is given.
WithQueryTotal() Report the number of matches in TotalCount. Costs a scan, so it is off by default.
WithQueryServer(id) Send this request to one server only.

A *client.CollectionQueryResponse has Items, HasMore and NextCursor. Follow the cursor rather than an offset, so items added between pages do not shift the results:

var cursor string
for {
opts := []client.CollectionQueryOption{client.WithQueryLimit(100)}
if cursor != "" {
opts = append(opts, client.WithQueryCursor(cursor))
}
page, err := cl.QueryCollection(ctx, ownerID, "contacts", nil, opts...)
if err != nil {
return err
}
handle(page.Items)
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}

TotalCount is -1 when the server did not count: on a page fetched by cursor, and on a filtered query without WithQueryTotal.

ListCollectionKeys returns one page of keys and the total; ListCollectionKeysPage adds the cursor:

keys, total, err := cl.ListCollectionKeys(ctx, ownerID, "contacts")
kp, err := cl.ListCollectionKeysPage(ctx, ownerID, "contacts", client.WithQueryLimit(500))
fmt.Println(kp.Keys, kp.HasMore, kp.NextCursor)
info, err := cl.GetCollection(ctx, ownerID, "contacts") // nil, nil if absent
fmt.Println(info.Name, info.RecordCount, info.Visibility)
all, err := cl.ListCollections(ctx, ownerID) // the ones you may read
_, err = cl.SetCollectionVisibility(ctx, "contacts", wire.VisibilityShared)
access, err := cl.GrantCollectionReader(ctx, "contacts", friendID)
_, err = cl.RevokeCollectionReader(ctx, "contacts", friendID)
access, err = cl.CollectionAccess(ctx, "contacts")

Visibility covers every item and every query. Only the owner writes, whatever the visibility.