Vantage UI

Features

Use Vantage to build your own data-heavy applications. Vantage stores its project files in a folder, constantly monitoring them for changes — so the app can be easily modified from the outside.

Your app = structure + code

Menu items, page layout and widget types are described in YAML. Custom logic and behaviour live in Rhai. The project folder also ships .skills that teach your AI coding agent to write both.

YAML — structure
# table/clients.yaml
title: Clients
source: main_db
columns: [name, email, balance]
has_many:
  - orders
context_menu:
  - title: View orders
    open_page: orders
    model: model.get_ref("orders", row.id)
Rhai — logic
// a computed label for each row
let label = row.name;
if row.balance < 0 {
    label += "  •  owes " + row.balance;
}
label

// drill two hops, in one expression
model.get_ref("clients", row.id)
     .get_ref("orders", client_id)

When a file changes on disk, Vantage re-reads it and the UI updates live.

Tables (or Models) and DataSources

Vantage needs a DataSource to read data. Each DataSource has a unique set of capabilities. Once a connection is established, Vantage reads tables to learn about columns, record types and relations. Enterprise builds can also define strongly typed tables in Rust.

CSV files SQLite PostgreSQL MySQL SurrealDB MongoDB REST API GraphQL CLI command Kubernetes AWS Sample data In-memory tables

SurrealDB

Read / write CBOR

Full querying, aggregation, pagination, related columns and dataset-level traversal, with relations you can define in Rhai. Tables update themselves when data changes, through LIVE queries — nothing to set up.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) check_circle
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") check_circle
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") check_circle
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) check_circle
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) check_circle
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. check_circle

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") check_circle
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) check_circle
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") check_circle
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } check_circle
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) check_circle
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. check_circle
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. check_circle
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. check_circle
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) check_circle
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) check_circle
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") check_circle
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) check_circle
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) check_circle
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); check_circle
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) check_circle

SQLite

Read / write Native (sqlx)

Full SQL driver via sqlx — sort, search, aggregation, pagination, subquery traversal and columns from related tables.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) check_circle
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") check_circle
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") check_circle
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) check_circle
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) check_circle
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. check_circle

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") check_circle
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) ~Partly supported From Rust (Table::with_expression), or as a column of a read-only rhai: / base: table. There is no expr: column in SQL YAML yet.
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") ~Partly supported From Rust (Table::with_expression), or as a column of a read-only rhai: / base: table. There is no expr: column in SQL YAML yet.
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } check_circle
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) check_circle
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) check_circle
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") check_circle
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) check_circle
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) check_circle
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); check_circle
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) check_circle

What stands out

  • One file on disk — no server to run or credentials to manage.
  • Related columns become correlated subqueries, so client.name sorts and filters like any column.
  • Views built from a Rhai query, or derived from another table with base:, taking arguments from the page.
  • JSON columns open as their own editable tables.

PostgreSQL

Read / write Native (sqlx)

Full SQL driver via sqlx — sort, search, aggregation, pagination, subquery traversal and columns from related tables. The framework can also read live changes from a NOTIFY trigger you install; turning that on from the app is next.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) check_circle
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") check_circle
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") check_circle
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) check_circle
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) check_circle
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. check_circle

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") check_circle
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) ~Partly supported From Rust (Table::with_expression), or as a column of a read-only rhai: / base: table. There is no expr: column in SQL YAML yet.
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") ~Partly supported From Rust (Table::with_expression), or as a column of a read-only rhai: / base: table. There is no expr: column in SQL YAML yet.
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } check_circle
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. ~Partly supported Needs a LISTEN/NOTIFY trigger you install, and the app doesn't switch it on yet. Changes arrive as "re-read the table", without the changed row.
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) check_circle
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) check_circle
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") check_circle
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) check_circle
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) check_circle
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); check_circle
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) check_circle

What stands out

  • Related columns become correlated subqueries, so client.name sorts and filters like any column.
  • Views built from a Rhai query, or derived from another table with base:, taking arguments from the page.
  • JSON columns open as their own editable tables.
  • The framework reads live changes from a LISTEN/NOTIFY trigger you install.

MariaDB

Read / write Native (sqlx)

Full SQL driver via sqlx — sort, search, aggregation, pagination, subquery traversal, columns from related tables, transactional imports and relations scripted in Rhai. Tested on MariaDB 11; the same driver runs MySQL 8.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) check_circle
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") check_circle
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") check_circle
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) check_circle
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) check_circle
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. check_circle

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") check_circle
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) ~Partly supported From Rust (Table::with_expression), or as a column of a read-only rhai: / base: table. There is no expr: column in SQL YAML yet.
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") ~Partly supported From Rust (Table::with_expression), or as a column of a read-only rhai: / base: table. There is no expr: column in SQL YAML yet.
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } check_circle
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) check_circle
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) check_circle
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) check_circle
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") check_circle
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) check_circle
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) check_circle
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) check_circle
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); check_circle
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) check_circle

What stands out

  • Related columns become correlated subqueries, so client.name sorts and filters like any column.
  • Bulk imports run in one transaction, match ids the way the column's collation does, and fire the same hooks as a single insert.
  • Views built from a Rhai query, or derived from another table with base:, taking arguments from the page.
  • modify: scripts add SQL conditions that YAML can't express: self.with_condition(ident("total") > 100).
  • JSON columns open as their own editable tables.
  • No live updates: MariaDB has no push channel short of reading the binlog.

MongoDB

Read / write BSON

Filters, sort and search push down to the server, including nested fields. Related columns run as $lookup joins, read-only views come from aggregation pipelines in YAML, and edits keep ObjectId, date and decimal types. On a replica set, tables update live through change streams and bulk imports run in one transaction.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) check_circle
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") check_circle
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") check_circle
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) check_circle
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) check_circle
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. check_circle

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") ~Partly supported Matches with in against the related ids, fetched first — not as a subquery.
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } check_circle
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) check_circle
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. ~Partly supported Needs a replica set: change streams don't run on a standalone server.
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. ~Partly supported Needs a replica set: change streams don't run on a standalone server.
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. ~Partly supported Needs a replica set: change streams don't run on a standalone server.
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) check_circle
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) check_circle
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") check_circle
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) ~Partly supported Needs a replica set: the import runs in a transaction.
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) check_circle
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); check_circle
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) check_circle

What stands out

  • On a replica set, tables update live through change streams — edits from any client appear in place.
  • Read-only views from an aggregation pipeline written in YAML, or built by a Rhai script.
  • client.name columns joined server-side with $lookup, several hops deep.
  • Nested fields as columns — nested_path: address.city — searchable and filterable like any other.
  • Embedded objects and arrays open as their own editable tables.
  • Edits keep ObjectId, date and decimal types; datetime and decimal columns check what you type.
  • Bulk imports run in one transaction and fire the same hooks as a single insert.
  • Rhai filters are plain maps: self.with_condition(#{price: #{"$gt": 100}}).

DynamoDB

Read-focused JSON

Key and scan-filter queries with cursor pagination. Often paired as a read source with writes routed elsewhere.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) remove
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") remove
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") remove
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) remove
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) remove
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. check_circle

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") remove
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } remove
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } remove

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) remove
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) remove
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") remove
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) remove
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); remove
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) remove

GraphQL

Read-only JSON

Generic and Hasura dialects. Read rows, filter, and drill into related records. With Hasura, sort, search and operator filters run on the server.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) ~Partly supported Hasura dialect only.
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") ~Partly supported Hasura dialect only.
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") ~Partly supported Hasura dialect only.
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) remove
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) ~Partly supported limit / offset are rendered when the table sets paginate: true, but the grid doesn't page with them yet.
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. remove

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") remove
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } remove
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) remove
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) remove
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") remove
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) remove
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); remove
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) remove

REST APIs

Read-only JSON

REST endpoints as tables. Filters, sort and paging map onto the URL as configured per API, and work when the API honours them.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") ~Partly supported Equality only, as a URL path segment or ?field=value; the API has to honour it. Can be set to filter in the app instead.
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) remove
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") ~Partly supported When the API has a sort parameter (OrderingParams); one column at a time.
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") remove
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() ~Partly supported Exact when the API reports a total (total_key); otherwise the rows are fetched and counted.
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) remove
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) ~Partly supported When page/limit or skip/limit parameters are configured and the API supports them.
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. remove

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") remove
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } remove
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) remove
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) remove
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") remove
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) remove
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); remove
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) remove

AWS

Read-only JSON

Infrastructure (CloudWatch, IAM, S3 and more) browsed as tables, with equality filters pushed down to the API.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) remove
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") remove
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") remove
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) remove
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) remove
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. remove

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") remove
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } remove
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } remove

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) remove
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) remove
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") remove
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) remove
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); remove
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) remove

CLI tools

Read-only JSON

Wrap a command (aws, kubectl, gh…) and read the JSON it prints as rows.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") remove
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) remove
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") remove
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") remove
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) remove
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) remove
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. remove

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") remove
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") remove
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } remove
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) remove
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) remove
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") remove
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) remove
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); remove
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) remove

CSV files

Read-only Typed text

Local files as tables, with in-memory filtering and record-level traversal.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) remove
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") remove
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") remove
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) remove
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) remove
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. remove

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") remove
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } remove
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) remove
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) remove
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") remove
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) remove
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); remove
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) remove

Append logs

Append-only JSONL

Append-only JSONL log files — write structured entries that the log viewer picks up on its next refresh.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") remove
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) remove
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") remove
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") remove
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() remove
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) remove
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) remove
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. remove

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") remove
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") remove
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } remove
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. remove
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. remove
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. remove
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) check_circle
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) remove
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") remove
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) remove
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) remove
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); remove
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) remove

Memory

Read / write In-process

Tables held in the app's own memory — what Faker simulations and tests run on. Every operator, sort, search, paging, atomic import and live updates, with no server.

Retrieving data

Filter by conditionsFilter by conditions Narrow the available table rows by applying one or more conditions.table("order").where("status", "open") check_circle
Conditional operatorsConditional operators Non-exact matching with basic operators such as >, < or !=. Full support also needs matching against a set of values (in) and fuzzy text matching (contains).table("order").where("total", ">=", 100) check_circle
Server-side sortServer-side sort The server re-arranges the results, respecting each column's data type, ascending or descending, on several columns in a set priority.table("order").sort("total", "desc") check_circle
Quick searchQuick search A text search across several columns, performed on the server, so it can cover the entire table.table("client").search("smith") check_circle
CountCount The server can say exactly how many rows remain in the set once the conditions are applied.table("order").where("status", "due").count() check_circle
Column aggregatesColumn aggregates Aggregate functions applied to a column of a filtered set. The backend supports at least some of them, such as max, avg, median or a string concatenation (group_concat).select().from("order") .expression(avg(ident("total")).alias("avg_total")) .expression(max(ident("total")).alias("max_total")) check_circle
Random-access pagesRandom-access pages Skip any number of rows in a filtered set without a performance penalty, and request any number of records up to a maximum.select().from("order") .order_by(ident("id"), "asc") .limit(50, 1000) check_circle
Progressive (cursor) loadProgressive load A fall-back way to paginate: pages are sequential, and the next page can be fetched only after the previous one. remove

Relations

One-to-many relationsOne-to-many relations Only needs a filter by condition whose value comes from a record loaded from another table — in the same data source or a different one.table("client").where("id", "c1").ref("orders") check_circle
Many-to-many relationsMany-to-many relations A condition can be an expression over another set, such as a subquery or a join. Partial support when the in operator can match against a set of values.table("client").where("country", "UK").ref("orders") ~Partly supported Matches with in against the related ids, fetched first — not as a subquery.
Relation aggregationRelation aggregation A column can be defined as an aggregate of a field or expression in a related table, calculated on the server. The classic example is account.balance as the sum of transaction.amount.// invoice.paid: the sum of its payments coalesce( select().value().from("payment") .expression(sum(ident("amount"))) .where(ident("invoice") == parent("id")) .group_all().subquery()[0], 0) remove
Arbitrary expressionsArbitrary expressions Per-row operations on the server, over stored values or relation aggregates — for example invoice_line.total as qty * item.price.mul(ident("qty"), ident("price")).alias("total") remove
Column from related tableColumn from related table Import a field through a one-to-one relation, such as country_id.name, as a read-only column.columns: client.name: { type: string } remove
Contained record expressionsContained record expressions A condition can iterate over sub-records stored inside a record — invoices whose JSON lines field contains item 123.self.with_condition(#{"lines.item_id": 123}) remove
Relations defined in RhaiRelations defined in Rhai Executes arbitrary code after a row is retrieved from the table, in the context of the entire row — a Rhai lazy: script in YAML, or a Rust closure that can also do I/O.badge: lazy: | if row.vip { "★ " + row.name } else { row.name } check_circle

Live updates

Record changedRecord changed An open record updates in place when someone else changes it — the source pushes the change, no polling. check_circle
Record set changedRecord set changed A filtered grid updates when rows matching its condition change, and a row that stops matching leaves the grid. check_circle
Table changedTable changed Any insert, update or delete in the table reaches open grids as it happens. check_circle
Related record changedRelated record changed A change in a related table — an invoice_line — reaches the parent's view, such as invoice.total. remove

Editing data

Add recordsAdd records Create rows from a form dialog. In a filtered view, new rows are filled in to belong to it.table("order") .insert(#{ id: "o10", client: "c3", total: 55 }) check_circle
Update recordsUpdate records Edit rows in a form dialog. A write through a filtered view can never change a row outside it.table("order").patch("o1", #{ status: "paid" }) check_circle
Delete recordsDelete records Remove rows, after a confirmation step, from a grid or a record view.table("order").where("status", "paid").delete("o2") check_circle
Atomic importAtomic import A CSV or wizard import lands in one transaction: either every new row is stored or none is, and existing rows are left untouched.let paid = table("order").where("status", "paid"); table("archive").import_from(paid) check_circle
Bulk modifyBulk modify Change every row of a filtered set in one operation — mark all open orders as shipped — without loading them first. remove

Custom tables

Tables from custom queriesTables from custom queries A table can be defined by a query instead of a stored table — a Rhai script or a native query — and is read-only.select().from("order") .inner_join("client", "c", expr("c.id = order.client_id")) remove
Table aggregateTable aggregate A table defined through another table — sales grouped by month, built on orders — inheriting its columns and relations.name: category_totals id_column: category_id columns: total_price: { type: int } sqlite: base: product inherit: columns: [category_id] rhai: | base.clear_fields().field("category_id") .expression(expr("SUM(price) AS total_price")) .group_by(expr("category_id")) remove
Server-side actionsServer-side actions An action runs inside the data source, such as a stored procedure or function, instead of in the app. remove

Misc

Auto-IDAuto-ID When the id column is flagged auto, an insert without an id asks the data source to assign one — auto-increment, a generated record id, an ObjectId — and returns it. Without the flag the app mints a UUIDv7 instead, so retries stay idempotent.# order.yaml columns: id: { type: int, flags: [id, auto] } // Rhai — no id, so the data source makes one let id = table("order") .insert(#{ client: "c2", total: 30 }); check_circle
IdempotencyIdempotency By default, every write can be retried safely: a repeated insert or delete changes nothing, and writes through a filtered view stay inside it. An insert that relies on Auto-ID is the exception — retrying it adds a second row.// run twice: same result table("order") .upsert("o10", #{ client: "c3", total: 60 }) check_circle

What stands out

  • Faker simulations write into it on a schedule, so grids, charts and logs have moving data to show.
  • Live updates are scoped to the vista's query: a row that stops matching arrives as a delete.
  • No server, no files — the data lasts as long as the app runs.

Every GUI element adapts to the table it's bound to — buttons and features follow the backend's capabilities.

Build a rich GUI — not just CRUD

In Vantage, UI elements are reactive. YAML controls layout and visibility, while Rhai drives behaviour. The data grid is a typical example — YAML sets column order, types, widths and captions, plus the buttons above the grid; Rhai scripts what a button press does.

Data grids

  • arrow_rightVirtualized — stays smooth past a million rows.
  • arrow_rightPersistent sort, quick search and column resize, per project.
  • arrow_rightBackground refresh with no flicker, on change or a schedule.

Tabs & drill-downs

  • arrow_rightOpen related records in child tabs; pin the ones you keep.
  • arrow_rightChain multi-hop drill-downs from a row's context menu.
Forms, dialogs & actions

Forms, dialogs & actions

  • arrow_rightAdd and edit records in form dialogs; confirm destructive steps.
  • arrow_rightRight-click actions run operations or call your own services.
  • arrow_rightStatus workflows offer only the valid next transitions.

Wizards

  • arrow_rightMulti-step flows for guided data entry and operations.
  • arrow_rightCollect input across screens, then commit in one go.
  • arrow_rightFully scripted interactions — full access to all tables.
  • arrow_rightA reactive UI to display progress bars or logs.

Charts & dashboards

  • arrow_rightStacked bars, lines, pies and KPI cards on one dashboard grid.
  • arrow_rightFilter dropdowns narrow the query itself, so every figure recomputes.
  • arrow_rightDeltas colour by meaning — a rising cost reads red, not green.

Custom UI elements

  • arrow_rightReactively bind to tables.
  • arrow_rightKanban, maps and many other custom UI widgets.
  • arrow_rightAllow scripting and custom actions.

Vantage for the Enterprise

As an enterprise client, you get a custom release pipeline with your own configuration of Vantage. Supply your own DataSources, add table definitions in native Rust, and bundle your own actions.

Capability Free download One binary, everyone Enterprise build Assembled for you
Build your app
Datasources out of the box 12 12 + your own
Colour themes 36 36 + one that's yours
Native types mapped per source up to 24 + types you define
Grids past a million rows check_circle check_circle
Charts, KPI cards, dashboards check_circle check_circle
Multi-step wizards & CSV import check_circle check_circle
Custom form layouts check_circle check_circle
Auto-refresh in the background check_circle check_circle
Push-style instant refresh SurrealDB, MongoDB + CDC: Kafka, Debezium, custom listeners
Your AI agent builds it over MCP check_circle check_circle
MCP debugging — query preview, error check check_circle + BDD suite, run at build
Custom tables
Custom queries & aggregates Rhai Rhai + Rust
Actions Rhai Rhai + Rust
Internal API Rhai Rhai + Rust
Custom datasourcesopen_in_new OSS, communityopen_in_new yours or ours
UI widgets 21 built in + your own in Rust
Third-party WASM plugins remove check_circle
Internal distribution control
Application distribution vantage-ui.com, or clone by hand internal catalog, apps auto-update
Platforms macOS, Linux + API/MCP
Verifiable builds, on demand remove your components in the binary
Your name, icon and About box remove check_circle
Turn off what you don't need remove check_circle
Your own release channel remove signed & notarised
vantage:// install allowlist remove your apps only
Central credentials & SSO sign-in remove check_circle
Crash telemetry optional your own Sentry account
What you're standing on
MIT framework underneath forever forever
Architecture partner & training remove check_circle
Support community Slack Connectopen_in_new
Price for personal and commercial use $0 forever talk to us

On the roadmap

Where Vantage is heading. Everything we ship reaches every build — free and enterprise alike. Tap the deck to flip through.

Roadmap desktop_windows

Windows

macOS and Linux today. A signed Windows build brings the same app to the laptops most of your colleagues actually use — no VM, no remote desktop.

Roadmap language

The web

The same YAML and Rhai, rendered in a browser. Hand someone a link instead of an install, and put a screen in front of people who will never open a terminal.

Roadmap api

Server-side facade APIs

Turn the same config — data sources plus Rhai logic — into real backend APIs your own frontend or mobile app can call. Start in the app; graduate to code.

Roadmap deployed_code

Export to real code

No lock-in: export your app to a code repository or container image you own and run anywhere. Start in low-code, leave with real code.

touch_app Click to deal the next card

download Download for Mac — free apps See example apps