Skip to content

SDK — excalibase/sdk

Pre-release

@excalibase/sdk v0.1 is in active development. Install from GitHub for now; npm publish is scheduled with the next platform release.

Official TypeScript client for Excalibase. Single package covers auth, GraphQL, REST, and Functions (typed RPC) surfaces with helpers and a persistent refresh-token session.

excalibase/excalibase-sdk-js

Why a first-party SDK

  • Auth done right — password login, refresh-before-expiry, OAuth2 /token flow, API keys. No manual JWT wrangling.
  • One client for every protocol — pick GraphQL, REST, or typed Functions (RPC) per call; same auth, same headers, same error types.
  • Typed search / vector — search, webSearch, vectorSearch are first-class; autocomplete on the operator shape.
  • Works in the browser and Node (≥18). ESM + CJS. Sub-10 KB gzipped core.
  • Codegen — excalibase-codegen points at your running server and emits TypeScript types for every table. Your client code becomes schema-aware.

Install

npm install @excalibase/sdk graphql-request

Create a client

import { createClient, localStorageAdapter } from "@excalibase/sdk";

const db = createClient({
  url: "https://api.example.com",
  projectId: "acme/prod",
  publishableKey: "esk_pub_...",
  storage: localStorageAdapter(),  // or memoryStorageAdapter() for SSR
  autoRefreshToken: true,
});

Browser origins

In the browser the request's Origin is set by the browser, not the SDK. The project must list the origins your app is served from (including http://localhost:<port> for local dev) or the call is blocked by CORS — see CORS (per-project origins).

Auth

await db.auth.signUp({ email, password, fullName });
await db.auth.signInWithPassword({ email, password });
await db.auth.signOut();

db.auth.onAuthStateChange((event, session) => { /* ... */ });

Supports API key exchange, OAuth2-style /token, and automatic refresh before JWT expiry. See the SDK README for the full surface.

GraphQL

const issues = await db.graphql.query<{ kanbanIssues: Issue[] }>(`
  {
    kanbanIssues(
      where: { status: { eq: "open" }, search_vec: { search: "stripe" } },
      limit: 10
    ) { id title priority }
  }
`);

await db.graphql.mutation<{ createKanbanIssue: Issue }>(`
  mutation ($input: CreateKanbanIssueInput!) {
    createKanbanIssue(input: $input) { id }
  }
`, { input: { title: "..." } });

Vector k-NN:

const nearby = await db.graphql.query(`
  {
    kanbanIssues(
      vector: { column: "embedding", near: [0.1, 0.2, 0.3], distance: "COSINE", limit: 5 }
    ) { id title }
  }
`);

REST

// GET /api/v1/rest/customer?status=eq.active&limit=10
const customers = await db.rest.get("customer", {
  query: { status: "eq.active", limit: 10 },
});

// POST
await db.rest.post("customer", { body: { name: "Vu", status: "active" } });

Functions (typed RPC)

Deployed functions are called as db.functions.<module>.<name>(args). Functions hold logic; the data stays behind the project's REST and GraphQL API shown above. A function has no ctx.db: when it needs data it calls that API with a project key kept as a function secret.

// deployed with id "users.welcome" (server-side)
import { z } from "zod";
import { mutation } from "@excalibase/server";

export default mutation({
  args: z.object({ email: z.string() }),
  handler: async (ctx, { email }) => {
    await ctx.scheduler.runAfter(0, { moduleName: "mail", exportName: "default" }, { to: email });
    return { queued: true };
  },
});

Then from the client:

await db.functions.users.welcome({ email: "vu@acme.com" });

Codegen

Point excalibase-codegen at a running server and regenerate types whenever your schema changes:

npx excalibase-codegen \
  --url http://localhost:10000 \
  --out src/generated/db.ts

Your generated Database type is the single source of truth for what operators and fields are available on your specific DB. The SDK's createClient<Database> picks up the generics and every query / filter is type-checked at compile time.

Errors

import { AuthError, NetworkError, ExcalibaseError } from "@excalibase/sdk";

try {
  await db.graphql.query("{ bogus }");
} catch (e) {
  if (e instanceof AuthError) { /* 401/403 */ }
  else if (e instanceof NetworkError) { /* transport failure */ }
  else if (e instanceof ExcalibaseError) { /* server error payload */ }
}

Error codes

Code Where Meaning
permission_check_failed GraphQL errors[].extensions.code; REST 403 body code A written row fails the role's permission check; the whole write rolled back. The message never contains database text. Reads are filtered silently and never raise.
permission_denied REST 403 body code The role holds no permission for this method's operation on a table it can see (or for Prefer: count=).
missing_session_variable / invalid_session_variable GraphQL errors[].extensions.code; REST 400 body code A permission needs a session variable the request lacks, or one its column type cannot hold.
permissions_unavailable 503, GraphQL-shaped body The project's permissions cannot be read right now.

See the error contract.

Realtime

WebSocket-based subscriptions land in a future SDK release — today the server exposes /api/v1/realtime and GraphQL subscriptions, and any graphql-ws or browser WebSocket client works. SDK-level db.realtime.channel(...).on(...) helpers are on the roadmap.

Roadmap

  • db.realtime helpers
  • Python SDK
  • Dart SDK (Flutter-first)
  • Typed filter builder variants per DB (PostgreSQL-only ops gated by Database generic)