Schemas
A schema is the shape of your data - like the columns of a spreadsheet. You write it as a small TOML file, send it to OriginChain once, and then you can save and query rows that follow that shape.
Three pages, three reading paths. This page is the overview - read it first. Tutorial walks you through building one from scratch, step by step. Reference is the field-by-field manual you reach for once you know what you're looking for.
The smallest possible schema.
This is the minimum viable schema - one table with three columns. Save it as manifest.toml.
# manifest.toml - the smallest schema you can write.
namespace = "shop"
table = "customers"
primary_key = ["id"]
[[columns]]
name = "id"
ty = "str"
required = true
[[columns]]
name = "email"
ty = "str"
[[columns]]
name = "signup_ms"
ty = "u64" | Piece | What it does |
|---|---|
| namespace | A logical grouping for related tables. Like a Postgres schema. Use one per product area: shop, analytics, auth. |
| table | The table's name. You query it as namespace.table - here, shop.customers. |
| primary_key | A list of column names that uniquely identify a row. Most tables use a single ID column; put it in the array. |
| [[columns]] | One block per column. name = the column name, ty = the type. Set required = true on columns that can't be null. |
Six core types - and purpose-built ones on top.
Six base scalars cover most data. On top of them OriginChain ships purpose-built types - timestamp, date, uuid, decimal, enum, json, list, inet, and more - each format-validated on write. See the schema reference for the full list.
| Type | Use it for |
|---|---|
| str | Any text. Names, emails, IDs (including ULIDs and UUIDs - they travel as their canonical text form), category labels, status strings, JSON blobs you don't need to query into. |
| i64 | Signed 64-bit integers. Use for money stored in minor units (cents, paise, satoshis), quantities, refundable counters. Never use floats for money. |
| u64 | Unsigned 64-bit integers. Monotonic counters, sequence numbers. For event time prefer the native timestamp type. |
| f64 | Double-precision float. Use for rates, ratios, percentages, scientific measurements - things where rounding doesn't matter. |
| bool | A single yes/no value. |
| bytes | Opaque binary blob. Use for compressed payloads, encoded vectors stored alongside rows, anything that's already a byte sequence. |
Purpose-built types
Declared the same way (ty = "timestamp") and validated on write - a wrong-shaped value is rejected with a 400. Each stores on the same substrate as its backing scalar, so filters and range scans stay fast.
| Type | For |
|---|---|
| timestamp | Event time - epoch milliseconds. Range-scans like an integer. |
| date | Calendar date (epoch days), no time component. |
| interval | A duration in milliseconds. |
| uuid | RFC-4122 UUID, format-validated. |
| decimal | Exact money - e.g. "19.99", sent as a string. No float rounding. |
| enum | A string constrained to a declared set of variants. |
| text | Unbounded UTF-8 prose. |
| json | A structured JSON object or array, stored verbatim. |
| list | A homogeneous array with an optional element type. |
| inet | An IPv4 or IPv6 address. |
| point | A geographic point - {lat, lng} or [lng, lat]. |
Register a schema.
Once you've written your manifest.toml, send it to OriginChain. After this succeeds, the table exists and you can start saving rows.
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/schemas" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: text/plain" \
--data-binary @manifest.tomlwith open("manifest.toml") as f:
db.schemas.register(f.read())import { readFileSync } from "node:fs";
await db.registerSchema(readFileSync("manifest.toml", "utf-8"));tomlBytes, _ := os.ReadFile("manifest.toml")
req, _ := http.NewRequestWithContext(ctx, "POST",
"https://"+os.Getenv("OC_HOST")+"/v1/tenants/"+os.Getenv("OC_TENANT")+"/schemas",
bytes.NewReader(tomlBytes))
req.Header.Set("Authorization", "Bearer "+os.Getenv("OC_TOKEN"))
req.Header.Set("Content-Type", "text/plain")
http.DefaultClient.Do(req) - Wrong Content-Type. The body is TOML, not JSON. Use
Content-Type: text/plain. - Forgetting primary_key. Every schema needs at least one column listed in the top-level
primary_key = ["..."]array. - Wrong column type. Only the six types above work.
ulid,decimal,timestamp,vector,string, andintall return 400.
What you can add beyond the basics.
The minimal schema above lets you save and read rows. Add any of these blocks when you need more:
WHERE status = ... scans every row.qty > 0, status IN ('paid','pending'), etc.customer.address.country into a queryable column at write time.What is not in the schema.
Two things people expect to declare on the schema but don't:
- Vector embeddings. There is no
[[extractions.vector]]block. Vectors are stored on a separate runtime endpoint (POST /vector/:table/put) and reference rows by primary key. See Vector tables. - Full-text indexes. There is no
[[extractions.fts]]block. Full-text indexes live on their own runtime endpoint (POST /fts/:table/:field). Tokenizer and analyzer pipeline live on the index call, not the schema. See FTS tables.
The schema is purely the row contract. Vector and full-text are auxiliary indexes that link back to rows by primary key.