Think in tuples. Storage is a detail.
XDB stores data as tuples: a path, an attribute, and a typed value. Read and write them from Go or a pipe-friendly CLI, and keep them in memory, files, Redis, or SQLite. The CLI is easy for humans and exact for agents.
Read about the motivation behind XDB in Introducing XDB.
For in-depth documentation on each concept, see docs/concepts.
The XDB data model is a tree of Namespaces, Schemas, Records, and Tuples.
┌─────────────────────────────────┐
│ Namespace │
└────────────────┬────────────────┘
↓
┌─────────────────────────────────┐
│ Schema │
└────────────────┬────────────────┘
↓
┌─────────────────────────────────┐
│ Record │
└────────────────┬────────────────┘
↓
┌─────────────────────────────────┐
│ Tuple │
├─────────────────────────────────┤
│ Path | Attr | Value │
└─────────────────────────────────┘
A Tuple is the fundamental building block in XDB. It has three parts:
- Path: the namespace, the schema, and the ID of the record. Together they identify the record.
- Attr: a string that identifies the attribute. It supports dot-separated nesting.
- Value: the value of the attribute.
One or more Tuples with the same path (NS + Schema + ID) make up a Record. The ID alone does not group tuples. The full path does. A record is its tuples. It adds no data of its own. When at least one tuple exists at a path, the record at that path exists.
This is the tuple-first framing of XDB: the tuple is the primitive, and every larger structure is built from tuples. A record is similar to an object, a struct, or a row in a database. It usually represents one entity of domain data.
A Namespace (NS) groups one or more Schemas. Namespaces usually organize schemas by domain, application, or tenant.
A Schema defines the structure of records and groups them together. Declared fields always type-check. A schema has one of three modes, and the mode governs only undeclared fields. strict (the default) rejects undeclared fields. flexible accepts undeclared fields as-is. dynamic infers undeclared fields and adds them to the schema. Each schema has a unique name within its namespace.
XDB URIs are valid Uniform Resource Identifiers (URI) according to RFC 3986. A URI identifies one resource in XDB.
The general format of a URI is:
[SCHEME]://[DOMAIN] [ "/" PATH] [ "?" QUERY] [ "#" FRAGMENT]
XDB URIs have this format:
xdb://NS [ "/" SCHEMA ] [ "/" ID ] [ "#" ATTRIBUTE ]
xdb://com.example/posts/123-456-789#author.id
└─┬──┘└────┬────┘└──┬─┘└─────┬─────┘└─────┬─────┘
scheme NS SCHEMA ID ATTRIBUTE
└───────────┬───────────┘
path
The components of the URI are:
- NS: the namespace.
- SCHEMA: the schema name.
- ID: the unique identifier of the record.
- ATTRIBUTE: the name of the attribute.
- path: NS, SCHEMA, and ID together. The path identifies one record (the URI without
xdb://).
Valid examples:
Namespace: xdb://com.example
Schema: xdb://com.example/posts
Record: xdb://com.example/posts/123-456-789
Attribute: xdb://com.example/posts/123-456-789#author.id
| Type | SQLite | Description |
|---|---|---|
string |
TEXT |
UTF-8 string |
integer |
INTEGER |
64-bit signed integer |
unsigned |
INTEGER |
64-bit unsigned integer |
float |
REAL |
64-bit floating point |
boolean |
INTEGER |
True or false |
time |
INTEGER |
Date and time in UTC |
json |
TEXT |
Arbitrary JSON data |
bytes |
BLOB |
Binary data |
array |
TEXT |
Array of typed values |
go install github.com/xdb-dev/xdb/cmd/xdb@latest
xdb init # creates the config and starts the daemonThe xdb CLI is a small, regular language for reading and writing data. Every invocation has the same shape:
xdb <resource> <action> <URI> [--filter CEL] [--fields MASK] [--json|--file|-] [-o FMT]
One grammar, one predicate language, and one output protocol apply to every resource. Agents learn the grammar once. Humans get shorthand on top.
| Primitive | Purpose | Example |
|---|---|---|
resource |
What you operate on: records, schemas, namespaces |
records |
action |
Closed set: get, list, create, update, upsert, delete |
upsert |
| URI | The noun: xdb://NS/SCHEMA/ID#ATTR. The depth selects the resource. |
xdb://com.example/posts/p-1 |
--filter |
CEL predicate (AIP-160) | --filter 'status == "published"' |
--fields |
Projection (field mask) | --fields _id,title |
| payload | JSON from --json, --file, or stdin - |
--json '{"title":"Hello"}' |
-o |
Output format: json, ndjson, table, yaml |
-o json |
Namespaces support only get and list. xdb watch <URI> is a top-level command, not an action. It streams change events as NDJSON.
One URI, walked through the full action set:
# Define the schema
xdb schemas create xdb://com.example/posts --json '{"fields":{"title":{"type":"string"}}}'
# Write data
xdb records create xdb://com.example/posts/p-1 --json '{"title":"Hello"}'
xdb records update xdb://com.example/posts/p-1 --json '{"title":"Updated"}'
xdb records upsert xdb://com.example/posts/p-1 --json '{"title":"Full replace"}'
# Read data
xdb records get xdb://com.example/posts/p-1 --fields title
xdb records list xdb://com.example/posts --filter 'title.contains("Hello")' --fields _id,title --limit 10
# Delete data
xdb records delete xdb://com.example/posts/p-1 --forcecreate fails with ALREADY_EXISTS if the record exists. update is a patch: only the fields in the payload change. upsert replaces the whole record.
describe introspects every part of the CLI:
xdb describe records.create # action signature
xdb describe Record # type definition
xdb describe --uri xdb://ns/schema # live data schema
xdb describe --actions # action × resource matrix
xdb describe --filter # CEL operators and functions
xdb describe --errors # error code catalog
xdb describe --value-types # supported value typesCommands compose through stdin, stdout, and one error shape.
Stdin - is the explicit pipe token. Every command that takes a URI or a payload accepts - to read it from stdin:
echo '{"title":"t"}' | xdb records create xdb://com.example/posts/p-1 -
echo '{"op":"records.upsert","uri":"xdb://com.example/posts/p-2","data":{"title":"t2"}}' \
| xdb batch - # one {"op":"...","uri":"...","data":{...}} operation per lineErrors are structured. Every error, in every format, has the same shape:
{
"code": "NOT_FOUND",
"message": "record not found",
"resource": "records",
"action": "get",
"uri": "xdb://...",
"hint": "try xdb records list xdb://..."
}The output format is a table on a TTY and JSON on a pipe. Override it with -o:
xdb records get xdb://com.example/posts/p-1 # table (TTY)
xdb records get xdb://com.example/posts/p-1 | jq . # JSON (pipe)
xdb records get xdb://com.example/posts/p-1 -o yaml # explicitThe URI depth dispatches to the right resource. These commands are macros. Each one expands to the full form:
| Shorthand | Expands to |
|---|---|
xdb get <uri> |
records/schemas/namespaces get (by URI depth) |
xdb ls [uri] |
records/schemas/namespaces list. Without a URI, it lists namespaces. |
xdb put <uri> |
records upsert (record URI only) |
xdb rm <uri> |
records/schemas delete (requires --force) |
xdb make-schema |
schemas create |
Use the full form in scripts and agent instructions. Use the shorthand at an interactive shell.
--config,-c: path to the config file (default~/.xdb/config.json)--output,-o: output format (json,ndjson,table,yaml)--verbose,-v: enable verbose logging--debug: enable debug logging
The daemon runs a JSON-RPC server that handles all operations. The CLI is a thin client. xdb init starts the daemon. Most users never run the daemon commands directly.
xdb daemon start
xdb daemon status
xdb daemon stop
xdb daemon restartFor the full grammar reference, with the action × resource matrix, the error codes, and the agent guidance, see cmd/xdb/cli/CONTEXT.md.
XDB reads a JSON config file at ~/.xdb/config.json. If the file does not exist, xdb init and xdb daemon start create it with defaults. If the file is missing, every other command uses the same defaults in memory.
The default config, as xdb init writes it:
{
"dir": "~/.xdb",
"daemon": {
"socket": "xdb.sock"
},
"log_level": "info",
"store": {
"backend": "sqlite"
}
}- sqlite (default): a SQLite database file, at
<dir>/data/xdb.dbby default. Keys understore.sqlite:path,journal,sync,cache_size,busy_timeout. - memory: an in-memory backend. When the daemon stops, the data is lost.
- fs: a filesystem backend, under
<dir>/databy default. Key understore.fs:dir. - redis: a Redis server.
store.redis.addris required. Optional keys understore.redis:password,db.
Example with SQLite:
{
"store": {
"backend": "sqlite",
"sqlite": {
"path": "/var/lib/xdb/xdb.db",
"journal": "wal"
}
}
}Example with Redis:
{
"store": {
"backend": "redis",
"redis": {
"addr": "localhost:6379"
}
}
}Run xdb describe --config for the full reference of all config keys and their defaults.