Collections and documents
A collection holds documents of one shape. collection.create takes a name of 1 to 64 characters and does not declare a schema by itself. Create one collection per kind of record, such as orders or customers. A schema, indexes, and security rules apply to a collection, so splitting one kind of record across collections for each customer or day multiplies the declarations you must keep in sync.
curl -s -X POST "$SUBSTRIO_URL/v1/_meta/collections" -H "Authorization: Bearer $SUBSTRIO_KEY" \
-H 'content-type: application/json' -d '{"name":"orders"}'
Ids
Every write returns _id, an OID assigned at insert. To make a record's key content-addressed, pass field names as identity to collection.create. The same identity fields resolve to the same record, so a second insert with that identity updates it instead of creating a duplicate. Use identity for records a caller might insert more than once with the same natural key, such as an external order id or deduplication key. Use the assigned _id for other records.
Schemas and unique fields
Once a collection exists, declare its schema with schema.update. Provide rules with field names, types, and properties such as required, nullable, min, max, pattern, and enum. You can also provide constraints across fields. With the default strict: true, a write naming an undeclared field is refused, so a misspelled field is caught rather than stored silently.
curl -s -X PATCH "$SUBSTRIO_URL/v1/orders/_schema" -H "Authorization: Bearer $SUBSTRIO_KEY" \
-H 'content-type: application/json' \
-d '{"rules":[{"field":"customer_id","type":"string","required":true},
{"field":"total","type":"number","min":0,"required":true}]}'
With a unique field or compound index (index.create with kind: "collection" and unique: true), a second document with the same value produces UNIQUE (409). Declare one for each natural key that your code would otherwise need to check for uniqueness.
{"name": "substrio_index_create", "arguments": {"kind": "collection", "collection": "customers", "field": "email", "unique": true}}
Indexes for the queries you run
For a query offered to another app, each field in its where clause other than _id must have a declared index or unique constraint. Otherwise, the offer is refused with APP_QUERY_NOT_INDEXED. A declared index also avoids the first-query indexing cost described in the chain reference. A compound index (fields instead of field) covers a filter on several fields together. A space (kind: "space") covers a unique value shared across more than one field. See storage/schema-views for the full index.create options, including expiry, text, vector, and aggregate indexes.
References between collections
A document can store another collection's _id as a plain field, such as order.customer_id. There is no foreign-key type to declare. Index that field, then query each side or run a single relational read with a join:
{"collection": "orders", "joins": [{"collection": "customers", "localField": "customer_id", "foreignField": "_id", "as": "customer"}]}
See chains-over-api for joins, grouping and aggregates as one typed request.
When not a collection
- KV (runtime/kv) stores bytes, counters, or a cache under a string key, such as a session token or rate-limit counter. It has no schema or query language and costs less per operation than a document.
- SQL (runtime/sql) suits relational data that would otherwise require many joined collections, or data a reporting tool expects as tables.
- The ledger (runtime/ledger) stores money or other signed, append-only balances. Entries are posted once, and balances stay current without summing them.
- Files (files) store bytes downloaded by path, such as an image, a PDF, or an export.