@ai-matrx/data

中文文档
0.29.126发布于 2026/10/30MITTypeScriptESM + CJS近 30 天下载 10万

The opinionated AI Matrx data layer: the /db tier's hard-won Supabase primitives — optimistic concurrency, complete-list reads, JSONB merge, session-retry, the domain-wide auth cookie, realtime channel hygiene — the /net resilience trio, and the /files la

原始使用文档
文档目录9 节
  1. @ai-matrx/data/db
  2. The doors — one way to talk to the database
  3. @ai-matrx/api/net
  4. @ai-matrx/data/next
  5. @ai-matrx/data/react
  6. @ai-matrx/data/knobs , /app-config , /config-attestation , …
  7. @ai-matrx/data/access
  8. @ai-matrx/data (core)
  9. Design

@ai-matrx/data

The opinionated AI Matrx data layer: the /db tier — the hard-won Supabase primitives every Matrx client uses against the platform database — the /net tier — the resilience trio every outbound call and event stream rides — and the /files tier — the Matrx file-communication client. Each primitive exists because a real production incident forged it; the file headers carry the stories.

npm install @ai-matrx/data
SubpathWhat it carries
@ai-matrx/dataCore: structural PostgREST types, pgErrorToError, the credentials port, plus the /net trio re-exported (it rides the core).
@ai-matrx/data/dbThe Supabase lane: optimistic concurrency, complete-list reads, JSONB merge, session-retry, auth cookie, realtime hygiene.
@ai-matrx/api/netThe net-resilience trio: resilient fetch, retry/backoff, stream stall watchdog, structured NetError vocabulary.
@ai-matrx/data/nextThe Next.js binding: ONE factory for every Supabase client a Next app builds — browser, Server Component, Route Handler, proxy/middleware, OAuth callback — plus the whole middleware auth-cookie pass.
@ai-matrx/data/reactThe React bindings: useAsyncData, useRealtimeChannel / useRealtimeQuery, useAsyncAction. Stamped "use client".
@ai-matrx/data/accessThe access gate's pure section: the "you can't open this" vocabulary, the access_denied_context resolver client, the request / decide / withdraw flow, the per-person hide, honest-copy helpers. Not stamped "use client"; app inputs injected with configureAccess.

@ai-matrx/data/db

ExportWhat it does
guardedUpdateOptimistic concurrency as compare-and-swap on the canonical version column. Classifies "0 rows updated" honestly: conflict (with the current row so the UI can offer merge/refresh) vs not_found. Callback-shaped — it never needs to know your schema or client.
readAllRows / tryReadAllRowsThe ONE way to read a list you intend to treat as complete. PostgREST silently caps responses (HTTP 206 looks like success); a truncated list used for an existence/diff decision is a confidently wrong answer. Pages to the server-declared total and throws (IncompleteReadError) rather than return a provably partial list. readAllRowsRest / tryReadAllRowsRest are raw-fetch twins for scripts with no client.
mergeJsonColumnMerge into a row's JSONB column without losing a concurrent writer's keys: composes guardedUpdate with a bounded re-read-and-remerge retry. Returns a typed status, never throws for a normal outcome.
createSessionRetryRecovers the one failure worth retrying: a request that reached PostgREST with no user JWT while a session actually exists (refresh in flight, cookie swap). Retries exactly that cause, exactly once, after re-resolving the session. Everything else passes through untouched.
createOAuthRefreshFetchKeeps auth-js's normal storage, locking, refresh lifecycle and events, but routes a proven allowlisted OAuth-client session's refresh to /auth/v1/oauth/token with its JWT client_id. Normal sessions continue through Supabase's ordinary refresh endpoint.
createAuthCookieOne login across every subdomain of your apex: a renamed (sb-matrx-auth), domain-wide auth cookie issued only on real apex hosts — localhost and previews get host-only, because a browser silently rejects a mismatched Domain.
createActiveOrgCookieONE browser memory of the organization a person is acting in, shared by every Matrx surface on the apex (matrx-active-org, Domain=.aimatrx.com): identity-keyed (<userId>:<orgId>), domain-wide only on a listed apex, host-only elsewhere. Stores the stored-selection rung of the canonical order; never decides.
uniqueChannelTopicRealtime channel hygiene: unique per-mount topics so React 19 double-invoked effects and Fast Refresh can never collide with a still-joined channel.

The doors — one way to talk to the database

Every app talks to the database through doors over descriptors it generates locally from the ONE database description. The description ships inside this package (description/, gzipped, the one this version was built with), so no repository or database access is needed.

# in your app (package.json: "db:generate": "matrx-data generate --out src/db/generated --schemas workbench,chat")
npx matrx-data generate --out src/db/generated --schemas workbench          # one file per schema
npx matrx-data generate --out src/db/generated --schemas workbench --check  # CI: fail when stale
npx matrx-data generate --out src/db/generated --flags-index                # + per-schema flags for tableByName
# fresher than this package version? With the direct-DB variables (SUPABASE_MATRIX_*):
npx matrx-data emit --out db-contract && npx matrx-data generate --from db-contract --out src/db/generated
import { createDb, listAll, read, insert, update, remove, call } from "@ai-matrx/data/db";
import { noteFolders, notes, noteFolderGetOrCreate } from "./db/generated/workbench";

const db = createDb({ client: supabase, organization: () => activeOrgId }); // wraps YOUR client
const pinned = await listAll(db, notes, (q) => q.eq("is_pinned", true));       // complete; typed filter; live rows
const row = await read(db, notes, id, { columns: ["id", "label"] });           // typed to those columns
await insert(db, notes, { folder_id, label });         // child: the folder's organization, never the active one
await update(db, notes, id, { label }, { expectedVersion: row.version });      // or { overwrite: true }
await remove(db, notes, id, { expectedVersion: v });   // soft delete; archive-only tables refuse
await call(db, noteFolderGetOrCreate, { p_name: "Clients" });
// @ai-matrx/api: pass db.credentials as its `credentials` option — one sign-in feeds both lanes

Rules come from the description (the live table registry), never the call site: organization by the access ladder; reads never filter by the active organization and exclude soft-deleted rows unless includeDeleted; listAll is complete and ordered by the full primary key; every write on a versioned table is guarded; the door set follows the registry type; createServiceDb is server-only.

@ai-matrx/api/net

ExportWhat it does
resilientFetchfetch with a two-stage timeout — connect (default 15s, request start → headers) and total (default 120s, null disables) — plus a composed AbortController returned to the caller for canceling body reads. Never pre-blocks on navigator.onLine (the flag is a hint; a real outage taught us it lies) — offline is only reported when a genuine network failure corroborates it.
withRetryExponential backoff with jitter for idempotent operations only. Default predicate retries exactly what the error taxonomy marks retryable (connect timeouts, network failures, HTTP 408/429/5xx). Honors an AbortSignal mid-backoff; never wrap user-authored submissions.
monitorStreamWraps an async event iterable with two guarantees: a heartbeat deadline (default 30s between events) and an absolute lifetime ceiling (default 10min). A stuck stream throws HeartbeatTimeoutError/TotalTimeoutError — and aborts your AbortController — instead of awaiting forever.
NetError familyConnectTimeoutError, TotalTimeoutError, HeartbeatTimeoutError, NetworkError, HttpError, AbortedError, OfflineError — every failure resolves to a class with a code and an honest retryable flag, so callers branch on type, never on string matching. Helpers: isNetError, toNetError, isTransportFailure (is this the transport dying, or the server answering? — decides how LOUD to log), extractErrorMessage.

Wiring: there is nothing to inject. The tier has zero dependencies and zero seams — fetch and timers come from the environment (Node 20+, browsers, React Native/Hermes), and it never touches Supabase, org context, or the API lane. Pass your own AbortSignal to resilientFetch/withRetry and your own AbortController to monitorStream so a watchdog timeout also cancels the underlying request. Everything here is also re-exported from the root @ai-matrx/data entry point.

@ai-matrx/data/next

A Next.js app has FIVE places that must construct a Supabase client, and every one of them must pass the SAME auth-cookie options or it cannot see the session the others wrote. One factory owns all five. The host injects IDENTITY only.

// One module in your app — everything else imports from here.
import { createNextSupabase } from "@ai-matrx/data/next";
import type { Database } from "@/types/database.types";

export const supabaseNext = createNextSupabase<Database>({
  // STATIC member accesses: Next only inlines NEXT_PUBLIC_* for those.
  supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
  publishableKey: process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
  apexDomain: "example.com",
  cookieName: "sb-example-auth-v2",
  legacyCookieName: "sb-example-auth", // optional: migrate + clear an old key
});
DoorCall
Client ComponentsupabaseNext.browserClient() — a singleton on a globalThis slot
Server Component / ActionsupabaseNext.serverClient({ cookieStore: await cookies(), host })
Route HandlersupabaseNext.routeClient({ requestCookies: request.cookies, setCookie, host })
Proxy / middlewareawait supabaseNext.middlewareSession({ host, requestCookies, createResponse, createRedirect })

middlewareSession is the whole auth pass in one call and returns { user, client, response, redirect(url), splitCookieJar, ambiguousAuthCookies, authUnavailable }. It owns: legacy-key migration (absent-only, so a live session is never overwritten), the "nothing may run between createServerClient and the identity resolve" rule made structural, persisting a migrated session under the current key, clearing the superseded key at the scope this host issued it on, no-store response headers on any cookie-bearing response, and cookie carry-over onto redirects. Dropping any one of those logs users out at random — which is how each was found.

The identity resolve is getClaims(), never getUser(), and it is BOUNDED. getUser() sends a request to the Auth server for every JWT; getClaims() verifies the access token against the project's JWKS with WebCrypto — locally, usually with no network call, and just as trusted, because the signature is checked. It needs an asymmetric signing key on the project (ECC/RSA); with a symmetric secret it degrades to a getUser()-shaped request per call, still correct but with none of the saving. A 2.5s identity budget — ONE deadline per request across /auth/v1/user, a token refresh and the JWKS fetch, after which auth-js is answered with a non-retryable 408 so its 30s refresh backoff cannot run (0.18.0) — means a stalled auth server costs one request its identity instead of costing the page its response — on Vercel, where Next 16 forces this pass into a Node Lambda with a hard task cap, the unbounded version returned 504 MIDDLEWARE_INVOCATION_TIMEOUT to real people.

🚨 authUnavailable is the consumer's obligation. It means the authority could not be REACHED, which is NOT "signed out": user is null, but no auth cookie was written, expired or healed and the superseded key was not cleared. Read it before treating user === null as signed out — a host that bounces to /login on a null user must skip that bounce when the flag is set, or a 2.5s network blink signs people out mid-session. A call site needing created_at, identities, last_sign_in_at, factors or *_confirmed_at must keep getUser() elsewhere: the JWT carries none of them. Full contract: common-docs/systems/platform/auth/proxy-identity/FEATURE.md.

No next/* import lives in the package. cookies() and NextResponse arrive through structural seams (createResponse: () => NextResponse.next({ request })), so /next builds and tests outside Next and never drags the framework into another consumer's graph. Missing identity throws MissingSupabaseIdentityError NAMING the variable, instead of the cryptic undefined.trim() TypeError that process.env.X! produces from inside Supabase.

@ai-matrx/data/react

Three hooks, each built at a MEASURED repeated consumer — not a data-fetching framework. There is no cache, no store and no query client here: these compose under your Redux or React Query, they do not compete with it. What they own is the part every hand-rolled copy gets wrong.

HookWhat it owns
useAsyncData(load, deps, opts)Cancellation (a real AbortSignal reaches your loader), stale-response ordering, honest error conversion (a PostgREST error is a plain object — String(err) renders "[object Object]"), retry of TRANSPORT failures only, keepPreviousData so a key change never blanks the screen, and an abort that is silent because it is not a failure.
useRealtimeChannel(client, topic, bindings, opts)Unique per-mount topics, reconnect with exponential backoff and a stability reset, an onReconnect catch-up door (events missed while the socket was down are GONE), honest connecting/connected/reconnecting/disconnected status, latest-callback refs, guaranteed teardown.
useRealtimeQuery({ client, topic, watch, load, deps }, opts)The two composed, with realtime events COALESCED so a bulk write is one read, and a re-read wired to reconnect.
useAsyncAction(action, opts)Double-submit REFUSAL (a disabled button is not a guard), unmount-safe state, honest errors, and a discriminated { ok: true, value } | { ok: false, error } result so nothing is swallowed. Writes are never retried — a replayed submission is how you get two invoices.
usePaginatedData({ queryKey, initialCursor, loadPage, getRowId })Cursor accumulation with one request at a time, identity masking, abort/stale-result handling, explicit retry, in-place row-id updates, and loud repeated-cursor refusal. It has no cache and no table dependency.
const { data, loading, error, refresh, realtimeStatus } = useRealtimeQuery({
  client: supabaseNext.browserClient(),
  topic: `jobs:${fileId}`,
  watch: [{ schema: "docproc", table: "jobs", filter: `file_id=eq.${fileId}` }],
  load: ({ signal }) => listJobsForFile(fileId, signal),
  deps: [fileId],
}, { enabled: fileId !== null });

The Supabase client type is STRUCTURAL — the package never imports @supabase/supabase-js. A real client satisfies it by shape, and there is a compile-time proof of that in the package's own tests (it caught a too-narrow signature during this package's build, which is exactly the failure a host would otherwise have hit).

@ai-matrx/data/knobs, /app-config, /config-attestation, /config-types

Config with no code defaults, where every read is counted:

import { createKnobStore, defineKnobFeature, knob, parse } from "@ai-matrx/data/knobs";
import { createAppConfigReader } from "@ai-matrx/data/app-config";
import { createConfigReadRecorder } from "@ai-matrx/data/config-attestation";

const CHAT = defineKnobFeature("desktop.confidential_chat", {
  recentlyDeletedDays: knob("recently_deleted_days", parse.int(0, 3650)),
});
const chat = createKnobStore({ feature: CHAT, client, address, bundled, cache, log, name: "confidential_chat", recorder });
chat.load();
await chat.refresh();
chat.get("recentlyDeletedDays"); // live DB → last-seen cache → build snapshot; throws KnobUnavailableError if none

createAppConfigReader reads an app's public.app_config row the same way (PostgREST with Accept-Profile: public, then aidream with the bearer). forDisplay() on either is a frozen copy that is never counted. createConfigReadRecorder batches reads into interval buckets for platform.config_reads_report; buildConfigManifest builds the release manifest from the typed registry.

@ai-matrx/data/access

Moved from the website's access-gate feature. Pure (no React): fetchAccessDeniedContext, deriveStatus, classifyDataError, readServerRefusal / readTypedRefusal, createAccessRequest / createDeleteRequest / createSettingAccessRequest / decideAccessRequest / withdrawAccessRequest / listAccessRequests, fetchRemovalRights / hideForMe / unhideForMe / listHiddenItems, the requestAccess routing helpers, actionAuthority, archivedWords, decideServerReadRecheck, isGovernedActionDenial, and the types (AccessDeniedContext, AccessGateStatus, ...).

The configureAccess contract. The package imports nothing from the app. Call configureAccess({ getClient, ... }) once at boot; any service used before that throws an error naming configureAccess. Fields (all but getClient are optional and needed only by the feature that uses them):

FieldUsed for
getClient()The host's Supabase client (required).
fetchWithOrganizationThe org-carrying fetch, for CMS-resolved tokens (/api/cms/access-context).
ensureOrgIdResolve an org id or raise the host's org gate (passed through to React consumers).
sendDirectActionMessageThe host's DM primitive; without it requests still file, delivered reports 0.
entityTokensCMS access-gate tokens: isCmsAccessGateToken, cmsAccessGateLabel, site action/key helpers.
platformOrgIds{ systemOrganizationId }; defaults to the platform's system org.
actionRegistries, extraRequestHandlersOpaque host registries/handlers read by the React section.

resetAccessEnv() clears the injection (tests). The React screens live in @ai-matrx/sharing/access/react (data owns no UI).

@ai-matrx/data (core)

Structural types (PostgrestLikeError, MaybeSingleResponse, Json) and pgErrorToError — PostgREST errors are plain objects, not Errors; this folds them into descriptive real errors. The /net trio is re-exported here as well.

Design

  • Zero runtime dependencies. All Supabase typing is structural — a real supabase-js client satisfies the shapes; nothing is imported from it. Works in Node 20+, browsers, and React Native/Hermes.
  • Injected hosts, opinionated behavior. Values and clients come in through arguments (apexDomain, auth); the behavior — the CAS contract, the refusal to return partial lists, the single-cause retry — is deliberately non-configurable. The opinions are the product.
  • Dual loader. ESM and CommonJS conditions with matching declarations, proven from the packed tarball on every release.

MIT © AI Matrx

版本与发布记录

@ai-matrx/data 共发布 265 个版本,首次发布于 2026/08/29;过去 12 个月发布了 265 个版本。

版本发布日期距今
0.29.126latest2026/10/306 天前
0.29.1252026/10/288 天前
0.29.1242026/10/2610 天前
0.29.1232026/10/2511 天前
0.29.1222026/10/2511 天前
0.29.1212026/10/2512 天前

关于 @ai-matrx/data 的常见问题

@ai-matrx/data 是什么?

The opinionated AI Matrx data layer: the /db tier's hard-won Supabase primitives — optimistic concurrency, complete-list reads, JSONB merge, session-retry, the domain-wide auth cookie, realtime channel hygiene — the /net resilience trio, and the /files la

如何安装 @ai-matrx/data?

在项目目录运行 npm install @ai-matrx/data,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@supabase/supabase-js(>=2.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 matrx-data,可以用 npx matrx-data 直接运行。

@ai-matrx/data 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ai-matrx/data 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@ai-matrx/data 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ai-matrx/data 可以商用吗?

@ai-matrx/data 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ai-matrx/data 的下载量有多少?

根据 npm 官方下载统计,@ai-matrx/data 在 2026/10/05 – 2026/11/03 这 30 天内被下载 99,713 次,日均约 3,324 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ai-matrx/data 还在维护吗?

最新版本 0.29.126 发布于 2026/10/30(6 天前);至今共发布 265 个版本,其中过去 12 个月发布了 265 个;首次发布于 2026/08/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。