Global storage
The D1 authority — what it is for, and why only actions may touch it.
There are two storage authorities, and no operation spans both.
| Tenant | Global | |
|---|---|---|
| Backed by | Durable Object SQLite | D1 |
| Access | ctx.db | ctx.global |
| Reachable from | query, mutation, action | action only |
| Transactions | yes, synchronous | none |
| Live subscriptions | yes | no |
A query cannot see global data because a query is reactive and global data is not live-subscribable. A mutation cannot, because a transaction that appeared to span two authorities would be a lie. Only an action holds both, and it composes them through a saga.
The types enforce this rather than the documentation:
mutation({
handler: (ctx) => {
ctx.global; // Property 'global' does not exist on type 'RequestContext'
},
});Setting it up
Give the tenant object a store, and create your tables on start:
import { createTenantObject, D1GlobalStore } from "@highx/cloudflare";
export const TenantObject = createTenantObject<Env>({
schema: databaseSchema,
functions: serverFunctions,
subscribableQueries,
globalStore: (env) => new D1GlobalStore(env.GLOBAL_DB),
// D1 has no migration hook a tenant object participates in, so tables are
// created inside blockConcurrencyWhile, before any request is served.
onStart: async (env) => {
await new D1GlobalStore(env.GLOBAL_DB).ensureTable("published_notes");
},
});highx generate infers the D1 binding from new D1GlobalStore(env.GLOBAL_DB)
and highx provision creates the database — you never declare it by hand.
Skip globalStore and every ctx.global call rejects with "No global store is
configured for this runtime". Returning empty data instead would turn a missing
binding into silently absent records, which is far harder to notice in
production.
The API
await ctx.global.get("published_notes", id);
await ctx.global.find("published_notes", { authorId });
await ctx.global.insert("published_notes", { title, body });
await ctx.global.update("published_notes", id, { title });
await ctx.global.delete("published_notes", id);Asynchronous, and with no transaction method at all — there is nothing for tenant work to be nested inside.
How rows are stored
D1GlobalStore keeps each row as (id TEXT PRIMARY KEY, data TEXT) — opaque
JSON. find() therefore scans the table and filters after decoding.
That is fine for a small read model and wrong for anything with volume. Promote
the fields you filter on to real columns and own the migrations yourself;
ensureTable is a convenience for getting started, not a schema tool.
Serving it publicly
Global data is read-through and eventually consistent. A public route says so explicitly:
cache-control: public, max-age=60
etag: W/"..."
x-highx-consistency: eventualThe payload deliberately carries no subscription ID and no sequence number, so a public reader cannot mistake it for a live view. A stale public page is acceptable; an authenticated live query must converge.