Skip to main content

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.clj
  • backend/src/gwlink/backbone/xtdb_skeleton.clj
  • backend/src/gwbuild/app/sharedworker.clj
  • ref/foundation/src-lang/xt/db/node/runtime.clj
  • backend/src/gwlink/backbone/local_dart.clj
  • backend/src/gwlink/pages/account/settings_ui.clj
  • ref/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:

  1. define the PostgreSQL model and invariants;
  2. define typed read and mutation functions plus access rules;
  3. inspect emitted PostgreSQL;
  4. generate RPC metadata and typed schemas;
  5. generate XTalk interface data through postgres.gen;
  6. define the platform-neutral page or feature model;
  7. test model queries, commands, drafts, events, refresh, sync, and failure recovery without UI;
  8. validate JavaScript shared-worker and server modes;
  9. validate Dart protocol compatibility;
  10. 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.