13Docs

SDK reference

The SDK ships as two scoped packages, @bounded-sh/client (browser / React Native) and @bounded-sh/server (Node), like convex or @supabase/supabase-js. Both speak to the realtime worker that enforces the deployed policy, so the SDK can never bypass a rule or invariant.

Two packages, client & server

ImportRuns inAdds
@bounded-sh/clientbrowser + React Nativeend-user auth (email/wallet), live subscriptions, the live runtime client, function invocation
@bounded-sh/servera serverkeypair signing (no browser auth), createWalletClient, verifyWebhook
npm i @bounded-sh/client        # browser / React Native
npm i @bounded-sh/server        # Node keypair / server
setup
// client (browser / React Native)
import { init, login, get, set, subscribe } from "@bounded-sh/client";
await init({ appId: "<appId>", authMethod: "email" });   // see Auth

// server
import { init as initServer, createWalletClient } from "@bounded-sh/server";
await initServer({ appId: "<appId>" });
const vault = await createWalletClient({ keypair: process.env.VAULT_KEY! });

init(config) takes { appId, authMethod, chain?, apiUrl?, wsApiUrl?, authApiUrl? }. authMethod is 'email' | 'phantom' | 'wallet' | 'privy-expo' | 'guest' (default 'email'). Phantom/wallet also requires walletLogin: true; Privy Expo requires an explicit PrivyExpoProvider. The current default entry does not provide none, mobile-wallet-adapter, or web Privy auth providers. See Auth. On React Native call setPlatform / ReactNativeSessionManager.configure first. See React Native.

Or hand the whole setup to a coding agent, the same prompt the homepage uses:

Paste into Claude Code, Codex, or your preferred AI agent.

Read, get / getMany

get(path, opts?) reads a single document (even-segment path) or lists a collection (odd-segment path). Read access always obeys the collection’s read rule, a filter never returns a doc the caller can’t read.

reads
const doc  = await get("agents/a1/spend/a");             // one document
const page = await get("agents/a1/spend", { limit: 20 }); // { data, nextCursor }
const open = await get("orders", {                       // filtered + sorted
  filter: { status: { $in: ["open", "pending"] }, total: { $gte: 100 } },
  sort: { createdAt: -1 },
  limit: 20,
});

const { data, nextCursor } = await get("orders", { limit: 20 });  // collection paging
const next = await get("orders", { limit: 20, cursor: nextCursor });
const many = await getMany(["agents/a1/spend/a", "agents/a1/spend/b"]); // batch read

GetOptions: filter, sort ({ field: 1 | -1 }), limit, cursor, includeSubPaths, shape, prompt, bypassCache.

Search & aggregate

search / queryAggregate / count
const hits = await search("notes", "shipping", { fields: ["title", "body"] });

const rows = await queryAggregate("agents/a1/spend", { groupBy: ["category"], count: true, sum: ["amount"] });

const n = await count("orders", { prompt: "created in the last 7 days" });  // { value }

AggregateSpec: groupBy?, count?, sum?, avg?, min?, max? (each a field list). See Files & search for the search config.

Write, set / setMany

set(path, document) is sugar for a one-element setMany. setMany([...]) is one atomic transaction: every rule, hook, and invariant passes for the whole batch or nothing commits, this is what makes transfers under conserve and guard-then-write composition safe.

writes
await set("agents/a1/spend/s1", { amount: 60 });          // sugar for a 1-element setMany

await setMany([                                          // ONE atomic transaction
  { path: "accounts/alice", document: { balance: 50 } },
  { path: "accounts/bob", document: { balance: 150 } },
]);   // every rule + invariant passes for the whole batch, or nothing commits

A violated invariant throws a stable 409 invariant_violation; a denied rule throws 403 policy_denied. Policy details are included only under full errorDisclosure. Nothing partial is applied. In-batch getAfter composition and failure codes: For agents.

Subscribe, files, auth, queries

  • subscribe(path, opts) (client only), live reads; see Realtime & live.
  • setFile(path, file) / getFiles(path), R2 file storage; see Files & search.
  • login / logout / getCurrentUser / useAuth (React hook), plus loginWithRedirect / loginWithPopup for hosted email or social auth, signInAnonymously for guests, and loginWithPrivy for Privy, end-user auth; see Auth.
  • setPlatform / ReactNativeSessionManager, the React Native setup hooks; see React Native.
  • runQuery / runQueryMany / runExpression / runExpressionMany, execute declared policy queries and expressions.

@bounded-sh/server

The server client wraps the same operations, signed by a keypair, with no browser auth. Each client has its own session.

server client
import { init, createWalletClient } from "@bounded-sh/server";

await init({ appId: "<appId>" });
const vault = await createWalletClient({ keypair: process.env.VAULT_KEY! });
vault.address;                                   // the signer's wallet (== @user.address == @user.id)
await vault.set("markets/123", { open: true });
await vault.setMany([ /* atomic batch */ ]);
const doc = await vault.get("markets/123");

vault exposes get, getMany, set, setMany, setFile, getFiles, search, queryAggregate, count, aggregate, runQuery/runExpression (and their *Many variants). App collaborators are a control-plane concern managed with bounded share, not SDK methods. keypair is a base58 string or JSON array secret key.

Verifying webhooks, verifyWebhook

@bounded-sh/server exports verifyWebhook for inbound mutation webhooks: it fetches + caches Bounded’s Ed25519 public key, checks the signature over the raw body, and enforces timestamp skew, returning the typed payload or throwing WebhookVerificationError.

verifyWebhook
import { verifyWebhook } from "@bounded-sh/server";
import { replayStore } from "./shared-webhook-replay-store"; // Redis/KV/DB uniqueness

// rawBody is the unparsed request body string; headers is the request headers.
const event = await verifyWebhook(rawBody, headers, {
  expectedAppId: process.env.BOUNDED_APP_ID!,
  replayStore,
});
// event: { id, appId, path, operation, document, previousDocument, timestamp }

Always pin expectedAppId so a valid webhook from another Bounded app cannot target this receiver. The default replay cache is process-local; multi-instance deployments should implement the exported WebhookReplayStore contract with an atomic shared store or database uniqueness constraint.

Invoking a function

The client exports functions.invoke(name, args). It attaches the active session token, then the dispatcher verifies the caller and evaluates the function’s auth rule before running it. A server keypair invokes through its scoped wallet client:

invoke from a client or server
import { functions } from "@bounded-sh/client";
import { init as initServer, createWalletClient } from "@bounded-sh/server";

const result = await functions.invoke("syncStripe", { customerId, targetUserId });

// Server/keypair identity:
await initServer({ appId: "<appId>" });
const vault = await createWalletClient({ keypair: process.env.VAULT_KEY! });
const serverResult = await vault.invoke("syncStripe", { customerId, targetUserId });
// -> function JSON, or a typed invocation error.

Full guide: Functions.