0020 xt.substrate Functional Slice Architecture
Purpose
xt.substrate is the event-driven backbone for cross-language data access in Greenways. It connects PostgreSQL domain functions, generated RPC and XTalk metadata, runtime transports, page models, and platform UIs.
PostgreSQL schema and functions
↓
postgres.gen
↓
generated RPC and typed XTalk interface data
↓
page or feature model
↓
xt.substrate nodes, spaces, sources, views, commands, and events
↓
JavaScript or Dart client library
↓
platform-specific UI
The portable client libraries are generated as linked package workspaces under
main/js/libs/xt-* and main/dart/libs/xt-*. JavaScript packages participate
directly in the main/js Yarn workspace; Dart packages participate in
main/dart/pubspec.yaml.
The Foundation namespace definitions and package generator remain the source of
truth for both languages.
The database defines durable behaviour. postgres.gen defines the cross-language contract. The page model defines application behaviour. xt.substrate distributes data and events. The UI renders page-model state and invokes page-model actions.
Repository source of truth
Inspect these implementations before changing the architecture:
backend/src/gwlink/backbone/remote_js.cljbackend/src/gwlink/backbone/xtdb_skeleton.cljbackend/src/gwbuild/app/sharedworker.cljref/foundation/src-lang/xt/db/node/runtime.cljbackend/src/gwlink/backbone/local_dart.cljbackend/src/gwlink/pages/account/settings_ui.cljref/foundation/src-lang/xt/ui/backend/src/gwlink/pages/template.clj- relevant
backend/src/gwdb/namespaces - relevant PostgreSQL generation and binding namespaces
Do not assume native PostgreSQL, PL/pgSQL, JavaScript, or Dart syntax is supported when Hara, std.lang, XTalk, or repository emitters define a narrower grammar.
Runtime topology
JavaScript browser
Browser applications use client/server shared-worker mode:
Browser UI
↓
Browser substrate client node
↓
SharedWorker transport
↓
SharedWorker substrate server node
├── remote PostgreSQL or Supabase source
├── SQLite WASM cache
├── session and draft state
└── event distribution to connected tabs
The emitted worker bundle is generic. After transport connection, the browser
connector sends the Greenways source configuration, schema, and lookup through
@xt.db/kernel-init. The shared worker then owns the long-lived database node,
caches, model registration, synchronisation, subscriptions, and event fan-out.
Browser clients remain thin.
JavaScript server and edge
Server and edge runtimes use the same logical model without the shared-worker hop. Runtime assembly changes, but model IDs, view IDs, command IDs, event names, payloads, and schemas remain stable.
Dart and mobile
Dart and Flutter embed the substrate node in the application process:
Flutter or Dart UI
↓
Dart xt.ui model store
↓
standalone Dart xt.db.node
├── model registry
├── PostgreSQL RPC access
├── cache and synchronisation
└── event routing
There is no webserver, JavaScript process, or transport hop between Flutter and the node. Dart uses the same generated descriptors, identifiers, schemas, page-model functions, and errors as JavaScript, while native Dart adapters provide HTTP, websocket, and SQLite capabilities.
Shared client contract
All platforms preserve the same logical operations:
connect
disconnect
ensureModel
queryView
invokeCommand
sourceGet
sourcePut
sourceRefresh
sourceSync
subscribe
unsubscribe
Host-language wrappers may be idiomatic, but identifiers, payloads, errors, correlation IDs, and event envelopes remain equivalent.
Delivering a slice
Implement each functional slice in this order:
- define the PostgreSQL model and invariants;
- define typed read and mutation functions plus access rules;
- inspect emitted PostgreSQL;
- generate RPC metadata and typed schemas;
- generate XTalk interface data through
postgres.gen; - define the platform-neutral page or feature model;
- test model queries, commands, drafts, events, refresh, sync, and failure recovery without UI;
- validate JavaScript shared-worker and server modes;
- validate Dart protocol compatibility;
- implement web and mobile presentation.
Page model and UI boundary
The page model owns data requirements, loading, validation, commands, drafts, refresh, events, errors, capabilities, and valid state transitions.
The UI owns layout, typography, responsive behaviour, widgets, animation, accessibility presentation, navigation controls, and input components.
A UI should consume a stable state shape such as:
status
data
draft
errors
capabilities
actions
It should not know table names, Supabase routes, SQLite internals, RPC argument encoding, shared-worker message formats, or transport details.
xt.ui application boundary
xt.ui is the generic application/UI layer above substrate. Its namespaces
have distinct responsibilities:
xt.ui.state.*owns headless state, actions, forms, collections, CRUD, sessions, feedback, diagnostics, and substrate projection;xt.ui.widgets.*owns reusable semantic widget contracts;xt.ui.frames.*owns optional reusable compositions such as operational forms and tables;- native React/Next.js and Flutter/Dart code owns framework lifecycle, routing, navigation, native SDKs, and bespoke screens.
State modules never construct UI nodes. Frames consume plain state and actions and must allow region overrides or complete native replacement.
Use feature stores by default for engagement-heavy or multi-screen behavior. Use generated page controllers for platform-neutral superuser and table-heavy CRUD interfaces. Shared identifiers, schemas, errors, commands, events, and required state transitions remain equivalent across JS and Dart, while routes and rendered screens may differ.
See architecture guide 0030 for the generation boundary and extension flow.
Queries, commands, and events
A query reads repeatable state. A command requests a durable change through a generated PostgreSQL function. An event announces a completed or observed change and may refresh a source, invalidate a view, update a cache, notify another space, or recompute derived state.
Use domain-oriented identifiers:
model: account/profile
space: app/account
view: account/profile
command: account/update-profile
event: account/profile-updated
Cache and synchronisation
Use a clear hierarchy:
remote authoritative source
↓
local cache
↓
session state
↓
draft state
↓
derived page state
Use refresh to reread authority, sync to reconcile remote and cached state, and direct source updates only for model-owned state such as drafts or local preferences. Durable mutations still pass through typed commands.
Validation
A slice is complete only when database permissions, emitted SQL, generated contracts, model behaviour, transports, Dart compatibility, and UI separation have been validated. Generation success alone is not sufficient.