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
| Import | Runs in | Adds |
|---|---|---|
@bounded-sh/client | browser + React Native | end-user auth (email/wallet), live subscriptions, the live runtime client, function invocation |
@bounded-sh/server | a server | keypair signing (no browser auth), createWalletClient, verifyWebhook |
npm i @bounded-sh/client # browser / React Native
npm i @bounded-sh/server # Node keypair / server// 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.
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 readGetOptions: filter, sort ({ field: 1 | -1 }), limit, cursor, includeSubPaths, shape, prompt, bypassCache.
Search & aggregate
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.
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 commitsA 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), plusloginWithRedirect/loginWithPopupfor hosted email or social auth,signInAnonymouslyfor guests, andloginWithPrivyfor 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.
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.
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:
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.