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 column types - that's all.
OriginChain keeps its type system small on purpose. There's no ulid, no decimal, no timestamp type. You map your data to one of six base types and the rest is convention.
| 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. Use for timestamps (epoch milliseconds or microseconds - pick one and stick with it), monotonic counters, sequence numbers. |
| 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. |
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.