HighX
Server

Schema

Tables, rows, and the indexes queries are allowed to use.

A schema declares your tables and their indexes. It is handed to the tenant object, and the reactive engine reads it to decide which subscriptions a write affects.

import { defineSchema, defineTable, schema } from "@highx/core";

const noteRow = schema((input: unknown): Record<string, unknown> => {
  if (typeof input !== "object" || input === null) throw new Error("Expected a note");
  return { ...input };
});

export const databaseSchema = defineSchema({
  notes: defineTable("notes", noteRow, [
    { name: "byCreatedAt", fields: ["createdAt"] },
    { name: "byColor", fields: ["color", "createdAt"] },
  ]),
});

Every row gets an id automatically. Rows are validated on the way in and on the way out — a row read back from SQLite is untrusted input like any other.

Collaborative fields

A column several people change at once is declared with a fourth argument, and merges by the algebra it names rather than by optimistic concurrency:

notes: defineTable("notes", noteRow, [
  { name: "byUpdatedAt", fields: ["updatedAt"] },
], {
  collaborative: { body: { algebra: "text", maxBytes: 1_000_000 } },
})

Per field, because one row usually wants both answers — here the body merges character by character while the title and colour keep optimistic concurrency. The algebra is named with a string; what that name means is registered by the application, which is what keeps merge logic out of every schema's dependency tree.

A collaborative field may not also be indexed. An index holds a value the query engine compares; a document reconstructed from an op log is not that, and refusing beats an index that silently never matches.

Indexes are a contract

An index lists fields in order, and a query may constrain them left to right:

Index ["color", "createdAt"]Allowed
withIndex("byColor", { color })yes
withIndex("byColor", { color }).range({ gte: t })yes — bounds createdAt
withIndex("byColor", { createdAt })no — skips a leading field

An index does two jobs: it makes the read fast, and it makes the invalidation precise. A query that pins color is only woken by writes touching that colour. Declaring one is therefore closer to declaring a subscription channel than to tuning a database. → Reactivity

Index ordering has exactly one definition in the codebase: the comparison used at query time and the byte encoding rows are keyed by must agree, and an exhaustive test says so.

Choosing indexes

  • One per query shape you intend to subscribe to.
  • Put the field you filter by first, and the field you order by after it.
  • A table with no index for a query means a full scan, which records a whole-table dependency and wakes on every write to that table.

Changing a schema

Adding a table or an index is a code change. Changing the shape of existing rows is a migration — applied per tenant, when that tenant next wakes.

Next

On this page