Quickstart - 5 minutes from zero to your first query
By the end of this page you will have signed up, defined a table, saved data into it, and run four kinds of search against it: SQL for filtering, vector for similarity, full-text for keywords, and graph for relationships - all on the same instance, with no extra setup between them.
Every code example is shown in cURL, Python, TypeScript, and Go. Pick the tab you want and the rest of the page follows that language.
Sign up and get your token.
Head to app.originchain.ai/signup. Add a card during signup - every account is billed from day 1 on whichever tier you pick. After signup you will:
- Pick a region close to your users. Your data never leaves it.
- Pick a tier (you can change it later). Start with the smallest one.
- Wait about 90 seconds while your dedicated instance provisions.
- Copy three things from the "Connect" panel: your endpoint URL, a one-time bearer token, and your tenant ID.
# Paste these into your shell. Replace the values with the ones
# from your dashboard after you finish step 1.
export OC_HOST=acme.ap-south-1.db.originchain.ai
export OC_TENANT=acme
export OC_TOKEN=oc_live_xxxxxxxxxxxxxxxx The bearer token is shown once and never again. Paste it into your secrets manager (1Password, AWS Secrets Manager, Doppler, etc.) before you close the tab. If you lose it, rotate it from the dashboard and update your apps.
Install an SDK (or skip if you prefer cURL).
The SDKs handle authentication, retries, and request signing for you. You can use raw HTTP too - every example below shows cURL alongside.
# cURL is built into macOS and most Linux distros - check it's there:
curl --version
# On Windows: comes with Git Bash and PowerShell 6+.pip install originchainnpm install @originchain/sdkgo get github.com/originchain/sdk-go # cURL has no client - every call carries the bearer header.
# Confirm your env vars resolve and the instance is reachable:
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
"https://$OC_HOST/health" \
-H "Authorization: Bearer $OC_TOKEN"
# → HTTP 200 if everything is wired up.import os
from originchain import OriginChain
db = OriginChain(
base_url=f"https://{os.environ['OC_HOST']}",
bearer=os.environ["OC_TOKEN"],
tenant=os.environ["OC_TENANT"],
)
# `db` is your handle for the rest of this page.import { OriginChainClient } from "@originchain/sdk";
const db = new OriginChainClient({
baseUrl: `https://${process.env.OC_HOST}`,
bearer: process.env.OC_TOKEN!,
});
// `db` is your handle for the rest of this page.package main
import (
"context"
"os"
"github.com/originchain/sdk-go"
)
var (
ctx = context.Background()
db = originchain.NewClient(originchain.Config{
BaseURL: "https://" + os.Getenv("OC_HOST"),
Bearer: os.Getenv("OC_TOKEN"),
})
)
// `db` and `ctx` are your handles for the rest of this page. Python wraps every endpoint on this page. TypeScript and Go wrap SQL, vector, full-text, graph, and the ask endpoint - row reads and writes use raw fetch / net/http for now (helpers ship in the next release).
Define your first table.
A schema is the shape of your data - the list of columns, their types, and any extra indexes or extractions you want. OriginChain reads schemas as small TOML files.
Save this as schemas/orders.toml. It declares an orders table that you can later query four ways - SQL, vector, full-text, graph.
namespace = "shop"
table = "orders"
primary_key = ["id"]
[[columns]]
name = "id"
ty = "str" # ULIDs / UUIDs travel as text
required = true
[[columns]]
name = "customer"
ty = "str"
[[columns]]
name = "amount_cents"
ty = "i64" # money in minor units - never f64
[[columns]]
name = "status"
ty = "str"
[[columns]]
name = "notes"
ty = "str"
[[columns]]
name = "placed_ms"
ty = "u64" # epoch milliseconds
# Make WHERE status = ... fast.
[[indexes]]
name = "by_status"
columns = ["status"] | Block | What it does |
|---|---|
| namespace, table | A two-part name for your table. You address it as namespace.table in queries (here, shop.orders). |
| 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 column per block. Set required = true on columns that can't be null. Six types: str, i64, u64, f64, bool, bytes. |
| [[indexes]] | Secondary index for fast filtering. Without this, WHERE status = ... scans every row. |
Notice there's no column type for vectors or full-text indexes. Those live on separate runtime endpoints - you'll see how in steps 7 and 8. The schema TOML is just for rows. See Schema reference for relations, foreign keys, CHECK constraints, and derived columns.
# Save the TOML above as schemas/orders.toml, then:
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/schemas" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: text/plain" \
--data-binary @schemas/orders.tomlwith open("schemas/orders.toml") as f:
db.schemas.register(f.read())import { readFileSync } from "node:fs";
const toml = readFileSync("schemas/orders.toml", "utf-8");
await db.registerSchema(toml);tomlBytes, _ := os.ReadFile("schemas/orders.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. TOML schemas use
text/plain, notapplication/json. - No primary key. Every schema needs a top-level
primary_key = ["..."]array naming at least one declared column, otherwise registration fails.
Save your first row.
One call saves the row and updates every secondary index that touches its columns - all atomically, behind one fsync. (Vector and full-text indexes are managed separately - we'll do those in steps 7 and 8.)
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/rows/shop.orders" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "01JTRX9KQ3YH8K2WMX0F5JZAB7",
"customer": "01JTRX1H4Q9P0N2WMX0F5JZ001",
"amount_cents": 12950,
"status": "paid",
"notes": "rush delivery, signed by recipient",
"placed_ms": 1714478049000
}'db.rows.put("shop.orders", {
"id": "01JTRX9KQ3YH8K2WMX0F5JZAB7",
"customer": "01JTRX1H4Q9P0N2WMX0F5JZ001",
"amount_cents": 12950,
"status": "paid",
"notes": "rush delivery, signed by recipient",
"placed_ms": 1714478049000,
})// TS SDK doesn't wrap row writes yet - using fetch directly.
await fetch(
`https://${process.env.OC_HOST}/v1/tenants/${process.env.OC_TENANT}/rows/shop.orders`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OC_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "01JTRX9KQ3YH8K2WMX0F5JZAB7",
customer: "01JTRX1H4Q9P0N2WMX0F5JZ001",
amount_cents: 12950,
status: "paid",
notes: "rush delivery, signed by recipient",
placed_ms: 1714478049000,
}),
},
);// Go SDK doesn't wrap row writes yet - using net/http directly.
body, _ := json.Marshal(map[string]any{
"id": "01JTRX9KQ3YH8K2WMX0F5JZAB7",
"customer": "01JTRX1H4Q9P0N2WMX0F5JZ001",
"amount_cents": 12950,
"status": "paid",
"notes": "rush delivery, signed by recipient",
"placed_ms": uint64(1714478049000),
})
req, _ := http.NewRequestWithContext(ctx, "POST",
"https://"+os.Getenv("OC_HOST")+"/v1/tenants/"+os.Getenv("OC_TENANT")+"/rows/shop.orders",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("OC_TOKEN"))
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req) For bulk imports (1k+ rows), see Insert → bulk. The endpoint takes JSON arrays or NDJSON streams.
Read it back.
Look up a row by its primary key. This is the fastest read in OriginChain - direct hash lookup, single-digit milliseconds.
curl "https://$OC_HOST/v1/tenants/$OC_TENANT/rows/shop.orders/01JTRX9KQ3YH8K2WMX0F5JZAB7" \
-H "Authorization: Bearer $OC_TOKEN"row = db.rows.get("shop.orders", "01JTRX9KQ3YH8K2WMX0F5JZAB7")
print(row["status"], row["amount_cents"])
# → paid 12950// TS SDK doesn't wrap row reads yet.
const res = await fetch(
`https://${process.env.OC_HOST}/v1/tenants/${process.env.OC_TENANT}/rows/shop.orders/01JTRX9KQ3YH8K2WMX0F5JZAB7`,
{ headers: { "Authorization": `Bearer ${process.env.OC_TOKEN}` } },
);
const row = await res.json();
console.log(row.status, row.amount_cents);req, _ := http.NewRequestWithContext(ctx, "GET",
"https://"+os.Getenv("OC_HOST")+"/v1/tenants/"+os.Getenv("OC_TENANT")+"/rows/shop.orders/01JTRX9KQ3YH8K2WMX0F5JZAB7",
nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("OC_TOKEN"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var row map[string]any
json.NewDecoder(resp.Body).Decode(&row)
fmt.Println(row["status"], row["amount_cents"]) Run a SQL query.
Filter, group, and aggregate with regular SQL. This query finds every paying customer and totals their orders.
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/sql" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sql": "SELECT customer, SUM(amount_cents) AS total FROM shop.orders WHERE status = '\''paid'\'' GROUP BY customer LIMIT 10"
}'result = db.sql("""
SELECT customer, SUM(amount_cents) AS total
FROM shop.orders
WHERE status = 'paid'
GROUP BY customer
LIMIT 10
""")
# result.kind == "select"; rows are dicts.
for row in result.rows:
print(row["customer"], row["total"])const result = await db.sql(`
SELECT customer, SUM(amount_cents) AS total
FROM shop.orders
WHERE status = 'paid'
GROUP BY customer
LIMIT 10
`);
if (result.kind === "select") {
for (const row of result.rows) {
console.log(row.customer, row.total);
}
}result, err := db.SQL(ctx, `
SELECT customer, SUM(amount_cents) AS total
FROM shop.orders
WHERE status = 'paid'
GROUP BY customer
LIMIT 10
`)
if err != nil { /* handle */ }
if result.Kind == "select" {
for _, row := range result.Rows {
fmt.Println(row["customer"], row["total"])
}
} The SQL surface supports SELECT, WHERE, GROUP BY, aggregates (COUNT, SUM, AVG, MIN, MAX), INNER / LEFT / RIGHT / FULL OUTER JOIN (up to 32 tables), LIMIT, plus INSERT, UPDATE, DELETE, and transactions.
Not yet supported: HAVING, ORDER BY, WITH (CTEs), window functions, and correlated subqueries. See SQL reference for the full status.
Vector search.
Vector data lives on its own endpoint, separate from rows. You store one embedding per row (linked by the row's primary key) and then run top-k similarity search against the query embedding.
# Save an embedding for the order. dim + metric are set per-call;
# the first put locks them in for the table.
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/vector/shop.orders/put" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "01JTRX9KQ3YH8K2WMX0F5JZAB7",
"embedding": [0.12, -0.04, 0.91, 0.33, -0.18, 0.05, 0.42, -0.27],
"dim": 8,
"metric": "cosine"
}' # Find the 10 orders whose embeddings are closest to a query vector.
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/vector/shop.orders/topk" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": [0.10, -0.05, 0.88, 0.30, -0.20, 0.07, 0.40, -0.25],
"k": 10,
"dim": 8,
"metric": "cosine"
}'# Put first, then query. The "id" links the vector back to the row.
db.vector.put(
"shop.orders",
"01JTRX9KQ3YH8K2WMX0F5JZAB7",
[0.12, -0.04, 0.91, 0.33, -0.18, 0.05, 0.42, -0.27],
)
hits = db.vector_topk(
"shop.orders",
query=[0.10, -0.05, 0.88, 0.30, -0.20, 0.07, 0.40, -0.25],
k=10,
dim=8,
metric="cosine",
)
for hit in hits:
print(hit.id, hit.score)const hits = await db.vectorTopk("shop.orders", {
query: [0.10, -0.05, 0.88, 0.30, -0.20, 0.07, 0.40, -0.25],
k: 10,
dim: 8,
metric: "cosine",
});
for (const hit of hits) {
console.log(hit.id, hit.score);
}hits, err := db.VectorTopK(ctx, "shop.orders", originchain.VectorTopKRequest{
Query: []float32{0.10, -0.05, 0.88, 0.30, -0.20, 0.07, 0.40, -0.25},
K: 10,
Dim: 8,
Metric: "cosine",
})
if err != nil { /* handle */ }
for _, h := range hits {
fmt.Println(h.ID, h.Score)
}
Recall and latency are tunable. See Vector reference for index choice (HNSW vs IVF), mode=fast vs mode=high_recall, and metadata filtering.
Full-text search.
Same pattern as vectors: full-text data lives on its own endpoint. You index the text once (linked to the row's primary key as doc_id), then search. mode=bm25 ranks hits by relevance (the same algorithm Elasticsearch and Lucene use).
# Index the text under (table, field, doc_id). doc_id = the row's PK.
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/fts/shop.orders/notes" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"doc_id": "01JTRX9KQ3YH8K2WMX0F5JZAB7",
"text": "rush delivery, signed by recipient"
}' # Then search.
curl "https://$OC_HOST/v1/tenants/$OC_TENANT/fts/shop.orders/notes?q=rush+delivery&mode=bm25&k=10" \
-H "Authorization: Bearer $OC_TOKEN"# Index + search.
db.fts.index(
"shop.orders",
"notes",
doc_id="01JTRX9KQ3YH8K2WMX0F5JZAB7",
text="rush delivery, signed by recipient",
)
hits = db.fts.search(
"shop.orders",
"notes",
q="rush delivery",
mode="bm25",
k=10,
)
for hit in hits:
print(hit.score, hit.doc_id)await db.ftsIndex("shop.orders", "notes", {
doc_id: "01JTRX9KQ3YH8K2WMX0F5JZAB7",
text: "rush delivery, signed by recipient",
});
const hits = await db.ftsSearch("shop.orders", "notes", {
q: "rush delivery",
mode: "bm25",
k: 10,
});
for (const hit of hits) {
if (typeof hit === "string") console.log(hit);
else console.log(hit.score, hit.doc_id);
}db.FTSIndex(ctx, "shop.orders", "notes", originchain.FTSIndexRequest{
DocID: "01JTRX9KQ3YH8K2WMX0F5JZAB7",
Text: "rush delivery, signed by recipient",
})
hits, err := db.FTSSearch(ctx, "shop.orders", "notes", originchain.FTSSearchRequest{
Q: "rush delivery",
Mode: "bm25",
K: 10,
})
if err != nil { /* handle */ }
for _, h := range hits {
fmt.Println(h.Score, h.DocID)
}
Other modes: mode=boolean (every term must match, no ranking), mode=phrase (exact phrase). See Full-text reference for fuzzy search, highlights, and facets.
Walk a graph.
The customer column links every order to a customer. Any column you'd later use as a graph edge needs a [[relations]] block on the schema (we'll add one in Insert → graph). Then walking is one call:
curl "https://$OC_HOST/v1/tenants/$OC_TENANT/graph/shop.orders/neighbors?rel=customer&pk=01JTRX1H4Q9P0N2WMX0F5JZ001" \
-H "Authorization: Bearer $OC_TOKEN"neighbors = db.graph.neighbors(
"shop.orders",
rel="customer",
pk="01JTRX1H4Q9P0N2WMX0F5JZ001",
)
print(len(neighbors), "orders for this customer")const neighbors = await db.graph.neighbors("shop.orders", {
rel: "customer",
pk: "01JTRX1H4Q9P0N2WMX0F5JZ001",
});
console.log(neighbors.length, "orders for this customer");neighbors, err := db.Graph().Neighbors(ctx, "shop.orders", originchain.NeighborsRequest{
Rel: "customer",
PK: "01JTRX1H4Q9P0N2WMX0F5JZ001",
})
if err != nil { /* handle */ }
fmt.Println(len(neighbors), "orders for this customer") More than one hop? See Graph reference for BFS, shortest path, PageRank, and community detection.