Data modeling

Pick collections and ids, declare schemas and unique fields, index for the queries you run, and choose KV, SQL, the ledger or files when a collection is the wrong shape.

Docs menu

Collections and documents

A collection holds documents of one shape; collection.create takes just a name (1 to 64 characters) and declares no schema by itself. Create one collection per kind of record (orders, customers), not one collection per customer or per day: a schema, its indexes and its security rules all apply per collection, so splitting a kind of record across many collections multiplies the declarations you 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 answers _id, an OID assigned at insert. Pass identity (field names) to collection.create to make a record's key content-addressed instead: the same identity fields always resolve to the same record, so a second insert with the same identity updates it rather than creating a duplicate. Use identity for records a caller might insert more than once with the same natural key (an external order id, a dedup key); use the assigned _id for everything else.

Schemas and unique fields

Declare a schema with schema.update once a collection exists: rules (field name, type, and required, nullable, min, max, pattern, enum), and optional constraints across fields. strict: true (the default) refuses a write naming a field no rule declares, which catches a typo'd field name at write time instead of leaving it silently stored.

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}]}'

A unique field or compound (index.create with kind: "collection", unique: true) refuses a second document holding the same value with UNIQUE (409): declare one for every natural key you would otherwise check for uniqueness in your own code.

{"name": "substrio_index_create", "arguments": {"kind": "collection", "collection": "customers", "field": "email", "unique": true}}

Indexes for the queries you run

A where clause on anything but _id needs that field declared indexed or unique first, or the query is refused with APP_QUERY_NOT_INDEXED: declare an index for every field you filter or sort on, before you ship the query that uses it. 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 (expiry, text, vector, aggregate).

References between collections

A document holds another collection's _id as a plain field (order.customer_id); there is no foreign-key type to declare. Index that field, then either query each side yourself 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) for bytes, counters or a cache keyed by a string you already have: a session token, a rate-limit counter. No schema, no query language, cheaper per operation than a document.
  • SQL (runtime/sql) for relational data you would otherwise flatten into many joined collections, or that a reporting tool expects as tables.
  • The ledger (runtime/ledger) for money or any signed, append-only balance: every entry is posted once and balances stay current without summing entries, which a total field you decrement yourself does not give you.
  • Files (files) for bytes meant to be downloaded by path (an image, a PDF, an export) rather than queried by field.