Agent index (llms.txt) — fetch this first, then the page it names.
Databases
Orama provides two database systems for your applications: per-namespace SQLite databases for isolated, node-local storage, and a distributed RQLite ORM for replicated relational data. SQLite databases are reached from the CLI and the REST API; the RQLite ORM is reached from the SDK and REST. Both are control-plane routes (db): they need an admin key or a wallet holding the developer role or above, and an app-runtime key does not reach them.
SQLite Databases
Per-namespace isolated SQLite databases stored on the node's local filesystem. Each database is created on a specific node (the "home node") and queries are routed to that node. Databases use WAL mode for concurrent read/write access.
Creating a Database
CLI
orama db create my-database
REST API
curl -X POST https://gateway.example.com/v1/db/sqlite/create \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"database_name": "my-database"}'Database names must contain only alphanumeric characters, underscores, and hyphens (max 64 characters).
Querying a Database
The SQLite endpoint accepts any SQL statement. It automatically detects whether a query is a read (SELECT) or write (INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, TRUNCATE, REPLACE) and returns the appropriate response format.
CLI
# Read query
orama db query my-database "SELECT * FROM users"
# Write query
orama db query my-database "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')"
# DDL query
orama db query my-database "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"REST API
curl -X POST https://gateway.example.com/v1/db/sqlite/query \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"database_name": "my-database",
"query": "SELECT * FROM users WHERE email = ?",
"params": ["alice@example.com"]
}'Read response:
{
"columns": ["id", "name", "email"],
"rows": [[1, "Alice", "alice@example.com"]]
}Write response:
{
"rows_affected": 1,
"last_insert_id": 1
}Listing Databases
CLI
orama db list
Outputs a table with columns: NAME, SIZE, BACKUP CID, CREATED.
REST API
curl https://gateway.example.com/v1/db/sqlite/list \ -H "Authorization: Bearer <token>"
Backups
Databases can be backed up to IPFS for disaster recovery. Backups are content-addressed and immutable.
CLI
# Create a backup orama db backup my-database # List all backups for a database orama db backups my-database
REST API
# Create a backup
curl -X POST https://gateway.example.com/v1/db/sqlite/backup \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"database_name": "my-database"}'
# List backups
curl "https://gateway.example.com/v1/db/sqlite/backups?database_name=my-database" \
-H "Authorization: Bearer <token>"Deleting a Database
orama db delete my-database # This permanently deletes "my-database" and its file. There is no undo. # Type the database name to confirm: my-database # ✓ my-database deleted (3 file(s) removed).
Typing the name back is the confirmation, not a y/n prompt: a y/n is answered
reflexively, and the mistake this guards against is the right command aimed at
the wrong database. --yes skips the prompt for scripts.
The database file and its write-ahead log are removed from the node that holds
them. Back up first with orama db backup if you may want the data again.
Database Features
- +✅ WAL Mode: Write-Ahead Logging for better concurrency
- +✅ Namespace Isolation: Complete separation between namespaces. Tenant SQL cannot
ATTACH/DETACHanother database file orVACUUM INTOa path, can run only per-file pragmas (table_info,table_list,index_list,index_info,foreign_key_list,foreign_key_check,foreign_keys,defer_foreign_keys,user_version,application_id,integrity_check,quick_check,page_count,freelist_count,optimize), and extra statements in one query are rejected. - +✅ On-Demand Backups: Back up to IPFS anytime with
orama db backup - +✅ ACID Transactions: Full SQLite transactional support
- +✅ Concurrent Reads: Multiple readers can query simultaneously
SQLite CLI Reference
| Command | Description |
|---|---|
orama db create <name> | Create a new SQLite database |
orama db query <name> <sql> | Execute a SQL query |
orama db list | List all databases |
orama db backup <name> | Backup database to IPFS |
orama db backups <name> | List backups for a database |
SQLite REST API Reference
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/db/sqlite/create | Create a new database |
POST | /v1/db/sqlite/query | Execute a SQL query |
GET | /v1/db/sqlite/list | List all databases |
POST | /v1/db/sqlite/backup | Backup database to IPFS |
GET | /v1/db/sqlite/backups | List backups for a database |
RQLite ORM
The distributed RQLite database is the cluster-wide database powering Orama's internal state. It is also exposed to applications through an ORM-style HTTP gateway and the TypeScript SDK. RQLite uses the Raft consensus protocol for replication -- writes go through the elected leader and are replicated to followers.
The RQLite ORM supports raw SQL (exec/query), a fluent query builder, a repository pattern, transactions, and schema management.
SDK Setup
import { createClient } from "@debros/orama";
const client = createClient({
baseURL: "https://ns-myapp.cluster.example.com",
apiKey: process.env.ORAMA_API_KEY,
});The RQLite routes are control plane (db:write): use an admin key from a server or CI, or sign in as a wallet holding the developer role or above. An app-runtime key does not reach them (Access control).
Creating Tables
await client.db.createTable( "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)" );
Writes with exec
Use exec for INSERT, UPDATE, DELETE, and DDL statements:
const result = await client.db.exec( "INSERT INTO users (name, email) VALUES (?, ?)", ["Alice", "alice@example.com"] ); console.log(result.rows_affected); console.log(result.last_insert_id);
Reads with query
Use query for SELECT statements:
const users = await client.db.query( "SELECT * FROM users WHERE email = ?", ["alice@example.com"] );
Find
Map-based criteria search without writing raw SQL:
// Find multiple records
const users = await client.db.find("users", { active: 1 });
// Find a single record (returns null if not found)
const user = await client.db.findOne("users", { email: "alice@example.com" });find supports options for ordering, grouping, pagination, and joins:
const users = await client.db.find(
"users",
{ active: 1 },
{ order_by: ["name ASC"], limit: 10, offset: 0 }
);Transactions
Group multiple operations into a sequence. Each statement specifies its kind (exec or query) along with the SQL and arguments:
const results = await client.db.transaction([
{
kind: "exec",
sql: "INSERT INTO logs (msg) VALUES (?)",
args: ["Event A"],
},
{
kind: "query",
sql: "SELECT COUNT(*) AS count FROM logs",
args: [],
},
]);QueryBuilder
The SDK includes a fluent query builder for constructing SELECT queries:
const activeUsers = await client.db
.createQueryBuilder("users")
.select("id", "name", "email")
.where("active = ?", [1])
.andWhere("age > ?", [18])
.orderBy("name DESC")
.limit(10)
.offset(0)
.getMany();
const singleUser = await client.db
.createQueryBuilder("users")
.where("id = ?", [1])
.getOne();
const count = await client.db
.createQueryBuilder("users")
.where("active = ?", [1])
.count();The query builder supports:
| Method | Description |
|---|---|
select(...columns) | Specify columns to select |
where(expr, args?) | Add a WHERE clause (AND) |
andWhere(expr, args?) | Add an AND WHERE clause |
orWhere(expr, args?) | Add an OR WHERE clause |
innerJoin(table, on) | Add an INNER JOIN |
leftJoin(table, on) | Add a LEFT JOIN |
rightJoin(table, on) | Add a RIGHT JOIN |
groupBy(...columns) | Group results |
orderBy(...columns) | Order results (e.g., "name DESC") |
limit(n) | Limit result count |
offset(n) | Skip first N results |
getMany() | Execute and return all matching rows |
getOne() | Execute and return the first matching row (or null) |
count() | Execute and return the count of matching rows |
Repository Pattern
For entity-oriented CRUD operations:
interface User {
id?: number;
name: string;
email: string;
}
const repo = client.db.repository<User>("users");
// Find multiple records
const users = await repo.find({ active: 1 });
// Find a single record (returns null if not found)
const user = await repo.findOne({ email: "alice@example.com" });
// Save a record (INSERT if no primary key, UPDATE if primary key exists)
await repo.save({ name: "Bob", email: "bob@example.com" });
// Remove a record (requires primary key)
await repo.remove({ id: 1 });By default, the primary key is "id". You can specify a different primary key:
const repo = client.db.repository<Session>("sessions", "session_id");Schema Management
// Get the full schema for all tables and views
const schema = await client.db.getSchema();
// Drop a table
await client.db.dropTable("old_table");RQLite ORM REST API Reference
All endpoints are under /v1/rqlite and accept POST with JSON body (except schema which is GET).
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/rqlite/query | Execute a SELECT query. Body: {sql, args} |
POST | /v1/rqlite/exec | Execute a write/DDL statement. Body: {sql, args} |
POST | /v1/rqlite/find | Find rows by criteria. Body: {table, criteria, options?} |
POST | /v1/rqlite/find-one | Find one row by criteria. Body: {table, criteria} |
POST | /v1/rqlite/select | Fluent SELECT builder. Body: {table, select?, where?, joins?, order_by?, group_by?, limit?, offset?, one?} |
POST | /v1/rqlite/transaction | Execute multiple ops atomically. Body: {ops: [{kind, sql, args}], return_results?} |
GET | /v1/rqlite/schema | Get database schema (tables, views, CREATE SQL) |
POST | /v1/rqlite/create-table | Create a table. Body: {schema: "CREATE TABLE ..."} |
POST | /v1/rqlite/drop-table | Drop a table. Body: {table: "name"} |
RQLite Native Backup/Restore
For full cluster-level backup and restore of the RQLite database:
| Method | Endpoint | Description |
|---|---|---|
GET | /v1/rqlite/export | Export a raw RQLite backup |
POST | /v1/rqlite/import | Import a RQLite backup |
SDK Method Reference
| Method | Signature | Description |
|---|---|---|
exec | exec(sql, args?) | Execute a write/DDL SQL statement |
query | query(sql, args?) | Execute a SELECT query |
find | find(table, criteria?, options?) | Find rows by criteria map |
findOne | findOne(table, criteria) | Find a single row by criteria |
createQueryBuilder | createQueryBuilder(table) | Create a fluent query builder |
repository | repository(table, primaryKey?) | Create an entity repository |
transaction | transaction(ops, returnResults?) | Execute multiple operations |
createTable | createTable(schema) | Create a table from DDL SQL |
dropTable | dropTable(table) | Drop a table |
getSchema | getSchema() | Get the database schema |
What tenant SQL may not do
Your namespace's RQLite sits behind a SQL filter (core/pkg/sqlguard), applied to the raw-database routes (/v1/rqlite/exec, query, transaction, select, find, create-table, drop-table) and to every database host function a serverless function calls. It exists because the platform's own tables once shared a schema with yours, and a statement could UPDATE a grant. They are now kept in the cluster registry, where your SQL cannot name them, and the filter closes the direct path in depth.
- +A statement that names a reserved platform table (keys, grants, sessions, nonces, operators, WireGuard peers, function secrets, deployments and their domains and history, the function and trigger tables,
namespaces,ipfs_content_ownership, and similar) is refused with403and{"code": "SQL_NOT_ALLOWED"}, whether the name is bare, quoted, bracketed, backticked, schema-qualified, or a string literal. A function that needs such text as data passes it as a bound argument. - +
ATTACH,DETACH,PRAGMA,VACUUMandCREATE TRIGGERare refused outright, and one call runs one statement. - +Some reserved names are generic. A table called
functionsin your own schema is refused too. - +
/v1/rqlite/exportand/v1/rqlite/import(the whole-database snapshot and load) are the owner's alone, and an imported image is checked first (Backups). - +On a namespace gateway nobody is exempt, owner and admin included: an admin who could write
api_keysdirectly would bypass the key-minting path and its scope checks. - +A function reaches only its own namespace's database. On the cluster gateway no function gets a database at all.
If you hit SQL_NOT_ALLOWED on a table you created yourself, rename it.