xt.db + xt.substrate WalkthroughThis is a GitHub-flavored explainer for how xt.db is used inside
xt.substrate to build application state for an admin-style SPA screen.
The main target shape is:
The examples mirror these walkthrough tests:
test-lang/xt/db/walkthrough/guide_00_model_basic_test.cljtest-lang/xt/db/walkthrough/guide_02_application_flow_test.cljtest-lang/xt/db/walkthrough/guide_09_local_sqlite_remote_postgres_test.cljtest-lang/xt/db/walkthrough/guide_10_remote_sqlite_postgres_pipeline_test.cljxt.substrate gives you the node, spaces, and request/response transport.xt.db gives you models, views, query planning, cached results, and refresh."room/local" or
"room/server".The smallest useful shape is a node with one space and one model representing an admin screen with two views on the same table.
flowchart LR
N[substrate node]
S[space: room/a]
M[model: orders-screen]
V1[view: detail]
V2[view: list]
N --> S
S --> M
M --> V1
M --> V2
At this stage, think of xt.db as a reactive model registry:
The guide-00 walkthrough does exactly that.
(var node (event-node/node-create {"id" "node-a"}))
(model/install node
{"schema" ...
"lookup" ...
"views" {}})
(model/model-put node
"room/a"
"orders"
{"views"
{"detail" {"query" ... "input" [selected-id]}
"list" {"query" ... "input" ["open"]}}})
That gives you:
"room/a""orders-screen""detail" and "list"Conceptually, a view looks like this:
{"query" ...
"input" []
"status" "idle"
"value" nil
"query_key" nil}
After a refresh, the same cell becomes:
{"status" "ready"
"value" [...]
"query_key" "..."}
So even before SQL is involved, the important idea is:
xt.dbgives you named live cells inside a space.
Because the model is the application shape, while the DB is only a backing execution engine.
flowchart TD
A[Application shape] --> B[space]
A --> C[model]
A --> D[view definitions]
E[Backing storage] --> F[db.cache]
E --> G[db.sql]
B --> H[space state]
C --> H
D --> H
F --> H
G --> H
This is the substrate-style part of the design:
That makes it possible to:
Now split the app into two spaces:
"room/server" = authoritative application state"room/local" = client-facing stateflowchart LR
L[space: room/local]
V[local view: entries]
R[space: room/server]
M[server model: entries]
L --> V
V -- remote refresh --> R
R --> M
The server model is defined normally:
(model/model-put node
"room/server"
"entries"
(@! fixtures/+model-spec+))
The local model is only a proxy view:
(model/model-put node
"room/local"
"entries"
{"views"
{"entries"
{"query" (@! fixtures/+model-query+)
"input" []
"remote" {"space" "room/server"}}}})
This is the key difference:
model-put defines the actual source modelmodel-put defines a client cell that refreshes from a remote spaceNo SQL detail is required to understand this step. It is still just:
Once the application shape is stable, attach DBs to spaces.
In the walkthrough:
"room/server" uses Postgres db.sql"room/local" uses SQLite db.sqlflowchart LR
subgraph Client
LS[room/local]
LV[view: entries]
LDB[(SQLite db.sql)]
end
subgraph Server
RS[room/server]
RM[model: entries]
RDB[(Postgres db.sql)]
end
LS --> LV
LS --> LDB
LV -- substrate request --> RS
RS --> RM
RS --> RDB
RS -- response --> LV
LV -- cached locally --> LDB
The important point is that spaces own DB attachments:
(xt/x:set-key (model/ensure-space-state node "room/server")
"db"
server-db)
(xt/x:set-key (model/ensure-space-state node "room/local")
"db"
local-db)
So the view definition does not need to know whether it is backed by:
It only needs to know:
This is the practical application flow used by the walkthrough.
sequenceDiagram
participant UI as App / UI
participant Local as room/local view
participant Node as xt.substrate node
participant Server as room/server model
participant PG as Postgres
participant SQLite as local SQLite
UI->>Local: view-refresh("entries")
Local->>Node: remote request for room/server
Node->>Server: run server view
Server->>PG: execute query
PG-->>Server: rows
Server-->>Node: result
Node-->>Local: remote response
Local->>SQLite: cache refreshed value
Local-->>UI: status=ready, value=[...]
What the application sees is much simpler:
xt.dbview-refreshview-get / view-valUsing SQLite for "room/local" makes the client cache explicit.
That gives you a nice separation:
This is useful because the local space can:
If you only remember one diagram, use this one:
flowchart LR
A[App] --> B[room/local view cell]
B --> C{local or remote?}
C -->|local| D[local db]
C -->|remote| E[room/server view]
E --> F[server db]
And if you only remember one sentence, use this:
xt.substratemoves requests between spaces, andxt.dbturns those spaces into live query cells.
guide_00_model_basic_test.cljUse this when you want to understand:
This is the best place to learn the local admin-screen mental model.
guide_02_application_flow_test.cljUse this when you want to understand:
This is the best place to learn the remote admin-screen topology.
guide_09_local_sqlite_remote_postgres_test.cljUse this when you want to test the exact implementation split:
This is the best place to learn the dedicated SQLite-local / Postgres-remote implementation path.
guide_10_remote_sqlite_postgres_pipeline_test.cljUse this when you want to test the direct remote view pipeline:
model/view-refresh or model/model-refresh on the local spacerun-view-remote / run-remote-queryThis is the best place to learn the true SQLite-local / Postgres-remote query pipeline.
guide_04_view_lifecycle_test.clj through guide_08_local_db_options_test.cljUse these when you want to understand the supporting behavior behind that same screen shape:
db.cache vs SQLite for the local admin screenguide_00_model_basic_test.cljguide_02_application_flow_test.cljguide_09_local_sqlite_remote_postgres_test.clj for the explicit local-sqlite / remote-postgres setupguide_10_remote_sqlite_postgres_pipeline_test.clj for the direct remote query pipeline between SQLite-local and Postgres-backed spacesguide_04 through guide_08 for the supporting screen mechanicsThat order matches how application developers usually think:
xt.db is best understood as a reactive cell layerxt.substrate is the space/transport/runtime layerCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |