The reactive runtime for Cloudflare apps and their agents

HighX is a TypeScript framework. You write plain typed functions. HighX runs them next to your data, keeps every screen up to date by itself, and gives your AI agents a safe place to work.

one commit, every screen a write commits your tables live queries who needs to know reads recorded matched here screens updated

Write plain functions. That is the backend.

Every function is one of three kinds. Each kind says what it is allowed to do — and the runtime enforces it, so mistakes fail loudly instead of corrupting data quietly.

kind 1 / query

Reads data. Nothing else.

A query reads the database and remembers what it looked at. That memory is what makes live updates work later.

reads
export const listNotes = query({
  args: z.object({ accountId: v.id("accounts") }),
  auth: authed,
  handler: (ctx, { accountId }) =>
    ctx.db.query("notes")
      .withIndex("byAccount", { accountId })
      .order("desc")
      .take(20),
});
kind 2 / mutation

Writes data, safely.

A mutation changes data inside one transaction. It all commits, or none of it does. Network calls are refused here.

all of it, or none of it
export const saveNote = mutation({
  args: z.object({ text: z.string().max(10_000) }),
  auth: authed,
  handler: (ctx, args) =>
    ctx.db.insert("notes", {
      accountId: ctx.identity.accountId,
      text: args.text,
      at: ctx.clock.now(),
    }),
});
kind 3 / action

Talks to the outside.

An action calls APIs and models, then saves results by running mutations. Cross-database work is written as explicit steps.

fn world
export const enrich = action({
  args: z.object({ domain: z.string() }),
  handler: async (ctx, { domain }) => {
    const res = await ctx.fetch(`https://ex.com/${domain}`);
    return ctx.run(saveEnrichment, {
      domain, value: await res.json(),
    });
  },
});

Screens update themselves

When a mutation commits, HighX checks which queries touched that data and pushes the new result to every subscriber. You never write refetch code, cache keys, or polling loops.

mutation commits once tenant durable object patches every screen updates
one connection, everything on it websocket
client →  hello · auth · sub · unsub · mutate · action · resume · ping
server →  welcome · snapshot · patch · stream · error · bye · pong
Each subscription keeps its own counter. If the connection drops, the client asks for what it missed — or gets a fresh copy and carries on.
app/Notes.tsx client
function Notes({ accountId }) {
  const notes = useQuery(api.notes.list, { accountId });
  return <List rows={notes} />;
}
  • —First result arrives with the page. No spinner wiring.
  • —New results arrive on their own. No polling.
  • —That is the whole client.

One tenant, one source of truth

Each tenant gets a private database — a Durable Object with SQLite inside. Shared public data lives separately in D1. The two never commit together, and HighX says so instead of hiding it.

Yours · the tenant database

commits as one subscribers
  • Your tables, indexes, live queries, and agent runs.
  • Transactions that fully commit, or fully fail.
  • One tenant talks to one database. Simple to reason about.

Shared · the public read model

copied later d1 · read model cache
  • Public, read-only copies for fast pages.
  • Cached at the edge — it can be a little stale.
  • No live updates here in v1, and no pretending otherwise.
highx/signup.ts · work that spans both action
// signup touches global and tenant data, so it is written
// as steps — with a cleanup step if something fails.
export const signUp = action({
  args: signUpArgs,
  handler: async (ctx, args) => {
    const user = await ctx.run(createGlobalUser, args);
    try {
      const account = await ctx.run(createTenantAccount, {
        userId: user.id, ...args,
      });
      await ctx.run(createSession, {
        userId: user.id, accountId: account.id,
      });
      return account;
    } catch (error) {
      await ctx.run(markUserProvisioningFailed, { userId: user.id });
      throw error;
    }
  },
});
the rules, in plain words
  • 01Atomic means one database at a time.
  • 02Work that spans both is written as steps, never hidden.
  • 03Need signup to be all-or-nothing? Keep those records in one place.
  • vecEmbeddings live in Vectorize; your table keeps the ID.
Being correct beats being clever. Every time.
HighX does not pretend that D1 and a Durable Object can commit together.

AI agents with a leash and a ledger

An agent is a model plus a set of skills. A harness runs it in a bounded loop. Every step is saved, every tool call is checked, and a human can be put in the loop at any point.

the harness · a bounded loop watch it run
model tool observe budget run stops budget left
The harness asks the model, calls one tool, looks at the result, checks the budget — repeat. When the budget is spent, the run stops. Agents cannot wander off.
what a run looks like
  • modelThe model decides the next step. Nothing more.
  • toolIt calls one declared tool. If it is not declared, it does not exist.
  • observeThe tool result goes back to the model.
  • budgetSteps and tokens are counted. Out of budget — the run stops.
  • savedEvery step is stored, so runs can sleep, retry, and resume after a deploy.
Streaming token deltas are kept in a bounded replay buffer; the durable record is steps, results, and interrupts.
skills/incident-response/skill.ts typed · versioned
export default defineSkill({
  name: "incident-response",
  version: "1.0.0",
  instructions: { file: "./instructions.md" },

  input: z.object({ incidentId: z.string(), symptoms: z.string() }),
  output: z.object({ diagnosis: z.string(), reportId: z.string() }),

  tools: { readLogs, inspectDeployment, rollbackDeployment },
  capabilities: ["logs:read", "deployments:read"],
  policy: { approvalRequiredFor: ["deployments:rollback"] },
});
A skill is a typed bundle: instructions, tools, input and output schemas, and the capabilities it asks for. Versions are immutable — a finished run replays against the same contract.
what a skill may actually do intersection only
host allows ∩ linked app allows ∩ agent allows ∩ skill asks for ∩ harness phase = allowed
A skill never grants power — it asks for it. It only gets what every layer above also allows. Asking for more fails the build.
interrupts · a human in the loop exactly one winner
operator a operator b first answer wins
When an agent needs a person, it opens an interrupt — one row in the tenant database. Many people can answer at once; the transaction accepts exactly one.
workflows · long jobs, durable steps
snapshot:v1 review:v1 publish:v1
export const review = workflow({
  args: z.object({ accountId: v.id("accounts"), period: z.string() }),
  handler: async (step, args) => {
    const snap = await step.run("snapshot:v1", snapshotAccount, args);
    const result = await step.run("review:v1", runReviewModel, snap);
    await step.run("publish:v1", publishReview, result);
  },
});
The body is deterministic — no clocks, no randomness, no bare fetch. Real work happens in named steps with stable IDs. Rename a step, ship a new version.

We write down the limits too

Every tool promises the world. Here is exactly what HighX guarantees — and what it openly does not.

Guaranteed

  • A write fully commits, or fully fails.
  • After a reconnect, live data always catches up.
  • Everything crossing a boundary is validated.
  • Pages load with data included — no second fetch.
  • An interrupt is answered exactly once.
  • A linked app cannot do more than it declared.

Not promised

  • No magic transactions across two databases.
  • No instant worldwide cache clearing — a cached page can be stale.
  • No sandbox for code you link in. It is trusted code; review it.
  • No live updates on public D1 data in v1.
  • No compiler that can prove what arbitrary JavaScript does.
  • No automatic migration of running workflows after code changes.
A stale public page is acceptable; a logged-in live query must catch up.

Everything maps to one platform

HighX does not hide Cloudflare behind fog. Each feature uses one service, and its limit is written right next to it. If the platform cannot do something, the build fails — it never silently degrades.

You need HighX uses The limit, in writing
tenant data Durable Objects SQLite One tenant = one place that can commit.
shared data D1 Read-only in v1. No live updates.
live updates Durable Object WebSocket WebSocket is the required carrier.
files R2 Big uploads use presigned URLs.
vectors Vectorize Indexes are created before first use.
background jobs Workflows + Queues Retries and idempotency are explicit.
models AI Gateway Usage is tracked in your own ledger.
secrets Secret bindings Never sent to the browser.

Take it for a run

Read the spec, then build the smallest thing: one query, one mutation, one screen that updates itself. That is the whole idea — everything else is the same pattern repeated.

terminal~/app
$ npm create highx@latest
$ cd app && highx dev

  worker   ready on :8787
  tenant   account-do attached
  schema   4 tables, 6 indexes ok
  watch    highx/**