zeekay e7dfab6b85 Publish to npm the estate's way: on main, with the credential from KMS
The old lane waited for a tag and a GitHub secret, NPM_TOKEN, which
neither plane holds, so v0.2.8 and v0.2.9 were tagged and never
published (npm serves 0.2.7). This is hanzo-js/sdk-fetch's lane: a push
to main whose package.json is ahead of the registry packs, reads
NPM_TOKEN from KMS through the org's KMS_CLIENT_ID and
KMS_CLIENT_SECRET, publishes, and waits until npm serves the digest it
packed.
2026-10-20 08:02:03 -07:00
2026-10-19 08:29:40 -07:00
2026-10-19 08:29:40 -07:00
2026-08-04 16:47:47 -07:00

@hanzo/base

npm

The JavaScript/TypeScript client for Hanzo Base, the reactive backend. Auth is Hanzo IAM-native: the client holds the IAM-issued JWT and validates it locally.

Zero runtime dependencies. Records/collections CRUD, realtime (SSE), files, reactive queries with optimistic writes, and CRDT primitives — with optional React bindings.

There is a companion Dart client, hanzo, that mirrors this shape.

Install

npm install @hanzo/base
import { BaseClient } from '@hanzo/base'

const base = new BaseClient('https://base.hanzo.ai')

Auth (Hanzo IAM)

Sign-in is Hanzo IAM's (PKCE in a browser, a key or client credentials on a server). Base has no password, OAuth, verification or reset routes of its own, so the client is handed the IAM-issued token and carries it on every request:

base.authStore.save(iamAccessToken, null)

base.authStore.isValid   // JWT present and unexpired
base.authStore.token     // the IAM JWT

base.signOut()

Persist the session in the browser:

import { BaseClient } from '@hanzo/base'
import { LocalAuthStore } from '@hanzo/base/compat'

const base = new BaseClient({
  url: 'https://base.hanzo.ai',
  authStore: new LocalAuthStore(),
})

Records / collections

const page = await base.collection('posts').getList(1, 20, {
  filter: 'published = true',
  sort: '-created',
})

const one = await base.collection('posts').getOne('RECORD_ID')
const made = await base.collection('posts').create({ title: 'hi' })
await base.collection('posts').update('RECORD_ID', { title: 'yo' })
await base.collection('posts').delete('RECORD_ID')

Realtime (SSE)

const off = base.collection('posts').subscribe('*', (e) => {
  console.log(e.action, e.record) // 'create' | 'update' | 'delete'
})
off()

An EventSource cannot send a header and the token never travels in a query string, so the stream is opened with a grant: the client POSTs <prefix>/realtime/token with the token, opens <prefix>/realtime?token=<grant> with what comes back, and mints a new grant on every reconnect. Collection rules decide which events reach the stream. On Hanzo Cloud an org's Base is at /v1/base:

const base = new BaseClient({ url: 'https://api.hanzo.ai', prefix: '/v1/base' })

Files

const url = base.files.getURL(record, 'photo.png', { thumb: '100x100' })

Reactive queries + CRDT

@hanzo/base ships a reactive QueryStore with optimistic updates and server reconciliation, plus CRDT primitives (@hanzo/base/crdt) for collaborative state. React bindings live at @hanzo/base/react.

import { QueryStore } from '@hanzo/base'
import { CRDTText } from '@hanzo/base/crdt'

Develop

npm install
npm run build
npm test

License

MIT © Hanzo AI, Inc. See LICENSE.

@hanzo/base/rest — the table wire

A sibling of the collection client, not a replacement. That one speaks Base's own /v1 surface: collections, records, realtime, CRDT. This one speaks the table wire at <prefix>/rest/{table}, where a table is the path and the filters are query params — the shape the console's data grid is built on.

import { base } from '@hanzo/base/rest'

const db = base('https://base.example', key)

const { data, count } = await db
  .from('posts')
  .select('id,title')
  .eq('status', 'live')
  .order('created', { ascending: false })
  .limit(20)
  .count()

Filters are eq neq gt gte lt lte like ilike is in, with .not(column, op, value) negating any of them. Writes are .insert(), .update() and .delete(), and return nothing unless .select() asks for the rows back.

Three things worth knowing, because each is where a client usually gets it wrong:

  • An unasked count is null, never 0. The server does not compute a total unless asked, so reporting zero would be inventing an answer to a question nobody put.
  • A query sends nothing until it is awaited, so a builder can be passed around and narrowed without anyone issuing a request by holding one.
  • update and delete require a filter. They act on every row the filter selects, and an unfiltered one would mean the whole table — the server refuses it rather than applying it.

Failures carry { message, details, hint, code }. code is what to branch on:

const { error } = await db.from('posts').insert({ title })
if (error?.code === '23505') return 'that title is taken'
S
Description
@hanzo/base — JavaScript/TypeScript client for Hanzo Base (reactive backend, Hanzo IAM-native auth)
Readme MIT
139 KiB
Languages
TypeScript 80.9%
JavaScript 19.1%