Skip to main content

JavaScript SDK

@kurabase/js is an independent, fetch-native client. It is not a wrapper around @supabase/supabase-js, although its query builder intentionally feels familiar.

Create a client​

import { createClient } from '@kurabase/js'

const db = createClient('http://127.0.0.1:55421', 'publishable-key')

Use the gateway directly from a browser​

The application gateway is a browser API origin. A frontend can call its public gateway URL directly; a same-origin proxy is optional architecture, not a Kurabase requirement. Gateway CORS accepts the SDK's apikey, Authorization, PostgREST and range headers, and exposes the response headers used for pagination and chain revisions.

Only put the publishable key in browser configuration:

const db = createClient(
import.meta.env.VITE_KURABASE_URL,
import.meta.env.VITE_KURABASE_PUBLISHABLE_KEY,
)

Never put KURA_DEV_SERVER_KEY, a gateway/server secret, a wallet private key, or an access token supplied out of band in browser code. A proxy can still be useful when an application needs its own server-only behavior, but developers do not need to provision one just to use Kurabase data APIs from a browser.

For local development, use the exact public URL reported by pnpm kura status, normally http://127.0.0.1:55421. The gateway also opts in to Chrome Private Network Access (PNA), so a page served from localhost can make a direct preflighted request to the loopback 127.0.0.1 gateway. If a browser extension, corporate browser policy, or HTTPS-only setting still blocks loopback HTTP, use the same loopback host for both the frontend and gateway, or serve the local frontend over the policy-required scheme; do not work around it by exposing a server credential.

The interactive sign-in flow does not accept a private key or ask for a pasted bearer token. Pass an EIP-1193 provider from an injected wallet or WalletConnect integration. The client requests an account, checks the target chain, obtains a gateway-bound session intent, and asks the wallet to sign its EIP-712 message:

const exchanged = await db.auth.signInWithWallet({
provider: window.ethereum,
})

if (exchanged.error) {
throw exchanged.error
}

The returned access token stays only in that client instance. exchangeSession() remains available for a configured identity-authority integration that has independently produced a valid envelope.

Wallet sign-in failures​

signInWithWallet() surfaces the wallet provider's message and code. A user rejection is returned as WalletRequestRejected; other provider failures use WalletSignInFailed while retaining the original provider message (for example User rejected the request (provider code 4001)). It first tries eth_signTypedData_v4, then falls back to v3 and standard typed-data only when the wallet says that method is unavailable. It never falls back after a user rejection or another provider error.

If no wallet prompt appears, check that the selected account is unlocked and on the chain reported by the gateway. A mismatch returns WalletChainMismatch with the required chain ID. Inspect the browser Network panel too: a failed OPTIONS request is a browser/network policy issue before the wallet flow, whereas a provider error is returned by the wallet's EIP-1193 API.

After a successful exchange, the gateway automatically registers canonical wallet IDs and strictly canonical did:privy: pseudonyms in its reserved chain-backed catalog when that catalog has been bootstrapped by a privileged gateway. It does not register arbitrary subjects or make the catalog proof of authentication. See automatic auth catalog for its exact relations, RLS, and public-data boundary.

Privy authentication belongs to the gateway provider​

In production, let Privy present the login and own the external or embedded wallet. A hosted gateway provider verifies the Privy credentials with its server-side default configuration (or an instance override) and returns the normal scoped Kurabase bearer. The SDK does not need a Privy dependency or a browser-held server key. Give its generic transaction.send callback Privy's sendTransaction function to execute application writes; see Privy and account abstraction.

The current callback API below remains for a self-hosted, custom identity-authority issuer. It is not the recommended integration for a new Privy-backed application and it never gives the SDK a Privy secret.

Custom Privy issuer adapter​

An application-owned session issuer can verify Privy credentials and create the short-lived Kurabase envelope with its identity-authority signer. Configure its public URL and callbacks from your Privy browser integration; the SDK has no Privy dependency and receives no Privy application secret:

const db = createClient(gatewayUrl, publishableKey, {
auth: {
privy: {
issuerUrl: 'https://control.example.com',
instanceId: 'ins_0123456789abcdef0123456789abcdef',
getAccessToken: () => getAccessToken(),
getIdentityToken: () => getIdentityToken(),
walletAddress: embeddedWallet.address,
},
},
})

const session = await db.auth.signInWithPrivy()
if (session.error) throw session.error

signInWithPrivy() posts the two ephemeral Privy tokens only to {issuerUrl}/data/v1/sessions, with { instanceId, walletAddress? }. The issuer must return { gatewayUrl, envelope }; the SDK rejects a response whose normalized gatewayUrl differs from this client. It then sends only the resulting envelope to the gateway's /auth/v1/session route. Neither Privy token is persisted by the SDK or sent to the Kurabase gateway.

For hosted Kurabase, issuerUrl is the public service API origin (for example the portal's VITE_KURA_SERVICE_API_URL), not the internal KURA_SERVICE_IDENTITY_SIGNER_URL. The instanceId must have the form ins_ followed by 32 lowercase hexadecimal characters. The hosted @kurabase/service-api implements this issuer when its external HTTPS identity signer is configured. It is hosted-provider infrastructure, not part of kura start; keep Privy verification and the identity-authority signer server-side. A provider without that optional signer deliberately returns 503 for data-session requests.

Query rows​

Builders are immutable and thenable:

const { data, error, count } = await db
.from('posts')
.select('id,title,author:profiles!inner(name)', { count: 'exact' })
.eq('published', true)
.order('id', { ascending: false })
.range(0, 24)

Common filters, ordering, range selection, relation projections, and count preferences use the REST contract directly.

match, not, and or preserve builder immutability. JSONB containment is available through contains and containedBy:

const visible = await db
.from('documents')
.select('id,meta')
.match({ owner: userId })
.not('status', 'eq', 'archived')
.contains('meta', { tags: ['red'] })

For JSONB arrays, the independent SDK accepts .contains('meta', [3, 1]). For a declared SQL array column, use overlaps:

await db.from('posts').select('id').overlaps('tags', ['release', 'urgent'])

Kurabase supports one-dimensional integer[], text[], boolean[], and uuid[] columns. Ranges, multidimensional arrays, SQL array containment, ordering, and equality filters return an explicit error.

Insert and upsert​

Mutations return no rows by default. Chain .select() to request a representation.

await db.from('posts').insert({ id: 1, title: 'Hello' })

const result = await db
.from('posts')
.upsert({ id: 1, title: 'Hello' }, { onConflict: 'id' })
.select()

Writes require authority that comes from a valid user session and a gateway that the instance owner has delegated.

On an AA-enabled gateway, provide a generic sender once when creating the client. The SDK asks the gateway to prepare an attested call, then gives that call to your Privy integration. It never substitutes its own wallet flow or falls back to gateway-submitted user writes:

const db = createClient(gatewayUrl, publishableKey, {
transaction: {
send: async (plan) => {
const tx = await sendTransaction({
chainId: Number(plan.chainId), to: plan.to, data: plan.data, value: plan.value,
}, { address: wallet.address, sponsor: true })
return { hash: tx.hash }
},
},
})

const result = await db.from('posts').insert({ id: 1, title: 'Hello' }).select()
if (result.error) throw result.error
console.log(result.transaction?.hash)

Without the callback, an AA-enabled instance returns the explicit KURA_TRANSACTION_REQUIRED client error. A fresh local kura start instance uses direct commits and does not require this adapter. Once Project > Authentication saves a Privy App ID, local application mutations also return plans, so the frontend must provide transaction.send; that public Dashboard setting does not supply or emulate a Privy/ZeroDev sender.

Call an RPC​

const answer = await db.rpc('add_one', { n: 41 })
const posts = await db.rpc('get_posts', { minimum: 1 }).select('id,title')

Kurabase currently supports scalar, SETOF or table, and atomic mutating SQL-language functions. Procedural functions and unsupported SQL return explicit errors.

Call published on-chain functions​

EXECUTION ON CHAIN functions are called at the instance contract, not through db.rpc(). Attach an EIP-1193 provider only for this direct ABI transport:

const onchain = db.onchain({ provider: window.ethereum })

// Any custom immutable ABI selector can be called with already ABI-encoded data.
const raw = await onchain.call({
to: instanceAddress,
data: '0x…',
})

// A complete `kura_erc20` extension has convenience methods.
const token = onchain.erc20(instanceAddress)
const balance = await token.balanceOf(account)
const receipt = await token.transfer(recipient, 25n)

call() uses eth_call; send() and ERC-20 mutations use eth_sendTransaction, requesting an account only when a write is actually sent. The helpers validate EVM addresses, calldata, transaction hashes, and uint256 values before calling the provider.

For Privy or another AA stack, keep the key and provider integration outside the SDK and provide a narrow sender adapter. It receives only { to, data, value? } and must return a transaction hash:

const onchain = db.onchain({
provider: walletProvider,
sender: async ({ to, data, value }) => {
const result = await sendTransaction({ to, data, value }, { address: wallet.address })
return { hash: result.hash }
},
})

This API is intentionally independent from the gateway-attested transaction.send callback used for relational REST/RPC writes. A direct ABI call may be an arbitrary custom function; erc20() is only an ergonomic adapter for the optional standard extension.

Authorize Dashboard management with an administrator wallet​

For browser-based instance management, connect an owner or delegated developer wallet to the privileged gateway. This is a distinct EIP-712 AdminSession, so normal application sign-in cannot be replayed as management approval:

const dashboard = createClient(adminGatewayUrl, publishableKey)
const management = await dashboard.auth.signInWithWallet({
provider: window.ethereum,
scope: 'admin',
})

The contract verifies that the direct signing wallet is currently an owner or developer, that the privileged gateway is still delegated, and that the administrator epoch has not changed. Identity-authority claims cannot become management authority.

Use administrative helpers in automation​

Administrative calls belong in a trusted server process:

const admin = createClient(url, process.env.KURABASE_SECRET_KEY!)

const instance = await admin.admin.instance()
const catalog = await admin.admin.catalog()
const sql = await admin.admin.sql('select * from posts')
await admin.admin.migrations.apply({ version: '202609100001', sql: 'create table posts (...)' })

instance() and catalog() provide project introspection. Automation can use a server credential held by the gateway deployment. Never bundle that key into a web application; interactive Dashboard operations use the management wallet flow above.

Response and error shape​

SDK responses have the shape:

{
data,
error,
count,
status,
statusText
}

Gateway errors retain their stable code, message, details, and hint. The client uses KURA_CARDINALITY for client-side cardinality errors and KURA_FETCH for transport failures.