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.