JavaScript and TypeScript SDK
@vaifstudio/client is the VAIF Studio SDK for JavaScript and TypeScript. It is built on fetch, so the same package runs in browsers, Node.js 22 or later, Bun, Deno, edge runtimes and React Native.
Install
Section titled “Install”npm install @vaifstudio/clientFor React, add @vaifstudio/react. For React Native and Expo, add @vaifstudio/expo.
Create a client
Section titled “Create a client”import { createVaifClient } from "@vaifstudio/client";
const vaif = createVaifClient({ projectId: "your-project-id", apiKey: "vk_pub_xxx", // the publishable key: safe in a browser});| Option | Meaning |
|---|---|
projectId |
Your project ID. Data, auth and realtime need it |
apiKey |
A publishable key (vk_pub_…) in browsers and apps, a secret key (vk_srv_…) only on servers. See API key types |
baseUrl |
The API’s address. Defaults to https://api.vaif.studio |
accessToken |
The signed-in user’s token, as a string or a function that returns one. Row rules use it. vaif.setAuth(token) changes it later |
timeout |
Per-request timeout in milliseconds (default 30000) |
retry |
The retry policy (below) |
realtime |
Options for the shared realtime connection, such as your own WebSocket implementation |
refreshTokenMode |
Where a signed-in user’s refresh token lives: "auto" (default), "body" or "cookie" |
In a browser, the client warns in the console when it is given a vk_srv_ key, because that key bypasses row rules.
type Note = { id: string; title: string; n: number; done: boolean; created_at: string };
const notes = vaif.from<Note>("notes");
const { data: created } = await notes.create({ title: "Buy milk", n: 1, done: false });
const { data: page } = await vaif .from<Note>("notes") .eq("done", false) .orderBy("created_at", "desc") .limit(20) .list();
await notes.update(created.id, { done: true });await notes.delete(created.id);
const open = await vaif.from<Note>("notes").eq("done", false).count();console.log(page.length, open);Filters are eq, neq, gt, gte, lt, lte, like, ilike and in, and chained filters must all match. select(...columns) limits the columns returned. single() and maybeSingle() return { data, error } instead of throwing. More in Querying data.
await vaif.auth.signUp("sam@example.com", "a-long-unique-passphrase");
const result = await vaif.auth.login("sam@example.com", "a-long-unique-passphrase");
if ("challengeToken" in result) { // Two-factor sign-in is on for this user: ask for the 6-digit code. const session = await vaif.auth.mfa.verify(result.challengeToken, "123456"); vaif.setAuth(session.accessToken);} else if ("accessToken" in result) { vaif.setAuth(result.accessToken);}signUp and requestPasswordReset are for the users of your own app, so the client needs your projectId. VAIF Studio accounts are created in the console: on a client without projectId, both methods throw VaifConsoleOnlyError (code: "CONSOLE_ONLY") before sending anything.
The SDK does not store tokens. Keep the session where your app keeps secrets, or use the React or Expo provider, which stores and refreshes it for you. More in Authentication.
Storage
Section titled “Storage”import { upload } from "@vaifstudio/client/storage";
const { key, sizeBytes } = await upload(vaif, "avatars", "users/42/me.jpg", file, { onProgress: (e) => console.log(`${Math.round(e.fraction * 100)}%`),});
const url = await vaif.storage.createSignedUrl(key, 3600, { bucket: "avatars" });console.log(sizeBytes, url);upload() switches to a multipart upload for large files and can resume after a crash; see Uploads in depth. More in File storage.
Functions
Section titled “Functions”const result = await vaif.functions.invoke<{ sent: boolean }>("send-email", { body: { to: "sam@example.com", subject: "Welcome" },});console.log(result.status, result.data.sent);invoke is a POST and is not retried unless you opt in with { retry: { maxRetries } }. Opt in only for functions that are safe to run twice.
Realtime
Section titled “Realtime”const subscription = vaif .from("messages") .eq("room_id", "room-42") .on("INSERT", (event) => console.log(event.new)) .subscribe();
// Latersubscription.unsubscribe();Row changes, broadcast and presence share one authenticated connection per client. More in Realtime.
Entry points
Section titled “Entry points”| Import | What it contains |
|---|---|
@vaifstudio/client |
The client: data, auth, storage, functions and realtime, plus the error classes |
@vaifstudio/client/storage |
upload() with multipart, progress, cancellation and localStorageResumeStore() |
@vaifstudio/client/storage/browser |
Browser helpers: uploadFromInput() and createDropZone() |
@vaifstudio/client/operations |
createOperationsClient(): one typed method for every other API route |
Errors, timeouts and retries
Section titled “Errors, timeouts and retries”Every failure is a VaifError subclass with code, statusCode and requestId:
| Class | When |
|---|---|
VaifAuthError |
401 or 403, or a refused realtime connection |
VaifValidationError |
400 or 422 |
VaifNotFoundError |
404 |
VaifConflictError |
409 |
VaifRateLimitError |
429, with retryAfter in seconds |
VaifNetworkError, VaifTimeoutError |
No response, or the request timed out |
VaifNotImplementedError |
A method this API does not serve |
VaifConsoleOnlyError |
Account sign-up or reset, which only the console does |
import { isVaifNotFoundError } from "@vaifstudio/client";
try { await vaif.from("notes").get("missing-id");} catch (error) { if (isVaifNotFoundError(error)) console.log("no such note"); else throw error;}Only GET, HEAD and OPTIONS requests are retried automatically, up to 3 times on 500, 502, 503 and 504 responses and on network errors, with exponential backoff. A 429 is retried at most once, never before its Retry-After. Writes are never replayed. See Error codes for the error body every response uses.